124 Commits

Author SHA1 Message Date
Vidar Holen
21462b11b3 Merge branch 'doctest-new-build' of https://github.com/phadej/shellcheck into phadej-doctest-new-build 2018-12-16 18:33:21 -08:00
Vidar Holen
88aef838f1 SC1068 (var = x) now alternatively suggests quoting (fixes #1412) 2018-12-16 15:45:52 -08:00
Vidar Holen
3d61b73e91 Be more specific about why you should read the wiki page 2018-12-16 15:14:29 -08:00
Vidar Holen
138080bdc7 Fix infinite loop on annotations for SC2188 (fixes #1413) 2018-12-16 14:42:19 -08:00
Vidar Holen
5b3f17c29d Allow tests to access token positions for fixes 2018-12-16 13:17:59 -08:00
Vidar Holen
b47e083ee3 Fix 'does not support multiple targets at once' error 2018-12-16 10:15:32 -08:00
Vidar Holen
3cba76dc7d Update CHANGELOG with new release and autofix merge 2018-12-09 15:01:08 -08:00
Vidar Holen
eb588f62f6 Enable autofix support. It's still preliminary. 2018-12-09 15:01:08 -08:00
Vidar Holen
bcd13614eb Improve Fix memory usage 2018-12-09 15:01:08 -08:00
Vidar Holen
a8376a09a9 Minor renaming and output fixes 2018-12-09 15:01:08 -08:00
Ng Zhi An
5ed89d2241 Change definition of Replacement, add ToJSON instance for it 2018-12-09 15:01:08 -08:00
Ng Zhi An
4a87d2a3de Expose token positions in params, use that to construct fixes 2018-12-09 15:01:08 -08:00
Ng Zhi An
41613babd9 Prototype fix 2018-12-09 15:01:08 -08:00
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
Oleg Grenrus
75949fe51e Change prop> to >>> prop $
As doctest doesn't make QuickCheck related magic, but only
evaluates expressions: We are fast.
2018-10-17 19:29:10 +03: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
Oleg Grenrus
d510a3ef6c Guard quickcheck.1 regeneration
Without this guard it will be regenerate on each run,
and cabal new-build considers whole package dirty.
2018-09-10 19:44:06 +03:00
Oleg Grenrus
5516596b26 Fix quicktest, add note to it and quickrun 2018-09-10 19:41:15 +03:00
Oleg Grenrus
9e7539c10b Use native codegen in docker build, results in smaller binary 2018-09-10 19:09:28 +03:00
Oleg Grenrus
a5a7b332f1 Use LLVM + split-sections 2018-09-10 19:09:28 +03:00
Oleg Grenrus
a68e3aeb26 Striptests is not necessary anymore 2018-09-10 19:09:28 +03:00
Oleg Grenrus
259b1a5dc6 Run tests as doctests 2018-09-10 19:09:28 +03: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
37 changed files with 3260 additions and 2093 deletions

View File

@@ -1,8 +1,8 @@
#### For bugs #### For bugs
- Rule Id (if any, e.g. SC1000): - Rule Id (if any, e.g. SC1000):
- My shellcheck version (`shellcheck --version` or "online"): - My shellcheck version (`shellcheck --version` or "online"):
- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
- [ ] I tried on shellcheck.net and verified that this is still a problem on the latest commit - [ ] I tried on 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 #### For new checks and feature suggestions
- [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this - [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this

3
.gitignore vendored
View File

@@ -13,6 +13,9 @@ cabal-dev
cabal.sandbox.config cabal.sandbox.config
cabal.config cabal.config
.stack-work .stack-work
dist-newstyle/
.ghc.environment.*
cabal.project.local
### Snap ### ### Snap ###
/snap/.snapcraft/ /snap/.snapcraft/

View File

@@ -27,7 +27,7 @@ do
zip "${file%.*}.zip" README.txt LICENSE.txt "$file" zip "${file%.*}.zip" README.txt LICENSE.txt "$file"
done done
for file in *.linux for file in *.linux-x86_64
do do
base="${file%.*}" base="${file%.*}"
cp "$file" "shellcheck" cp "$file" "shellcheck"
@@ -35,6 +35,14 @@ do
rm "shellcheck" rm "shellcheck"
done 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 ./* for file in ./*
do do
sha512sum "$file" > "$file.sha512sum" 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

@@ -11,6 +11,7 @@ before_install:
- TAGS="" - TAGS=""
- test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true - test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
- test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true - test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
- echo "Tags are $TAGS"
script: script:
- mkdir deploy - mkdir deploy
@@ -29,18 +30,21 @@ script:
- docker rm "$id" - docker rm "$id"
- ls -l shellcheck - ls -l shellcheck
- ./shellcheck myscript - ./shellcheck myscript
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux"; done - for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux-x86_64"; done
# Linux Alpine based Docker image # Linux Alpine based Docker image
- name="$DOCKER_BASE-alpine" - name="$DOCKER_BASE-alpine"
- DOCKER_BUILDS="$DOCKER_BUILDS $name" - DOCKER_BUILDS="$DOCKER_BUILDS $name"
- sed -e '/DELETE-MARKER/,$d' Dockerfile > Dockerfile.alpine - sed -e '/DELETE-MARKER/,$d' Dockerfile > Dockerfile.alpine
- docker build -f Dockerfile.alpine -t "$name:current" . - docker build -f Dockerfile.alpine -t "$name:current" .
- docker run "$name:current" sh -c 'shellcheck --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 # Windows .exe
- docker pull koalaman/winghc - docker run --user="$UID" -v "$PWD:/appdata" koalaman/winghc cuib
- 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 - for tag in $TAGS; do cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe"; done
- rm -rf dist || true - rm -rf dist shellcheck || true
# Misc packaging # Misc packaging
- ./.prepare_deploy - ./.prepare_deploy

View File

@@ -1,3 +1,31 @@
## Since previous release
### Added
- Preliminary support for fix suggestions
## 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 ## v0.5.0 - 2018-05-31
### Added ### Added
- SC2233/SC2234/SC2235: Suggest removing or replacing (..) around tests - SC2233/SC2234/SC2235: Suggest removing or replacing (..) around tests

View File

@@ -1,22 +1,55 @@
# Build-only image # Build-only image
FROM ubuntu:17.10 AS build FROM ubuntu:18.04 AS build
USER root USER root
WORKDIR /opt/shellCheck WORKDIR /opt/shellCheck
# Install OS deps # Install OS deps, including GHC from HVR-PPA
RUN apt-get update && apt-get install -y ghc cabal-install # https://launchpad.net/~hvr/+archive/ubuntu/ghc
RUN apt-get -yq update \
&& apt-get -yq install software-properties-common \
&& apt-add-repository -y "ppa:hvr/ghc" \
&& apt-get -yq update \
&& apt-get -yq install cabal-install-2.4 ghc-8.4.3 pandoc \
&& rm -rf /var/lib/apt/lists/*
ENV PATH="/opt/ghc/bin:${PATH}"
# Use gold linker and check tools versions
RUN ln -s $(which ld.gold) /usr/local/bin/ld && \
cabal --version \
&& ghc --version \
&& ld --version
# Install Haskell deps # Install Haskell deps
# (This is a separate copy/run so that source changes don't require rebuilding) # (This is a separate copy/run so that source changes don't require rebuilding)
#
# We also patch regex-tdfa and aeson removing hard-coded -O2 flag.
# This makes compilation faster and binary smaller.
# Performance loss is unnoticeable for ShellCheck
#
# Remember to update versions, once in a while.
COPY ShellCheck.cabal ./ COPY ShellCheck.cabal ./
RUN cabal update && cabal install --dependencies-only RUN cabal update && \
cabal get regex-tdfa-1.2.3.1 && sed -i 's/-O2//' regex-tdfa-1.2.3.1/regex-tdfa.cabal && \
cabal get aeson-1.4.0.0 && sed -i 's/-O2//' aeson-1.4.0.0/aeson.cabal && \
echo 'packages: . regex-tdfa-1.2.3.1 aeson-1.4.0.0 > cabal.project' && \
cabal new-build --dependencies-only \
--disable-executable-dynamic --enable-split-sections --disable-tests
# Copy source and build it # Copy source and build it
COPY LICENSE Setup.hs shellcheck.hs ./ COPY LICENSE Setup.hs shellcheck.hs shellcheck.1.md ./
COPY src src COPY src src
RUN cabal build Paths_ShellCheck && \ COPY test test
ghc -optl-static -optl-pthread -isrc -idist/build/autogen --make shellcheck && \ # This SED is the only "nastyness" we have to do
strip --strip-all shellcheck # Hopefully soon we could add per-component ld-options to cabal.project
RUN sed -i 's/-- STATIC/ld-options: -static -pthread -Wl,--gc-sections/' ShellCheck.cabal && \
cat ShellCheck.cabal && \
cabal new-build \
--disable-executable-dynamic --enable-split-sections --disable-tests && \
cp $(find dist-newstyle -type f -name shellcheck) . && \
strip --strip-all shellcheck && \
file shellcheck && \
ls -l shellcheck
RUN mkdir -p /out/bin && \ RUN mkdir -p /out/bin && \
cp shellcheck /out/bin/ cp shellcheck /out/bin/

10
LICENSE
View File

@@ -1,3 +1,13 @@
Employer mandated disclaimer:
I am providing code in the repository to you under an open source license.
Because this is my personal repository, the license you receive to my code is
from me and other individual contributors, and not my employer (Facebook).
- Vidar "koala_man" Holen
----
GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007 Version 3, 29 June 2007

View File

@@ -4,7 +4,7 @@
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts: 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 The goals of ShellCheck are
@@ -70,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). ![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). ![Screenshot of emacs showing inlined shellcheck feedback](doc/emacs-flycheck.png).
@@ -133,6 +133,10 @@ On OS X with homebrew:
brew install shellcheck brew install shellcheck
On OpenBSD:
pkg_add shellcheck
On openSUSE On openSUSE
zypper in ShellCheck zypper in ShellCheck
@@ -163,27 +167,35 @@ or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based imag
Alternatively, you can download pre-compiled binaries for the latest release here: Alternatively, you can download pre-compiled binaries for the latest release here:
* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.x86_64.tar.xz) (statically linked) * [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) * [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, older versions and the latest daily builds. or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds.
Distro packages already come with a `man` page. If you are building from source, it can be installed with:
pandoc -s -t man shellcheck.1.md -o shellcheck.1
sudo mv shellcheck.1 /usr/share/man/man1
## Travis CI ## Travis CI
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it. Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
If you still want to do so in order to upgrade at your leisure or ensure the latest release: 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.
install: ## Installing the shellcheck binary
# Install a custom version of shellcheck instead of Travis CI's default *Pre-requisite*: the program 'xz' needs to be installed on the system.
- scversion="stable" # or "v0.4.7", or "latest" To install it on debian/ubuntu/linux mint, run `apt install xz-utils`.
- wget "https://storage.googleapis.com/shellcheck/shellcheck-$scversion.linux.x86_64.tar.xz" To install it on Redhat/Fedora/CentOS, run `yum -y install xz`.
- tar --xz -xvf "shellcheck-$scversion.linux.x86_64.tar.xz"
- shellcheck() { "shellcheck-$scversion/shellcheck" "$@"; }
- shellcheck --version
script: ```bash
- shellcheck *.sh 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 ## Compiling from source
@@ -442,3 +454,8 @@ ShellCheck is licensed under the GNU General Public License, v3. A copy of this
Copyright 2012-2018, Vidar 'koala_man' Holen and contributors. Copyright 2012-2018, Vidar 'koala_man' Holen and contributors.
Happy ShellChecking! 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

@@ -1,3 +1,8 @@
{-# LANGUAGE CPP #-}
{-# OPTIONS_GHC -Wall #-}
module Main (main) where
import Distribution.PackageDescription ( import Distribution.PackageDescription (
HookedBuildInfo, HookedBuildInfo,
emptyHookedBuildInfo ) emptyHookedBuildInfo )
@@ -9,12 +14,42 @@ import Distribution.Simple (
import Distribution.Simple.Setup ( SDistFlags ) import Distribution.Simple.Setup ( SDistFlags )
import System.Process ( system ) import System.Process ( system )
import System.Directory ( doesFileExist, getModificationTime )
#ifndef MIN_VERSION_cabal_doctest
#define MIN_VERSION_cabal_doctest(x,y,z) 0
#endif
#if MIN_VERSION_cabal_doctest(1,0,0)
import Distribution.Extra.Doctest ( addDoctestsUserHook )
main :: IO ()
main = defaultMainWithHooks $ addDoctestsUserHook "doctests" myHooks
where
myHooks = simpleUserHooks { preSDist = myPreSDist }
#else
#ifdef MIN_VERSION_Cabal
-- If the macro is defined, we have new cabal-install,
-- but for some reason we don't have cabal-doctest in package-db
--
-- Probably we are running cabal sdist, when otherwise using new-build
-- workflow
#warning You are configuring this package without cabal-doctest installed. \
The doctests test-suite will not work as a result. \
To fix this, install cabal-doctest before configuring.
#endif
main :: IO ()
main = defaultMainWithHooks myHooks main = defaultMainWithHooks myHooks
where where
myHooks = simpleUserHooks { preSDist = myPreSDist } myHooks = simpleUserHooks { preSDist = myPreSDist }
#endif
-- | This hook will be executed before e.g. @cabal sdist@. It runs -- | This hook will be executed before e.g. @cabal sdist@. It runs
-- pandoc to create the man page from shellcheck.1.md. If the pandoc -- pandoc to create the man page from shellcheck.1.md. If the pandoc
-- command is not found, this will fail with an error message: -- command is not found, this will fail with an error message:
@@ -27,10 +62,20 @@ main = defaultMainWithHooks myHooks
-- --
myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo
myPreSDist _ _ = do myPreSDist _ _ = do
putStrLn "Building the man page (shellcheck.1) with pandoc..." exists <- doesFileExist "shellcheck.1"
putStrLn pandoc_cmd if exists
result <- system pandoc_cmd then do
putStrLn $ "pandoc exited with " ++ show result source <- getModificationTime "shellcheck.1.md"
target <- getModificationTime "shellcheck.1"
if target < source
then makeManPage
else putStrLn "shellcheck.1 is more recent than shellcheck.1.md"
else makeManPage
return emptyHookedBuildInfo return emptyHookedBuildInfo
where where
makeManPage = do
putStrLn "Building the man page (shellcheck.1) with pandoc..."
putStrLn pandoc_cmd
result <- system pandoc_cmd
putStrLn $ "pandoc exited with " ++ show result
pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1" pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"

View File

@@ -1,5 +1,5 @@
Name: ShellCheck Name: ShellCheck
Version: 0.5.0 Version: 0.6.0
Synopsis: Shell script analysis tool Synopsis: Shell script analysis tool
License: GPL-3 License: GPL-3
License-file: LICENSE License-file: LICENSE
@@ -28,14 +28,14 @@ Extra-Source-Files:
shellcheck.1.md shellcheck.1.md
-- built with a cabal sdist hook -- built with a cabal sdist hook
shellcheck.1 shellcheck.1
-- tests
test/shellcheck.hs
custom-setup custom-setup
setup-depends: setup-depends:
base >= 4 && <5, base >= 4 && <5,
process >= 1.0 && <1.7, directory >= 1.2 && <1.4,
Cabal >= 1.10 && <2.3 process >= 1.0 && <1.7,
cabal-doctest >= 1.0.6 && <1.1,
Cabal >= 1.10 && <2.5
source-repository head source-repository head
type: git type: git
@@ -53,11 +53,11 @@ library
base > 4.6.0.1 && < 5, base > 4.6.0.1 && < 5,
bytestring, bytestring,
containers >= 0.5, containers >= 0.5,
deepseq >= 1.4.0.0,
directory, directory,
mtl >= 2.2.1, mtl >= 2.2.1,
parsec, parsec,
regex-tdfa, regex-tdfa,
QuickCheck >= 2.7.4,
-- When cabal supports it, move this to setup-depends: -- When cabal supports it, move this to setup-depends:
process process
exposed-modules: exposed-modules:
@@ -89,27 +89,29 @@ executable shellcheck
aeson, aeson,
base >= 4 && < 5, base >= 4 && < 5,
bytestring, bytestring,
deepseq >= 1.4.0.0,
ShellCheck, ShellCheck,
containers, containers,
directory, directory,
mtl >= 2.2.1, mtl >= 2.2.1,
parsec >= 3.0, parsec >= 3.0,
QuickCheck >= 2.7.4,
regex-tdfa regex-tdfa
main-is: shellcheck.hs main-is: shellcheck.hs
test-suite test-shellcheck -- Marker to add flags for static linking
type: exitcode-stdio-1.0 -- STATIC
build-depends:
aeson,
base >= 4 && < 5,
bytestring,
ShellCheck,
containers,
directory,
mtl >= 2.2.1,
parsec,
QuickCheck >= 2.7.4,
regex-tdfa
main-is: test/shellcheck.hs
test-suite doctests
type: exitcode-stdio-1.0
main-is: doctests.hs
build-depends:
base,
doctest >= 0.16.0 && <0.17,
QuickCheck >=2.11 && <2.13,
ShellCheck,
template-haskell
x-doctest-options: --fast
ghc-options: -Wall -threaded
hs-source-dirs: test

View File

@@ -1,5 +1,12 @@
#!/bin/bash #!/usr/bin/env bash
# quickrun runs ShellCheck in an interpreted mode. # quickrun runs ShellCheck in an interpreted mode.
# This allows testing changes without recompiling. # This allows testing changes without recompiling.
runghc -isrc -idist/build/autogen shellcheck.hs "$@" runghc -isrc -idist/build/autogen shellcheck.hs "$@"
# Note: with new-build you can
#
# % cabal new-run --disable-optimization -- shellcheck "$@"
#
# This does build the executable, but as the optimisation is disabled,
# the build is quite fast.

View File

@@ -1,22 +1,21 @@
#!/bin/bash #!/bin/bash
# quicktest runs the ShellCheck unit tests in an interpreted mode. # shellcheck disable=SC2091
# This allows running tests without compiling, which can be faster.
# quicktest runs the ShellCheck unit tests.
# Once `doctests` test executable is build, we can just run it
# This allows running tests without compiling library, which is faster.
# 'cabal test' remains the source of truth. # 'cabal test' remains the source of truth.
( $(find dist -type f -name doctests)
var=$(echo 'liftM and $ sequence [
ShellCheck.Analytics.runTests # Note: if you have build the project with new-build
,ShellCheck.Parser.runTests #
,ShellCheck.Checker.runTests # % cabal new-build -w ghc-8.4.3 --enable-tests
,ShellCheck.Checks.Commands.runTests #
,ShellCheck.Checks.ShellSupport.runTests # and have cabal-plan installed (e.g. with cabal new-install cabal-plan),
,ShellCheck.AnalyzerLib.runTests # then you can quicktest with
]' | tr -d '\n' | cabal repl 2>&1 | tee /dev/stderr) #
if [[ $var == *$'\nTrue'* ]] # % $(cabal-plan list-bin doctests)
then #
exit 0 # Once the test executable exists, we can simply run it to perform doctests
else # which use GHCi under the hood.
grep -C 3 -e "Fail" -e "Tracing" <<< "$var"
exit 1
fi
) 2>&1

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** standard output. Subsequent **-f** options are ignored, see **FORMATS**
below for more information. 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* **-s**\ *shell*,\ **--shell=***shell*
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*. : Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
@@ -66,6 +71,11 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
: Print version information and exit. : 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** **-x**,\ **--external-sources**
: Follow 'source' statements even when the file is not specified as input. : Follow 'source' statements even when the file is not specified as input.

View File

@@ -67,15 +67,17 @@ instance Monoid Status where
data Options = Options { data Options = Options {
checkSpec :: CheckSpec, checkSpec :: CheckSpec,
externalSources :: Bool, externalSources :: Bool,
formatterOptions :: FormatterOptions formatterOptions :: FormatterOptions,
minSeverity :: Severity
} }
defaultOptions = Options { defaultOptions = Options {
checkSpec = emptyCheckSpec, checkSpec = emptyCheckSpec,
externalSources = False, externalSources = False,
formatterOptions = FormatterOptions { formatterOptions = newFormatterOptions {
foColorOption = ColorAuto foColorOption = ColorAuto
} },
minSeverity = StyleC
} }
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..." usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
@@ -93,8 +95,14 @@ options = [
Option "s" ["shell"] Option "s" ["shell"]
(ReqArg (Flag "shell") "SHELLNAME") (ReqArg (Flag "shell") "SHELLNAME")
"Specify dialect (sh, bash, dash, ksh)", "Specify dialect (sh, bash, dash, ksh)",
Option "S" ["severity"]
(ReqArg (Flag "severity") "SEVERITY")
"Minimum severity of errors to consider (error, warning, info, style)",
Option "V" ["version"] Option "V" ["version"]
(NoArg $ Flag "version" "true") "Print version information", (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"] Option "x" ["external-sources"]
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES" (NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES"
] ]
@@ -137,12 +145,6 @@ split char str =
else split' rest (a:element) else split' rest (a:element)
split' [] element = [reverse 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 toStatus = fmap (either id id) . runExceptT
getEnvArgs = do getEnvArgs = do
@@ -222,12 +224,28 @@ runFormatter sys format options files = do
then NoProblems then NoProblems
else SomeProblems else SomeProblems
parseColorOption colorOption = parseEnum name value list =
case colorOption of case filter ((== value) . fst) list of
"auto" -> ColorAuto [(name, value)] -> return value
"always" -> ColorAlways [] -> do
"never" -> ColorNever printErr $ "Unknown value for --" ++ name ++ ". " ++
_ -> error $ "Bad value for --color `" ++ colorOption ++ "'" "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 = parseOption flag options =
case flag of case flag of
@@ -241,7 +259,7 @@ parseOption flag options =
} }
Flag "exclude" str -> do Flag "exclude" str -> do
new <- mapM parseNum $ split ',' str new <- mapM parseNum $ filter (not . null) $ split ',' str
let old = csExcludedWarnings . checkSpec $ options let old = csExcludedWarnings . checkSpec $ options
return options { return options {
checkSpec = (checkSpec options) { checkSpec = (checkSpec options) {
@@ -258,10 +276,11 @@ parseOption flag options =
externalSources = True externalSources = True
} }
Flag "color" color -> Flag "color" color -> do
option <- parseColorOption color
return options { return options {
formatterOptions = (formatterOptions options) { formatterOptions = (formatterOptions options) {
foColorOption = parseColorOption color foColorOption = option
} }
} }
@@ -272,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 _ -> return options
where where
die s = do die s = do
@@ -280,7 +315,7 @@ parseOption flag options =
parseNum ('S':'C':str) = parseNum str parseNum ('S':'C':str) = parseNum str
parseNum num = do parseNum num = do
unless (all isDigit num) $ do unless (all isDigit num) $ do
printErr $ "Bad exclusion: " ++ num printErr $ "Invalid number: " ++ num
throwError SyntaxFailure throwError SyntaxFailure
return (Prelude.read num :: Integer) return (Prelude.read num :: Integer)

View File

@@ -37,9 +37,16 @@ parts:
source: ./ source: ./
build-packages: build-packages:
- cabal-install - cabal-install
- squid3
build: | build: |
# See comments in .snapsquid.conf
[ "$http_proxy" ] && {
squid3 -f .snapsquid.conf
export http_proxy="http://localhost:8888"
sleep 3
}
cabal sandbox init cabal sandbox init
cabal update cabal update || cat /var/log/squid/*
cabal install -j cabal install -j
install: | install: |
install -d $SNAPCRAFT_PART_INSTALL/usr/bin install -d $SNAPCRAFT_PART_INSTALL/usr/bin

View File

@@ -17,14 +17,17 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
module ShellCheck.AST where module ShellCheck.AST where
import GHC.Generics (Generic)
import Control.Monad.Identity import Control.Monad.Identity
import Control.DeepSeq
import Text.Parsec import Text.Parsec
import qualified ShellCheck.Regex as Re import qualified ShellCheck.Regex as Re
import Prelude hiding (id) import Prelude hiding (id)
newtype Id = Id Int deriving (Show, Eq, Ord) newtype Id = Id Int deriving (Show, Eq, Ord, Generic, NFData)
data Quoted = Quoted | Unquoted deriving (Show, Eq) data Quoted = Quoted | Unquoted deriving (Show, Eq)
data Dashed = Dashed | Undashed deriving (Show, Eq) data Dashed = Dashed | Undashed deriving (Show, Eq)

View File

@@ -23,6 +23,7 @@ import ShellCheck.AST
import Control.Monad.Writer import Control.Monad.Writer
import Control.Monad import Control.Monad
import Data.Char
import Data.Functor import Data.Functor
import Data.List import Data.List
import Data.Maybe import Data.Maybe
@@ -226,8 +227,43 @@ getLiteralStringExt more = g
g (T_SingleQuoted _ s) = return s g (T_SingleQuoted _ s) = return s
g (T_Literal _ s) = return s g (T_Literal _ s) = return s
g (T_ParamSubSpecialChar _ s) = return s g (T_ParamSubSpecialChar _ s) = return s
g (T_DollarSingleQuoted _ s) = return $ decodeEscapes s
g x = more x 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? -- Is this token a string literal?
isLiteral t = isJust $ getLiteralString t isLiteral t = isJust $ getLiteralString t
@@ -257,17 +293,27 @@ getCommand t =
T_Annotation _ _ t -> getCommand t T_Annotation _ _ t -> getCommand t
_ -> Nothing _ -> Nothing
-- Maybe get the command name of a token representing a command -- Maybe get the command name string of a token representing a command
getCommandName t = do 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 (T_SimpleCommand _ _ (w:rest)) <- getCommand t
s <- getLiteralString w s <- getLiteralString w
if "busybox" `isSuffixOf` s || "builtin" == s if "busybox" `isSuffixOf` s || "builtin" == s
then then
case rest of case rest of
(applet:_) -> getLiteralString applet (applet:_) -> return (getLiteralString applet, applet)
_ -> return s _ -> return (Just s, w)
else else
return s return (Just s, w)
-- If a command substitution is a single command, get its name. -- If a command substitution is a single command, get its name.
-- $(date +%s) = Just "date" -- $(date +%s) = Just "date"

File diff suppressed because it is too large Load Diff

View File

@@ -30,7 +30,7 @@ import qualified ShellCheck.Checks.ShellSupport
-- TODO: Clean up the cruft this is layered on -- TODO: Clean up the cruft this is layered on
analyzeScript :: AnalysisSpec -> AnalysisResult analyzeScript :: AnalysisSpec -> AnalysisResult
analyzeScript spec = AnalysisResult { analyzeScript spec = newAnalysisResult {
arComments = arComments =
filterByAnnotation spec params . nub $ filterByAnnotation spec params . nub $
runAnalytics spec runAnalytics spec

View File

@@ -18,7 +18,6 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.AnalyzerLib where module ShellCheck.AnalyzerLib where
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.ASTLib import ShellCheck.ASTLib
@@ -28,6 +27,7 @@ import ShellCheck.Parser
import ShellCheck.Regex import ShellCheck.Regex
import Control.Arrow (first) import Control.Arrow (first)
import Control.DeepSeq
import Control.Monad.Identity import Control.Monad.Identity
import Control.Monad.RWS import Control.Monad.RWS
import Control.Monad.State import Control.Monad.State
@@ -38,8 +38,9 @@ import qualified Data.Map as Map
import Data.Maybe import Data.Maybe
import Data.Semigroup import Data.Semigroup
import Test.QuickCheck.All (forAllProperties) prop :: Bool -> IO ()
import Test.QuickCheck.Test (maxSuccess, quickCheckWithResult, stdArgs) prop False = putStrLn "FAIL"
prop True = return ()
type Analysis = AnalyzerM () type Analysis = AnalyzerM ()
type AnalyzerM a = RWS Parameters [TokenComment] Cache a type AnalyzerM a = RWS Parameters [TokenComment] Cache a
@@ -81,8 +82,9 @@ data Parameters = Parameters {
parentMap :: Map.Map Id Token, -- A map from Id to parent Token parentMap :: Map.Map Id Token, -- A map from Id to parent Token
shellType :: Shell, -- The shell type, such as Bash or Ksh shellType :: Shell, -- The shell type, such as Bash or Ksh
shellTypeSpecified :: Bool, -- True if shell type was forced via flags 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
} tokenPositions :: Map.Map Id (Position, Position) -- map from token id to start and end position
} deriving (Show)
-- TODO: Cache results of common AST ops here -- TODO: Cache results of common AST ops here
data Cache = Cache {} data Cache = Cache {}
@@ -109,35 +111,42 @@ data DataSource =
data VariableState = Dead Token String | Alive deriving (Show) data VariableState = Dead Token String | Alive deriving (Show)
defaultSpec root = AnalysisSpec { defaultSpec pr = spec {
asScript = root,
asShellType = Nothing, asShellType = Nothing,
asCheckSourced = False, asCheckSourced = False,
asExecutionMode = Executed asExecutionMode = Executed,
} asTokenPositions = prTokenPositions pr
} where spec = newAnalysisSpec (fromJust $ prRoot pr)
pScript s = pScript s =
let let
pSpec = ParseSpec { pSpec = newParseSpec {
psFilename = "script", psFilename = "script",
psScript = s, psScript = s
psCheckSourced = False
} }
in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec in runIdentity $ parseScript (mockedSystemInterface []) pSpec
-- For testing. If parsed, returns whether there are any comments -- For testing. If parsed, returns whether there are any comments
producesComments :: Checker -> String -> Maybe Bool producesComments :: Checker -> String -> Maybe Bool
producesComments c s = do producesComments c s = do
root <- pScript s let pr = pScript s
let spec = defaultSpec root prRoot pr
let spec = defaultSpec pr
let params = makeParameters spec let params = makeParameters spec
return . not . null $ runChecker params c return . not . null $ runChecker params c
makeComment :: Severity -> Id -> Code -> String -> TokenComment makeComment :: Severity -> Id -> Code -> String -> TokenComment
makeComment severity id code note = makeComment severity id code note =
TokenComment id $ Comment severity code note newTokenComment {
tcId = id,
tcComment = newComment {
cSeverity = severity,
cCode = code,
cMessage = note
}
}
addComment note = tell [note] addComment note = note `deepseq` tell [note]
warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m () warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m ()
warn id code str = addComment $ makeComment WarningC id code str warn id code str = addComment $ makeComment WarningC id code str
@@ -145,6 +154,20 @@ err id code str = addComment $ makeComment ErrorC id code str
info id code str = addComment $ makeComment InfoC id code str info id code str = addComment $ makeComment InfoC id code str
style id code str = addComment $ makeComment StyleC id code str style id code str = addComment $ makeComment StyleC id code str
warnWithFix id code str fix = addComment $
let comment = makeComment WarningC id code str in
comment {
tcFix = Just fix
}
makeCommentWithFix :: Severity -> Id -> Code -> String -> Fix -> TokenComment
makeCommentWithFix severity id code str fix =
let comment = makeComment severity id code str
withFix = comment {
tcFix = Just fix
}
in withFix `deepseq` withFix
makeParameters spec = makeParameters spec =
let params = Parameters { let params = Parameters {
rootNode = root, rootNode = root,
@@ -159,7 +182,8 @@ makeParameters spec =
shellTypeSpecified = isJust $ asShellType spec, shellTypeSpecified = isJust $ asShellType spec,
parentMap = getParentTree root, parentMap = getParentTree root,
variableFlow = getVariableFlow params root variableFlow = getVariableFlow params root,
tokenPositions = asTokenPositions spec
} in params } in params
where root = asScript spec where root = asScript spec
@@ -191,16 +215,16 @@ containsLastpipe root =
_ -> False _ -> False
prop_determineShell0 = determineShell (fromJust $ pScript "#!/bin/sh") == Sh -- |
prop_determineShell1 = determineShell (fromJust $ pScript "#!/usr/bin/env ksh") == Ksh -- >>> prop $ determineShellTest "#!/bin/sh" == Sh
prop_determineShell2 = determineShell (fromJust $ pScript "") == Bash -- >>> prop $ determineShellTest "#!/usr/bin/env ksh" == Ksh
prop_determineShell3 = determineShell (fromJust $ pScript "#!/bin/sh -e") == Sh -- >>> prop $ determineShellTest "" == Bash
prop_determineShell4 = determineShell (fromJust $ pScript -- >>> prop $ determineShellTest "#!/bin/sh -e" == Sh
"#!/bin/ksh\n#shellcheck shell=sh\nfoo") == Sh -- >>> prop $ determineShellTest "#!/bin/ksh\n#shellcheck shell=sh\nfoo" == Sh
prop_determineShell5 = determineShell (fromJust $ pScript -- >>> prop $ determineShellTest "#shellcheck shell=sh\nfoo" == Sh
"#shellcheck shell=sh\nfoo") == Sh -- >>> prop $ determineShellTest "#! /bin/sh" == Sh
prop_determineShell6 = determineShell (fromJust $ pScript "#! /bin/sh") == Sh -- >>> prop $ determineShellTest "#! /bin/ash" == Dash
prop_determineShell7 = determineShell (fromJust $ pScript "#! /bin/ash") == Dash determineShellTest = determineShell . fromJust . prRoot . pScript
determineShell t = fromMaybe Bash $ do determineShell t = fromMaybe Bash $ do
shellString <- foldl mplus Nothing $ getCandidates t shellString <- foldl mplus Nothing $ getCandidates t
shellForExecutable shellString shellForExecutable shellString
@@ -235,9 +259,10 @@ getParentTree t =
where where
pre t = modify (first ((:) t)) pre t = modify (first ((:) t))
post t = do post t = do
(_:rest, map) <- get (x, map) <- get
case rest of [] -> put (rest, map) case x of
(x:_) -> put (rest, Map.insert (getId t) x map) _: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 -- Given a root node, make a map from Id to Token
getTokenMap :: Token -> Map.Map Id Token getTokenMap :: Token -> Map.Map Id Token
@@ -520,12 +545,22 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
getReferencedVariableCommand _ = [] 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)) = getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $ filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
case x of case x of
"read" -> "read" ->
let params = map getLiteral rest in let params = map getLiteral rest
catMaybes . takeWhile isJust . reverse $ params readArrayVars = getReadArrayVariables rest
in
catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params
"getopts" -> "getopts" ->
case rest of case rest of
opts:var:_ -> maybeToList $ getLiteral var opts:var:_ -> maybeToList $ getLiteral var
@@ -568,10 +603,14 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
where where
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
getLiteral t = do getLiteralOfDataType t d = do
s <- getLiteralString t s <- getLiteralString t
when ("-" `isPrefixOf` s) $ fail "argument" 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 getModifierParamString = getModifierParam DataString
@@ -613,6 +652,11 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
guard $ isVariableName name guard $ isVariableName name
return (base, lastArg, name, DataArray SourceExternal) 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 _ = [] getModifiedVariableCommand _ = []
getIndexReferences s = fromMaybe [] $ do getIndexReferences s = fromMaybe [] $ do
@@ -622,10 +666,11 @@ getIndexReferences s = fromMaybe [] $ do
where where
re = mkRegex "(\\[.*\\])" re = mkRegex "(\\[.*\\])"
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"] -- |
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"] -- >>> prop $ getOffsetReferences ":bar" == ["bar"]
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"] -- >>> prop $ getOffsetReferences ":bar:baz" == ["bar", "baz"]
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"] -- >>> prop $ getOffsetReferences "[foo]:bar" == ["bar"]
-- >>> prop $ getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
getOffsetReferences mods = fromMaybe [] $ do getOffsetReferences mods = fromMaybe [] $ do
-- if mods start with [, then drop until ] -- if mods start with [, then drop until ]
match <- matchRegex re mods match <- matchRegex re mods
@@ -700,9 +745,15 @@ isUnqualifiedCommand token str = isCommandMatch token (== str)
isCommandMatch token matcher = fromMaybe False $ isCommandMatch token matcher = fromMaybe False $
fmap matcher (getCommandName token) fmap matcher (getCommandName token)
-- |
-- Does this regex look like it was intended as a glob? -- Does this regex look like it was intended as a glob?
-- True: *foo* --
-- False: .*foo.* -- >>> isConfusedGlobRegex "*foo*"
-- True
--
-- >>> isConfusedGlobRegex ".*foo.*"
-- False
--
isConfusedGlobRegex :: String -> Bool isConfusedGlobRegex :: String -> Bool
isConfusedGlobRegex ('*':_) = True isConfusedGlobRegex ('*':_) = True
isConfusedGlobRegex [x,'*'] | x /= '\\' = True isConfusedGlobRegex [x,'*'] | x /= '\\' = True
@@ -712,9 +763,10 @@ isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
isVariableChar x = isVariableStartChar x || isDigit x isVariableChar x = isVariableStartChar x || isDigit x
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*" variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
prop_isVariableName1 = isVariableName "_fo123" -- |
prop_isVariableName2 = not $ isVariableName "4" -- >>> prop $ isVariableName "_fo123"
prop_isVariableName3 = not $ isVariableName "test: " -- >>> prop $ not $ isVariableName "4"
-- >>> prop $ not $ isVariableName "test: "
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
isVariableName _ = False isVariableName _ = False
@@ -723,27 +775,28 @@ getVariablesFromLiteralToken token =
-- Try to get referenced variables from a literal string like "$foo" -- Try to get referenced variables from a literal string like "$foo"
-- Ignores tons of cases like arithmetic evaluation and array indices. -- Ignores tons of cases like arithmetic evaluation and array indices.
prop_getVariablesFromLiteral1 = -- >>> prop $ getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
getVariablesFromLiteral string = getVariablesFromLiteral string =
map (!! 0) $ matchAllSubgroups variableRegex string map (!! 0) $ matchAllSubgroups variableRegex string
where where
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)" variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
-- |
-- Get the variable name from an expansion like ${var:-foo} -- Get the variable name from an expansion like ${var:-foo}
prop_getBracedReference1 = getBracedReference "foo" == "foo" --
prop_getBracedReference2 = getBracedReference "#foo" == "foo" -- >>> prop $ getBracedReference "foo" == "foo"
prop_getBracedReference3 = getBracedReference "#" == "#" -- >>> prop $ getBracedReference "#foo" == "foo"
prop_getBracedReference4 = getBracedReference "##" == "#" -- >>> prop $ getBracedReference "#" == "#"
prop_getBracedReference5 = getBracedReference "#!" == "!" -- >>> prop $ getBracedReference "##" == "#"
prop_getBracedReference6 = getBracedReference "!#" == "#" -- >>> prop $ getBracedReference "#!" == "!"
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo" -- >>> prop $ getBracedReference "!#" == "#"
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo" -- >>> prop $ getBracedReference "!foo#?" == "foo"
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo" -- >>> prop $ getBracedReference "foo-bar" == "foo"
prop_getBracedReference10= getBracedReference "foo: -1" == "foo" -- >>> prop $ getBracedReference "foo:-bar" == "foo"
prop_getBracedReference11= getBracedReference "!os*" == "" -- >>> prop $ getBracedReference "foo: -1" == "foo"
prop_getBracedReference12= getBracedReference "!os?bar**" == "" -- >>> prop $ getBracedReference "!os*" == ""
prop_getBracedReference13= getBracedReference "foo[bar]" == "foo" -- >>> prop $ getBracedReference "!os?bar**" == ""
-- >>> prop $ getBracedReference "foo[bar]" == "foo"
getBracedReference s = fromMaybe s $ getBracedReference s = fromMaybe s $
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
where where
@@ -766,9 +819,10 @@ getBracedReference s = fromMaybe s $
return "" return ""
nameExpansion _ = Nothing nameExpansion _ = Nothing
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz" -- |
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo" -- >>> prop $ getBracedModifier "foo:bar:baz" == ":bar:baz"
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]" -- >>> prop $ getBracedModifier "!var:-foo" == ":-foo"
-- >>> prop $ getBracedModifier "foo[bar]" == "[bar]"
getBracedModifier s = fromMaybe "" . listToMaybe $ do getBracedModifier s = fromMaybe "" . listToMaybe $ do
let var = getBracedReference s let var = getBracedReference s
a <- dropModifier s a <- dropModifier s
@@ -785,10 +839,13 @@ getBracedModifier s = fromMaybe "" . listToMaybe $ do
-- Run an action in a Maybe (or do nothing). -- Run an action in a Maybe (or do nothing).
-- Example: -- Example:
--
-- @
-- potentially $ do -- potentially $ do
-- s <- getLiteralString cmd -- s <- getLiteralString cmd
-- guard $ s `elem` ["--recursive", "-r"] -- guard $ s `elem` ["--recursive", "-r"]
-- return $ warn .. "Something something recursive" -- return $ warn .. "Something something recursive"
-- @
potentially :: Monad m => Maybe (m ()) -> m () potentially :: Monad m => Maybe (m ()) -> m ()
potentially = fromMaybe (return ()) potentially = fromMaybe (return ())
@@ -812,10 +869,9 @@ filterByAnnotation asSpec params =
filter (not . shouldIgnore) filter (not . shouldIgnore)
where where
token = asScript asSpec token = asScript asSpec
idFor (TokenComment id _) = id
shouldIgnore note = shouldIgnore note =
any (shouldIgnoreFor (getCode note)) $ any (shouldIgnoreFor (getCode note)) $
getPath parents (T_Bang $ idFor note) getPath parents (T_Bang $ tcId note)
shouldIgnoreFor num (T_Annotation _ anns _) = shouldIgnoreFor num (T_Annotation _ anns _) =
any hasNum anns any hasNum anns
where where
@@ -824,7 +880,7 @@ filterByAnnotation asSpec params =
shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec
shouldIgnoreFor _ _ = False shouldIgnoreFor _ _ = False
parents = parentMap params parents = parentMap params
getCode (TokenComment _ (Comment _ c _)) = c getCode = cCode . tcComment
-- Is this a ${#anything}, to get string length or array count? -- Is this a ${#anything}, to get string length or array count?
isCountingReference (T_DollarBraced id token) = isCountingReference (T_DollarBraced id token) =
@@ -874,6 +930,3 @@ getOpts flagTokenizer string cmd = process flags
else do else do
more <- process rest2 more <- process rest2
return $ (flag1, token1) : more return $ (flag1, token1) : more
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -17,8 +17,7 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE TemplateHaskell #-} module ShellCheck.Checker (checkScript) where
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Parser import ShellCheck.Parser
@@ -35,58 +34,75 @@ import qualified System.IO
import Prelude hiding (readFile) import Prelude hiding (readFile)
import Control.Monad import Control.Monad
import Test.QuickCheck.All tokenToPosition startMap t = fromMaybe fail $ do
span <- Map.lookup (tcId t) startMap
tokenToPosition map (TokenComment id c) = fromMaybe fail $ do return $ newPositionedComment {
position <- Map.lookup id map pcStartPos = fst span,
return $ PositionedComment position position c pcEndPos = snd span,
pcComment = tcComment t,
pcFix = tcFix t
}
where where
fail = error "Internal shellcheck error: id doesn't exist. Please report!" fail = error "Internal shellcheck error: id doesn't exist. Please report!"
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
checkScript sys spec = do checkScript sys spec = do
results <- checkScript (csScript spec) results <- checkScript (csScript spec)
return CheckResult { return emptyCheckResult {
crFilename = csFilename spec, crFilename = csFilename spec,
crComments = results crComments = results
} }
where where
checkScript contents = do checkScript contents = do
result <- parseScript sys ParseSpec { result <- parseScript sys newParseSpec {
psFilename = csFilename spec, psFilename = csFilename spec,
psScript = contents, psScript = contents,
psCheckSourced = csCheckSourced spec psCheckSourced = csCheckSourced spec,
psShellTypeOverride = csShellTypeOverride spec
} }
let parseMessages = prComments result let parseMessages = prComments result
let tokenPositions = prTokenPositions result
let analysisSpec root =
as {
asScript = root,
asShellType = csShellTypeOverride spec,
asCheckSourced = csCheckSourced spec,
asExecutionMode = Executed,
asTokenPositions = tokenPositions
} where as = newAnalysisSpec root
let analysisMessages = let analysisMessages =
fromMaybe [] $ fromMaybe [] $
(arComments . analyzeScript . analysisSpec) (arComments . analyzeScript . analysisSpec)
<$> prRoot result <$> prRoot result
let translator = tokenToPosition (prTokenPositions result) let translator = tokenToPosition tokenPositions
return . nub . sortMessages . filter shouldInclude $ return . nub . sortMessages . filter shouldInclude $
(parseMessages ++ map translator analysisMessages) (parseMessages ++ map translator analysisMessages)
shouldInclude (PositionedComment _ _ (Comment _ code _)) = shouldInclude pc =
code `notElem` csExcludedWarnings spec let code = cCode (pcComment pc)
severity = cSeverity (pcComment pc)
in
code `notElem` csExcludedWarnings spec &&
severity <= csMinSeverity spec
sortMessages = sortBy (comparing order) sortMessages = sortBy (comparing order)
order (PositionedComment pos _ (Comment severity code message)) = order pc =
(posFile pos, posLine pos, posColumn pos, severity, code, message) let pos = pcStartPos pc
getPosition (PositionedComment pos _ _) = pos comment = pcComment pc in
(posFile pos,
posLine pos,
posColumn pos,
cSeverity comment,
cCode comment,
cMessage comment)
getPosition = pcStartPos
analysisSpec root =
AnalysisSpec {
asScript = root,
asShellType = csShellTypeOverride spec,
asCheckSourced = csCheckSourced spec,
asExecutionMode = Executed
}
getErrors sys spec = getErrors sys spec =
sort . map getCode . crComments $ sort . map getCode . crComments $
runIdentity (checkScript sys spec) runIdentity (checkScript sys spec)
where where
getCode (PositionedComment _ _ (Comment _ code _)) = code getCode = cCode . pcComment
check = checkWithIncludes [] check = checkWithIncludes []
@@ -106,97 +122,132 @@ checkRecursive includes src =
csCheckSourced = True csCheckSourced = True
} }
prop_findsParseIssue = check "echo \"$12\"" == [1037] -- | Dummy binding for doctest to run
--
prop_commentDisablesParseIssue1 = -- >>> check "echo \"$12\""
null $ check "#shellcheck disable=SC1037\necho \"$12\"" -- [1037]
prop_commentDisablesParseIssue2 = --
null $ check "#shellcheck disable=SC1037\n#lol\necho \"$12\"" -- >>> check "#shellcheck disable=SC1037\necho \"$12\""
-- []
prop_findsAnalysisIssue = --
check "echo $1" == [2086] -- >>> check "#shellcheck disable=SC1037\n#lol\necho \"$12\""
prop_commentDisablesAnalysisIssue1 = -- []
null $ check "#shellcheck disable=SC2086\necho $1" --
prop_commentDisablesAnalysisIssue2 = -- >>> check "echo $1"
null $ check "#shellcheck disable=SC2086\n#lol\necho $1" -- [2086]
--
prop_optionDisablesIssue1 = -- >>> check "#shellcheck disable=SC2086\necho $1"
null $ getErrors -- []
(mockedSystemInterface []) --
emptyCheckSpec { -- >>> check "#shellcheck disable=SC2086\n#lol\necho $1"
csScript = "echo $1", -- []
csExcludedWarnings = [2148, 2086] --
} -- >>> :{
-- getErrors
prop_optionDisablesIssue2 = -- (mockedSystemInterface [])
null $ getErrors -- emptyCheckSpec {
(mockedSystemInterface []) -- csScript = "echo $1",
emptyCheckSpec { -- csExcludedWarnings = [2148, 2086]
csScript = "echo \"$10\"", -- }
csExcludedWarnings = [2148, 1037] -- :}
} -- []
--
prop_canParseDevNull = -- >>> :{
[] == check "source /dev/null" -- getErrors
-- (mockedSystemInterface [])
prop_failsWhenNotSourcing = -- emptyCheckSpec {
[1091, 2154] == check "source lol; echo \"$bar\"" -- csScript = "echo \"$10\"",
-- csExcludedWarnings = [2148, 1037]
prop_worksWhenSourcing = -- }
null $ checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\"" -- :}
-- []
prop_worksWhenDotting = --
null $ checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\"" -- >>> check "#!/usr/bin/python\ntrue $1\n"
-- [1071]
prop_noInfiniteSourcing = --
[] == checkWithIncludes [("lib", "source lib")] "source lib" -- >>> :{
-- getErrors
prop_canSourceBadSyntax = -- (mockedSystemInterface [])
[1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1" -- emptyCheckSpec {
-- csScript = "#!/usr/bin/python\ntrue\n",
prop_cantSourceDynamic = -- csShellTypeOverride = Just Sh
[1090] == checkWithIncludes [("lib", "")] ". \"$1\"" -- }
-- :}
prop_cantSourceDynamic2 = -- []
[1090] == checkWithIncludes [("lib", "")] "source ~/foo" --
-- >>> check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
prop_canSourceDynamicWhenRedirected = -- []
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\"" --
-- >>> check "source /dev/null"
prop_recursiveAnalysis = -- []
[2086] == checkRecursive [("lib", "echo $1")] "source lib" --
-- >>> check "source lol; echo \"$bar\""
prop_recursiveParsing = -- [1091,2154]
[1037] == checkRecursive [("lib", "echo \"$10\"")] "source lib" --
-- >>> checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\""
prop_sourceDirectiveDoesntFollowFile = -- []
null $ checkWithIncludes --
[("foo", "source bar"), ("bar", "baz=3")] -- >>> checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\""
"#shellcheck source=foo\n. \"$1\"; echo \"$baz\"" -- []
--
prop_filewideAnnotationBase = [2086] == check "#!/bin/sh\necho $1" -- >>> checkWithIncludes [("lib", "source lib")] "source lib"
prop_filewideAnnotation1 = null $ -- []
check "#!/bin/sh\n# shellcheck disable=2086\necho $1" --
prop_filewideAnnotation2 = null $ -- >>> checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1" -- [1094,2086]
prop_filewideAnnotation3 = null $ --
check "#!/bin/sh\n#unerlated\n# shellcheck disable=2086\ntrue\necho $1" -- >>> checkWithIncludes [("lib", "")] ". \"$1\""
prop_filewideAnnotation4 = null $ -- [1090]
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1" --
prop_filewideAnnotation5 = null $ -- >>> checkWithIncludes [("lib", "")] "source ~/foo"
check "#!/bin/sh\n\n\n\n#shellcheck disable=2086\ntrue\necho $1" -- [1090]
prop_filewideAnnotation6 = null $ --
check "#shellcheck shell=sh\n#unrelated\n#shellcheck disable=2086\ntrue\necho $1" -- >>> checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
prop_filewideAnnotation7 = null $ -- []
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1" --
-- >>> checkRecursive [("lib", "echo $1")] "source lib"
prop_filewideAnnotationBase2 = [2086, 2181] == check "true\n[ $? == 0 ] && echo $1" -- [2086]
prop_filewideAnnotation8 = null $ --
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1" -- >>> checkRecursive [("lib", "echo \"$10\"")] "source lib"
-- [1037]
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source' --
2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh" -- >>> checkWithIncludes [("foo", "source bar"), ("bar", "baz=3")] "#shellcheck source=foo\n. \"$1\"; echo \"$baz\""
-- []
--
return [] -- >>> check "#!/bin/sh\necho $1"
runTests = $quickCheckAll -- [2086]
--
-- >>> check "#!/bin/sh\n# shellcheck disable=2086\necho $1"
-- []
--
-- >>> check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
-- []
--
-- >>> check "#!/bin/sh\n#unrelated\n# shellcheck disable=2086\ntrue\necho $1"
-- []
--
-- >>> check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
-- []
--
-- >>> check "#!/bin/sh\n\n\n\n#shellcheck disable=2086\ntrue\necho $1"
-- []
--
-- >>> check "#shellcheck shell=sh\n#unrelated\n#shellcheck disable=2086\ntrue\necho $1"
-- []
--
-- >>> check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
-- []
--
-- check "true\n[ $? == 0 ] && echo $1"
-- [2086, 2181]
--
-- check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
-- []
--
-- >>> 2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
-- True
--
-- >>> check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n"
-- []
doctests :: ()
doctests = ()

View File

@@ -17,11 +17,9 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
-- This module contains checks that examine specific commands by name. -- This module contains checks that examine specific commands by name.
module ShellCheck.Checks.Commands (checker , ShellCheck.Checks.Commands.runTests) where module ShellCheck.Checks.Commands (checker) where
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.ASTLib import ShellCheck.ASTLib
@@ -37,8 +35,6 @@ import Data.Char
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
data CommandName = Exactly String | Basename String data CommandName = Exactly String | Basename String
deriving (Eq, Ord) deriving (Eq, Ord)
@@ -46,7 +42,6 @@ data CommandName = Exactly String | Basename String
data CommandCheck = data CommandCheck =
CommandCheck CommandName (Token -> Analysis) CommandCheck CommandName (Token -> Analysis)
verify :: CommandCheck -> String -> Bool verify :: CommandCheck -> String -> Bool
verify f s = producesComments (getChecker [f]) s == Just True verify f s = producesComments (getChecker [f]) s == Just True
verifyNot f s = producesComments (getChecker [f]) s == Just False verifyNot f s = producesComments (getChecker [f]) s == Just False
@@ -61,6 +56,7 @@ commandChecks = [
,checkGrepRe ,checkGrepRe
,checkTrapQuotes ,checkTrapQuotes
,checkReturn ,checkReturn
,checkExit
,checkFindExecWithSingleArgument ,checkFindExecWithSingleArgument
,checkUnusedEchoEscapes ,checkUnusedEchoEscapes
,checkInjectableFindSh ,checkInjectableFindSh
@@ -92,6 +88,7 @@ commandChecks = [
,checkWhich ,checkWhich
,checkSudoRedirect ,checkSudoRedirect
,checkSudoArgs ,checkSudoArgs
,checkSourceArgs
] ]
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis) buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
@@ -128,19 +125,21 @@ getChecker list = Checker {
checker :: Parameters -> Checker checker :: Parameters -> Checker
checker params = getChecker commandChecks checker params = getChecker commandChecks
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]" -- |
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'" -- >>> prop $ verify checkTr "tr [a-f] [A-F]"
prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'" -- >>> prop $ verify checkTr "tr 'a-z' 'A-Z'"
prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'" -- >>> prop $ verify checkTr "tr '[a-z]' '[A-Z]'"
prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'" -- >>> prop $ verifyNot checkTr "tr -d '[:lower:]'"
prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'" -- >>> prop $ verifyNot checkTr "tr -d '[:upper:]'"
prop_checkTr4 = verifyNot checkTr "ls [a-z]" -- >>> prop $ verifyNot checkTr "tr -d '|/_[:upper:]'"
prop_checkTr5 = verify checkTr "tr foo bar" -- >>> prop $ verifyNot checkTr "ls [a-z]"
prop_checkTr6 = verify checkTr "tr 'hello' 'world'" -- >>> prop $ verify checkTr "tr foo bar"
prop_checkTr8 = verifyNot checkTr "tr aeiou _____" -- >>> prop $ verify checkTr "tr 'hello' 'world'"
prop_checkTr9 = verifyNot checkTr "a-z n-za-m" -- >>> prop $ verifyNot checkTr "tr aeiou _____"
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr" -- >>> prop $ verifyNot checkTr "a-z n-za-m"
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'" -- >>> prop $ verifyNot checkTr "tr --squeeze-repeats rl lr"
-- >>> prop $ verifyNot checkTr "tr abc '[d*]'"
-- >>> prop $ verifyNot checkTr "tr '[=e=]' 'e'"
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments) checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
where where
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme? f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
@@ -152,7 +151,7 @@ checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
Just s -> do -- Eliminate false positives by only looking for dupes in SET2? Just s -> do -- Eliminate false positives by only looking for dupes in SET2?
when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $ when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $
info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)." 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)) $ 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." info (getId word) 2021 "Don't use [] around classes in tr, it replaces literal square brackets."
Nothing -> return () Nothing -> return ()
@@ -161,9 +160,10 @@ checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
let relevant = filter isAlpha s let relevant = filter isAlpha s
in relevant /= nub relevant in relevant /= nub relevant
prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php" -- |
prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)" -- >>> prop $ verify checkFindNameGlob "find / -name *.php"
prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'" -- >>> prop $ verify checkFindNameGlob "find / -type f -ipath *(foo)"
-- >>> prop $ verifyNot checkFindNameGlob "find * -name '*.php'"
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ] acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
acceptsGlob _ = False acceptsGlob _ = False
@@ -176,35 +176,37 @@ checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
f (b:r) f (b:r)
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)" -- |
prop_checkNeedlessExpr2 = verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``" -- >>> prop $ verify checkNeedlessExpr "foo=$(expr 3 + 2)"
prop_checkNeedlessExpr3 = verifyNot checkNeedlessExpr "foo=$(expr foo : regex)" -- >>> prop $ verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``"
prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)" -- >>> prop $ verifyNot checkNeedlessExpr "foo=$(expr foo : regex)"
-- >>> prop $ verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)"
checkNeedlessExpr = CommandCheck (Basename "expr") f where checkNeedlessExpr = CommandCheck (Basename "expr") f where
f t = f t =
when (all (`notElem` exceptions) (words $ arguments 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 [[ ]]." "expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
-- These operators are hard to replicate in POSIX -- These operators are hard to replicate in POSIX
exceptions = [ ":", "<", ">", "<=", ">=" ] exceptions = [ ":", "<", ">", "<=", ">=" ]
words = mapMaybe getLiteralString words = mapMaybe getLiteralString
prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3" -- |
prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3" -- >>> prop $ verify checkGrepRe "cat foo | grep *.mp3"
prop_checkGrepRe3 = verify checkGrepRe "grep --regex=*.mp3 file" -- >>> prop $ verify checkGrepRe "grep -Ev cow*test *.mp3"
prop_checkGrepRe4 = verifyNot checkGrepRe "grep foo *.mp3" -- >>> prop $ verify checkGrepRe "grep --regex=*.mp3 file"
prop_checkGrepRe5 = verifyNot checkGrepRe "grep-v --regex=moo *" -- >>> prop $ verifyNot checkGrepRe "grep foo *.mp3"
prop_checkGrepRe6 = verifyNot checkGrepRe "grep foo \\*.mp3" -- >>> prop $ verifyNot checkGrepRe "grep-v --regex=moo *"
prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file" -- >>> prop $ verifyNot checkGrepRe "grep foo \\*.mp3"
prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg" -- >>> prop $ verify checkGrepRe "grep *foo* file"
prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file" -- >>> prop $ verify checkGrepRe "ls | grep foo*.jpg"
prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file" -- >>> prop $ verifyNot checkGrepRe "grep '[0-9]*' file"
prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo" -- >>> prop $ verifyNot checkGrepRe "grep '^aa*' file"
prop_checkGrepRe12= verifyNot checkGrepRe "grep -F 'Foo*' file" -- >>> prop $ verifyNot checkGrepRe "grep --include=*.png foo"
prop_checkGrepRe13= verifyNot checkGrepRe "grep -- -foo bar*" -- >>> prop $ verifyNot checkGrepRe "grep -F 'Foo*' file"
prop_checkGrepRe14= verifyNot checkGrepRe "grep -e -foo bar*" -- >>> prop $ verifyNot checkGrepRe "grep -- -foo bar*"
prop_checkGrepRe15= verifyNot checkGrepRe "grep --regex -foo bar*" -- >>> prop $ verifyNot checkGrepRe "grep -e -foo bar*"
-- >>> prop $ verifyNot checkGrepRe "grep --regex -foo bar*"
checkGrepRe = CommandCheck (Basename "grep") check where checkGrepRe = CommandCheck (Basename "grep") check where
check cmd = f cmd (arguments cmd) check cmd = f cmd (arguments cmd)
@@ -255,10 +257,11 @@ checkGrepRe = CommandCheck (Basename "grep") check where
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]" contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT" -- |
prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT" -- >>> prop $ verify checkTrapQuotes "trap \"echo $num\" INT"
prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT" -- >>> prop $ verify checkTrapQuotes "trap \"echo `ls`\" INT"
prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG" -- >>> prop $ verifyNot checkTrapQuotes "trap 'echo $num' INT"
-- >>> prop $ verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
f (x:_) = checkTrap x f (x:_) = checkTrap x
f _ = return () f _ = return ()
@@ -272,22 +275,37 @@ checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
checkExpansions _ = return () checkExpansions _ = return ()
prop_checkReturn1 = verifyNot checkReturn "return" -- |
prop_checkReturn2 = verifyNot checkReturn "return 1" -- >>> prop $ verifyNot checkReturn "return"
prop_checkReturn3 = verifyNot checkReturn "return $var" -- >>> prop $ verifyNot checkReturn "return 1"
prop_checkReturn4 = verifyNot checkReturn "return $((a|b))" -- >>> prop $ verifyNot checkReturn "return $var"
prop_checkReturn5 = verify checkReturn "return -1" -- >>> prop $ verifyNot checkReturn "return $((a|b))"
prop_checkReturn6 = verify checkReturn "return 1000" -- >>> prop $ verify checkReturn "return -1"
prop_checkReturn7 = verify checkReturn "return 'hello world'" -- >>> prop $ verify checkReturn "return 1000"
checkReturn = CommandCheck (Exactly "return") (f . arguments) -- >>> prop $ verify checkReturn "return 'hello world'"
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 $ verifyNot checkExit "exit"
-- >>> prop $ verifyNot checkExit "exit 1"
-- >>> prop $ verifyNot checkExit "exit $var"
-- >>> prop $ verifyNot checkExit "exit $((a|b))"
-- >>> prop $ verify checkExit "exit -1"
-- >>> prop $ verify checkExit "exit 1000"
-- >>> prop $ 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 where
f (first:second:_) = f (first:second:_) =
err (getId second) 2151 multi (getId first)
"Only one integer 0-255 can be returned. Use stdout for other data."
f [value] = f [value] =
when (isInvalid $ literal value) $ when (isInvalid $ literal value) $
err (getId value) 2152 invalid (getId value)
"Can only return 0-255. Other data should be written to stdout."
f _ = return () f _ = return ()
isInvalid s = s == "" || any (not . isDigit) s || length s > 5 isInvalid s = s == "" || any (not . isDigit) s || length s > 5
@@ -301,9 +319,10 @@ checkReturn = CommandCheck (Exactly "return") (f . arguments)
lit _ = return "WTF" lit _ = return "WTF"
prop_checkFindExecWithSingleArgument1 = verify checkFindExecWithSingleArgument "find . -exec 'cat {} | wc -l' \\;" -- |
prop_checkFindExecWithSingleArgument2 = verify checkFindExecWithSingleArgument "find . -execdir 'cat {} | wc -l' +" -- >>> prop $ verify checkFindExecWithSingleArgument "find . -exec 'cat {} | wc -l' \\;"
prop_checkFindExecWithSingleArgument3 = verifyNot checkFindExecWithSingleArgument "find . -exec wc -l {} \\;" -- >>> prop $ verify checkFindExecWithSingleArgument "find . -execdir 'cat {} | wc -l' +"
-- >>> prop $ verifyNot checkFindExecWithSingleArgument "find . -exec wc -l {} \\;"
checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments) checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments)
where where
f = void . sequence . mapMaybe check . tails f = void . sequence . mapMaybe check . tails
@@ -319,36 +338,30 @@ checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments)
commandRegex = mkRegex "[ |;]" commandRegex = mkRegex "[ |;]"
prop_checkUnusedEchoEscapes1 = verify checkUnusedEchoEscapes "echo 'foo\\nbar\\n'" -- |
prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'" -- >>> prop $ verify checkUnusedEchoEscapes "echo 'foo\\nbar\\n'"
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\"" -- >>> prop $ verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'"
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol" -- >>> prop $ verify checkUnusedEchoEscapes "echo \"n:\\t42\""
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'" -- >>> prop $ verifyNot checkUnusedEchoEscapes "echo lol"
checkUnusedEchoEscapes = CommandCheck (Basename "echo") (f . arguments) -- >>> prop $ verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
checkUnusedEchoEscapes = CommandCheck (Basename "echo") f
where where
isDashE = mkRegex "^-.*e"
hasEscapes = mkRegex "\\\\[rnt]" hasEscapes = mkRegex "\\\\[rnt]"
f args | concat (concatMap oversimplify allButLast) `matches` isDashE = f cmd =
return () whenShell [Sh, Bash, Ksh] $
where allButLast = reverse . drop 1 . reverse $ args unless (cmd `hasFlag` "e") $
f args = mapM_ checkEscapes args mapM_ examine $ arguments cmd
checkEscapes (T_NormalWord _ args) = examine token = do
mapM_ checkEscapes args let str = onlyLiteralString token
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 =
when (str `matches` hasEscapes) $ 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 {}' \\;" -- |
prop_checkInjectableFindSh2 = verify checkInjectableFindSh "find . -execdir bash -c 'rm \"{}\"' ';'" -- >>> prop $ verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
prop_checkInjectableFindSh3 = verifyNot checkInjectableFindSh "find . -exec sh -c 'rm \"$@\"' _ {} \\;" -- >>> prop $ verify checkInjectableFindSh "find . -execdir bash -c 'rm \"{}\"' ';'"
-- >>> prop $ verifyNot checkInjectableFindSh "find . -exec sh -c 'rm \"$@\"' _ {} \\;"
checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments) checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments)
where where
check args = do check args = do
@@ -371,9 +384,10 @@ checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments)
warn id 2156 "Injecting filenames is fragile and insecure. Use parameters." warn id 2156 "Injecting filenames is fragile and insecure. Use parameters."
prop_checkFindActionPrecedence1 = verify checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au' -exec rm {} +" -- |
prop_checkFindActionPrecedence2 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o \\( -name '*.au' -exec rm {} + \\)" -- >>> prop $ verify checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au' -exec rm {} +"
prop_checkFindActionPrecedence3 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au'" -- >>> prop $ verifyNot checkFindActionPrecedence "find . -name '*.wav' -o \\( -name '*.au' -exec rm {} + \\)"
-- >>> prop $ verifyNot checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au'"
checkFindActionPrecedence = CommandCheck (Basename "find") (f . arguments) checkFindActionPrecedence = CommandCheck (Basename "find") (f . arguments)
where where
pattern = [isMatch, const True, isParam ["-o", "-or"], isMatch, const True, isAction] pattern = [isMatch, const True, isParam ["-o", "-or"], isMatch, const True, isAction]
@@ -390,28 +404,29 @@ checkFindActionPrecedence = CommandCheck (Basename "find") (f . arguments)
warnFor t = warn (getId t) 2146 "This action ignores everything before the -o. Use \\( \\) to group." warnFor t = warn (getId t) 2146 "This action ignores everything before the -o. Use \\( \\) to group."
prop_checkMkdirDashPM0 = verify checkMkdirDashPM "mkdir -p -m 0755 a/b" -- |
prop_checkMkdirDashPM1 = verify checkMkdirDashPM "mkdir -pm 0755 $dir" -- >>> prop $ verify checkMkdirDashPM "mkdir -p -m 0755 a/b"
prop_checkMkdirDashPM2 = verify checkMkdirDashPM "mkdir -vpm 0755 a/b" -- >>> prop $ verify checkMkdirDashPM "mkdir -pm 0755 $dir"
prop_checkMkdirDashPM3 = verify checkMkdirDashPM "mkdir -pm 0755 -v a/b" -- >>> prop $ verify checkMkdirDashPM "mkdir -vpm 0755 a/b"
prop_checkMkdirDashPM4 = verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b" -- >>> prop $ verify checkMkdirDashPM "mkdir -pm 0755 -v a/b"
prop_checkMkdirDashPM5 = verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b" -- >>> prop $ verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b"
prop_checkMkdirDashPM6 = verify checkMkdirDashPM "mkdir -p --mode=0755 a/b" -- >>> prop $ verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b"
prop_checkMkdirDashPM7 = verify checkMkdirDashPM "mkdir --parents -m 0755 a/b" -- >>> prop $ verify checkMkdirDashPM "mkdir -p --mode=0755 a/b"
prop_checkMkdirDashPM8 = verifyNot checkMkdirDashPM "mkdir -p a/b" -- >>> prop $ verify checkMkdirDashPM "mkdir --parents -m 0755 a/b"
prop_checkMkdirDashPM9 = verifyNot checkMkdirDashPM "mkdir -m 0755 a/b" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p a/b"
prop_checkMkdirDashPM10 = verifyNot checkMkdirDashPM "mkdir a/b" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir -m 0755 a/b"
prop_checkMkdirDashPM11 = verifyNot checkMkdirDashPM "mkdir --parents a/b" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir a/b"
prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir --parents a/b"
prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b"
prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b"
prop_checkMkdirDashPM15 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../bin" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"
prop_checkMkdirDashPM16 = verify checkMkdirDashPM "mkdir -p -m 0755 ../bin/laden" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../bin"
prop_checkMkdirDashPM17 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./bin" -- >>> prop $ verify checkMkdirDashPM "mkdir -p -m 0755 ../bin/laden"
prop_checkMkdirDashPM18 = verify checkMkdirDashPM "mkdir -p -m 0755 ./bin/laden" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./bin"
prop_checkMkdirDashPM19 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./../bin" -- >>> prop $ verify checkMkdirDashPM "mkdir -p -m 0755 ./bin/laden"
prop_checkMkdirDashPM20 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 .././bin" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./../bin"
prop_checkMkdirDashPM21 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../../bin" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 .././bin"
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../../bin"
checkMkdirDashPM = CommandCheck (Basename "mkdir") check checkMkdirDashPM = CommandCheck (Basename "mkdir") check
where where
check t = potentially $ do check t = potentially $ do
@@ -427,13 +442,14 @@ checkMkdirDashPM = CommandCheck (Basename "mkdir") check
re = mkRegex "^(\\.\\.?\\/)+[^/]+$" re = mkRegex "^(\\.\\.?\\/)+[^/]+$"
prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8" -- |
prop_checkNonportableSignals2 = verifyNot checkNonportableSignals "trap f 0" -- >>> prop $ verify checkNonportableSignals "trap f 8"
prop_checkNonportableSignals3 = verifyNot checkNonportableSignals "trap f 14" -- >>> prop $ verifyNot checkNonportableSignals "trap f 0"
prop_checkNonportableSignals4 = verify checkNonportableSignals "trap f SIGKILL" -- >>> prop $ verifyNot checkNonportableSignals "trap f 14"
prop_checkNonportableSignals5 = verify checkNonportableSignals "trap f 9" -- >>> prop $ verify checkNonportableSignals "trap f SIGKILL"
prop_checkNonportableSignals6 = verify checkNonportableSignals "trap f stop" -- >>> prop $ verify checkNonportableSignals "trap f 9"
prop_checkNonportableSignals7 = verifyNot checkNonportableSignals "trap 'stop' int" -- >>> prop $ verify checkNonportableSignals "trap f stop"
-- >>> prop $ verifyNot checkNonportableSignals "trap 'stop' int"
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments) checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
where where
f args = case args of f args = case args of
@@ -462,10 +478,11 @@ checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
"SIGKILL/SIGSTOP can not be trapped." "SIGKILL/SIGSTOP can not be trapped."
prop_checkInteractiveSu1 = verify checkInteractiveSu "su; rm file; su $USER" -- |
prop_checkInteractiveSu2 = verify checkInteractiveSu "su foo; something; exit" -- >>> prop $ verify checkInteractiveSu "su; rm file; su $USER"
prop_checkInteractiveSu3 = verifyNot checkInteractiveSu "echo rm | su foo" -- >>> prop $ verify checkInteractiveSu "su foo; something; exit"
prop_checkInteractiveSu4 = verifyNot checkInteractiveSu "su root < script" -- >>> prop $ verifyNot checkInteractiveSu "echo rm | su foo"
-- >>> prop $ verifyNot checkInteractiveSu "su root < script"
checkInteractiveSu = CommandCheck (Basename "su") f checkInteractiveSu = CommandCheck (Basename "su") f
where where
f cmd = when (length (arguments cmd) <= 1) $ do f cmd = when (length (arguments cmd) <= 1) $ do
@@ -480,11 +497,13 @@ checkInteractiveSu = CommandCheck (Basename "su") f
undirected _ = True undirected _ = True
-- |
-- This is hard to get right without properly parsing ssh args -- This is hard to get right without properly parsing ssh args
prop_checkSshCmdStr1 = verify checkSshCommandString "ssh host \"echo $PS1\"" --
prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\"" -- >>> prop $ verify checkSshCommandString "ssh host \"echo $PS1\""
prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\"" -- >>> prop $ verifyNot checkSshCommandString "ssh host \"ls foo\""
prop_checkSshCmdStr4 = verifyNot checkSshCommandString "ssh -i key \"$host\"" -- >>> prop $ verifyNot checkSshCommandString "ssh \"$host\""
-- >>> prop $ verifyNot checkSshCommandString "ssh -i key \"$host\""
checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments) checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
where where
isOption x = "-" `isPrefixOf` (concat $ oversimplify x) isOption x = "-" `isPrefixOf` (concat $ oversimplify x)
@@ -500,21 +519,25 @@ checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
checkArg _ = return () checkArg _ = return ()
prop_checkPrintfVar1 = verify checkPrintfVar "printf \"Lol: $s\"" -- |
prop_checkPrintfVar2 = verifyNot checkPrintfVar "printf 'Lol: $s'" -- >>> prop $ verify checkPrintfVar "printf \"Lol: $s\""
prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)" -- >>> prop $ verifyNot checkPrintfVar "printf 'Lol: $s'"
prop_checkPrintfVar4 = verifyNot checkPrintfVar "printf \"%${count}s\" var" -- >>> prop $ verify checkPrintfVar "printf -v cow $(cmd)"
prop_checkPrintfVar5 = verify checkPrintfVar "printf '%s %s %s' foo bar" -- >>> prop $ verifyNot checkPrintfVar "printf \"%${count}s\" var"
prop_checkPrintfVar6 = verify checkPrintfVar "printf foo bar baz" -- >>> prop $ verify checkPrintfVar "printf '%s %s %s' foo bar"
prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz" -- >>> prop $ verify checkPrintfVar "printf foo bar baz"
prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\"" -- >>> prop $ verify checkPrintfVar "printf -- foo bar baz"
prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png" -- >>> prop $ verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\""
prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz" -- >>> prop $ verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
prop_checkPrintfVar11= verifyNot checkPrintfVar "printf '%(%s%s)T' -1" -- >>> prop $ verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
prop_checkPrintfVar12= verify checkPrintfVar "printf '%s %s\\n' 1 2 3" -- >>> prop $ verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4" -- >>> prop $ verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1" -- >>> prop $ verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2" -- >>> prop $ verify checkPrintfVar "printf '%*s\\n' 1"
-- >>> prop $ verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
-- >>> prop $ verifyNot checkPrintfVar "printf $'string'"
-- >>> prop $ verify checkPrintfVar "printf '%-*s\\n' 1"
-- >>> prop $ verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
@@ -525,11 +548,20 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
case string of case string of
'%':'%':rest -> countFormats rest '%':'%':rest -> countFormats rest
'%':'(':rest -> 1 + countFormats (dropWhile (/= ')') rest) '%':'(':rest -> 1 + countFormats (dropWhile (/= ')') rest)
'%':'*':rest -> 2 + countFormats rest -- width is specified as an argument '%':rest -> regexBasedCountFormats rest + countFormats (dropWhile (/= '%') rest)
'%':rest -> 1 + countFormats rest
_:rest -> countFormats rest _:rest -> countFormats rest
[] -> 0 [] -> 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 check format more = do
fromMaybe (return ()) $ do fromMaybe (return ()) $ do
string <- getLiteralString format string <- getLiteralString format
@@ -554,24 +586,26 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)" -- |
prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`" -- >>> prop $ verify checkUuoeCmd "echo $(date)"
prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\"" -- >>> prop $ verify checkUuoeCmd "echo `date`"
prop_checkUuoeCmd4 = verify checkUuoeCmd "echo \"`date`\"" -- >>> prop $ verify checkUuoeCmd "echo \"$(date)\""
prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\"" -- >>> prop $ verify checkUuoeCmd "echo \"`date`\""
prop_checkUuoeCmd6 = verifyNot checkUuoeCmd "echo \"$(<file)\"" -- >>> prop $ verifyNot checkUuoeCmd "echo \"The time is $(date)\""
-- >>> prop $ verifyNot checkUuoeCmd "echo \"$(<file)\""
checkUuoeCmd = CommandCheck (Exactly "echo") (f . arguments) where checkUuoeCmd = CommandCheck (Exactly "echo") (f . arguments) where
msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'." msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'."
f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token) f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token)
f _ = return () f _ = return ()
prop_checkSetAssignment1 = verify checkSetAssignment "set foo 42" -- |
prop_checkSetAssignment2 = verify checkSetAssignment "set foo = 42" -- >>> prop $ verify checkSetAssignment "set foo 42"
prop_checkSetAssignment3 = verify checkSetAssignment "set foo=42" -- >>> prop $ verify checkSetAssignment "set foo = 42"
prop_checkSetAssignment4 = verifyNot checkSetAssignment "set -- if=/dev/null" -- >>> prop $ verify checkSetAssignment "set foo=42"
prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'" -- >>> prop $ verifyNot checkSetAssignment "set -- if=/dev/null"
prop_checkSetAssignment6 = verifyNot checkSetAssignment "set" -- >>> prop $ verifyNot checkSetAssignment "set 'a=5'"
-- >>> prop $ verifyNot checkSetAssignment "set"
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments) checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
where where
f (var:value:rest) = f (var:value:rest) =
@@ -591,10 +625,11 @@ checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
literal _ = "*" literal _ = "*"
prop_checkExportedExpansions1 = verify checkExportedExpansions "export $foo" -- |
prop_checkExportedExpansions2 = verify checkExportedExpansions "export \"$foo\"" -- >>> prop $ verify checkExportedExpansions "export $foo"
prop_checkExportedExpansions3 = verifyNot checkExportedExpansions "export foo" -- >>> prop $ verify checkExportedExpansions "export \"$foo\""
prop_checkExportedExpansions4 = verifyNot checkExportedExpansions "export ${foo?}" -- >>> prop $ verifyNot checkExportedExpansions "export foo"
-- >>> prop $ verifyNot checkExportedExpansions "export ${foo?}"
checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments) checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
where where
check t = potentially $ do check t = potentially $ do
@@ -603,14 +638,15 @@ checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . argumen
return . warn (getId t) 2163 $ return . warn (getId t) 2163 $
"This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet." "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 $ verify checkReadExpansions "read $var"
prop_checkReadExpansions3 = verifyNot checkReadExpansions "read -p $var" -- >>> prop $ verify checkReadExpansions "read -r $var"
prop_checkReadExpansions4 = verifyNot checkReadExpansions "read -rd $delim name" -- >>> prop $ verifyNot checkReadExpansions "read -p $var"
prop_checkReadExpansions5 = verify checkReadExpansions "read \"$var\"" -- >>> prop $ verifyNot checkReadExpansions "read -rd $delim name"
prop_checkReadExpansions6 = verify checkReadExpansions "read -a $var" -- >>> prop $ verify checkReadExpansions "read \"$var\""
prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1" -- >>> prop $ verify checkReadExpansions "read -a $var"
prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}" -- >>> prop $ verifyNot checkReadExpansions "read $1"
-- >>> prop $ verifyNot checkReadExpansions "read ${var?}"
checkReadExpansions = CommandCheck (Exactly "read") check checkReadExpansions = CommandCheck (Exactly "read") check
where where
options = getGnuOpts "sreu:n:N:i:p:a:" options = getGnuOpts "sreu:n:N:i:p:a:"
@@ -637,9 +673,10 @@ getSingleUnmodifiedVariable word =
in guard (contents == name) >> return t in guard (contents == name) >> return t
_ -> Nothing _ -> Nothing
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'" -- |
prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'" -- >>> prop $ verify checkAliasesUsesArgs "alias a='cp $1 /a'"
prop_checkAliasesUsesArgs3 = verify checkAliasesUsesArgs "alias a=\"echo \\${@}\"" -- >>> prop $ verifyNot checkAliasesUsesArgs "alias $1='foo'"
-- >>> prop $ verify checkAliasesUsesArgs "alias a=\"echo \\${@}\""
checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments) checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments)
where where
re = mkRegex "\\$\\{?[0-9*@]" re = mkRegex "\\$\\{?[0-9*@]"
@@ -651,9 +688,10 @@ checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments)
"Aliases can't use positional parameters. Use a function." "Aliases can't use positional parameters. Use a function."
prop_checkAliasesExpandEarly1 = verify checkAliasesExpandEarly "alias foo=\"echo $PWD\"" -- |
prop_checkAliasesExpandEarly2 = verifyNot checkAliasesExpandEarly "alias -p" -- >>> prop $ verify checkAliasesExpandEarly "alias foo=\"echo $PWD\""
prop_checkAliasesExpandEarly3 = verifyNot checkAliasesExpandEarly "alias foo='echo {1..10}'" -- >>> prop $ verifyNot checkAliasesExpandEarly "alias -p"
-- >>> prop $ verifyNot checkAliasesExpandEarly "alias foo='echo {1..10}'"
checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments) checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
where where
f = mapM_ checkArg f = mapM_ checkArg
@@ -663,8 +701,8 @@ checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
checkArg _ = return () checkArg _ = return ()
prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]" -- >>> prop $ verify checkUnsetGlobs "unset foo[1]"
prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo" -- >>> prop $ verifyNot checkUnsetGlobs "unset foo"
checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments) checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)
where where
check arg = check arg =
@@ -672,32 +710,38 @@ checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)
warn (getId arg) 2184 "Quote arguments to unset so they're not glob expanded." warn (getId arg) 2184 "Quote arguments to unset so they're not glob expanded."
prop_checkFindWithoutPath1 = verify checkFindWithoutPath "find -type f" -- |
prop_checkFindWithoutPath2 = verify checkFindWithoutPath "find" -- >>> prop $ verify checkFindWithoutPath "find -type f"
prop_checkFindWithoutPath3 = verifyNot checkFindWithoutPath "find . -type f" -- >>> prop $ verify checkFindWithoutPath "find"
prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print" -- >>> prop $ verifyNot checkFindWithoutPath "find . -type f"
prop_checkFindWithoutPath5 = verifyNot checkFindWithoutPath "find -O3 ." -- >>> prop $ verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"
prop_checkFindWithoutPath6 = verifyNot checkFindWithoutPath "find -D exec ." -- >>> prop $ verifyNot checkFindWithoutPath "find -O3 ."
-- >>> prop $ verifyNot checkFindWithoutPath "find -D exec ."
-- >>> prop $ verifyNot checkFindWithoutPath "find --help"
-- >>> prop $ verifyNot checkFindWithoutPath "find -Hx . -print"
checkFindWithoutPath = CommandCheck (Basename "find") f checkFindWithoutPath = CommandCheck (Basename "find") f
where where
f (T_SimpleCommand _ _ (cmd:args)) = f t@(T_SimpleCommand _ _ (cmd:args)) =
unless (hasPath args) $ unless (t `hasFlag` "help" || hasPath args) $
info (getId cmd) 2185 "Some finds don't have a default path. Specify '.' explicitly." 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, -- This is a bit of a kludge. find supports flag arguments both before and
-- as well as multiple non-flag arguments that are not the path. We assume that all the -- after the path, as well as multiple non-flag arguments that are not the
-- pre-path flags are single characters, which is generally the case except for -O3. -- path. We assume that all the pre-path flags are single characters from a
-- list of GNU and macOS flags.
hasPath (first:rest) = hasPath (first:rest) =
let flag = fromJust $ getLiteralStringExt (const $ return "___") first in let flag = fromJust $ getLiteralStringExt (const $ return "___") first in
not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest
hasPath [] = False 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" -- |
prop_checkTimeParameters2 = verifyNot checkTimeParameters "time sleep 10" -- >>> prop $ verify checkTimeParameters "time -f lol sleep 10"
prop_checkTimeParameters3 = verifyNot checkTimeParameters "time -p foo" -- >>> prop $ verifyNot checkTimeParameters "time sleep 10"
prop_checkTimeParameters4 = verifyNot checkTimeParameters "command time -f lol sleep 10" -- >>> prop $ verifyNot checkTimeParameters "time -p foo"
-- >>> prop $ verifyNot checkTimeParameters "command time -f lol sleep 10"
checkTimeParameters = CommandCheck (Exactly "time") f checkTimeParameters = CommandCheck (Exactly "time") f
where where
f (T_SimpleCommand _ _ (cmd:args:_)) = f (T_SimpleCommand _ _ (cmd:args:_)) =
@@ -708,9 +752,10 @@ checkTimeParameters = CommandCheck (Exactly "time") f
f _ = return () f _ = return ()
prop_checkTimedCommand1 = verify checkTimedCommand "#!/bin/sh\ntime -p foo | bar" -- |
prop_checkTimedCommand2 = verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar; )" -- >>> prop $ verify checkTimedCommand "#!/bin/sh\ntime -p foo | bar"
prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1" -- >>> prop $ verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar; )"
-- >>> prop $ verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1"
checkTimedCommand = CommandCheck (Exactly "time") f where checkTimedCommand = CommandCheck (Exactly "time") f where
f (T_SimpleCommand _ _ (c:args@(_:_))) = f (T_SimpleCommand _ _ (c:args@(_:_))) =
whenShell [Sh, Dash] $ do whenShell [Sh, Dash] $ do
@@ -734,32 +779,37 @@ checkTimedCommand = CommandCheck (Exactly "time") f where
T_SimpleCommand {} -> return True T_SimpleCommand {} -> return True
_ -> return False _ -> return False
prop_checkLocalScope1 = verify checkLocalScope "local foo=3" -- |
prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }" -- >>> prop $ verify checkLocalScope "local foo=3"
-- >>> prop $ verifyNot checkLocalScope "f() { local foo=3; }"
checkLocalScope = CommandCheck (Exactly "local") $ \t -> checkLocalScope = CommandCheck (Exactly "local") $ \t ->
whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local
path <- getPathM t path <- getPathM t
unless (any isFunction path) $ 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)" -- >>> prop $ verify checkDeprecatedTempfile "var=$(tempfile)"
-- >>> prop $ verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $ 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 '.+'" -- |
-- >>> prop $ verify checkDeprecatedEgrep "egrep '.+'"
checkDeprecatedEgrep = CommandCheck (Basename "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" -- |
-- >>> prop $ verify checkDeprecatedFgrep "fgrep '*' files"
checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $ 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" -- >>> prop $ verify checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; esac; done"
prop_checkWhileGetoptsCase3 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; b) bar;; *) :;esac; done" -- >>> prop $ verify checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; b) bar;; esac; done"
prop_checkWhileGetoptsCase4 = verifyNot checkWhileGetoptsCase "while getopts 'a:123' x; do case $x in a) foo;; [0-9]) bar;; esac; done" -- >>> prop $ verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; b) bar;; *) :;esac; done"
prop_checkWhileGetoptsCase5 = verifyNot checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; \\?) bar;; *) baz;; esac; done" -- >>> prop $ verifyNot checkWhileGetoptsCase "while getopts 'a:123' x; do case $x in a) foo;; [0-9]) bar;; esac; done"
-- >>> prop $ verifyNot checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; \\?) bar;; *) baz;; esac; done"
checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
where where
f :: Token -> Analysis f :: Token -> Analysis
@@ -824,19 +874,20 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
T_Redirecting _ _ x@(T_CaseExpression {}) -> return x T_Redirecting _ _ x@(T_CaseExpression {}) -> return x
_ -> Nothing _ -> Nothing
prop_checkCatastrophicRm1 = verify checkCatastrophicRm "rm -r $1/$2" -- |
prop_checkCatastrophicRm2 = verify checkCatastrophicRm "rm -r /home/$foo" -- >>> prop $ verify checkCatastrophicRm "rm -r $1/$2"
prop_checkCatastrophicRm3 = verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*" -- >>> prop $ verify checkCatastrophicRm "rm -r /home/$foo"
prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*" -- >>> prop $ verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*"
prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*" -- >>> prop $ verify checkCatastrophicRm "rm -fr /home/$(whoami)/*"
prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*" -- >>> prop $ verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"
prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home" -- >>> prop $ verify checkCatastrophicRm "rm --recursive /etc/*$config*"
prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}" -- >>> prop $ verify checkCatastrophicRm "rm -rf /home"
prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec" -- >>> prop $ verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec" -- >>> prop $ verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
prop_checkCatastrophicRm13= verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec" -- >>> prop $ verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"
prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg" -- >>> prop $ verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"
prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*" -- >>> prop $ verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"
-- >>> prop $ verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"
checkCatastrophicRm = CommandCheck (Basename "rm") $ \t -> checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
when (isRecursive t) $ when (isRecursive t) $
mapM_ (mapM_ checkWord . braceExpand) $ arguments t mapM_ (mapM_ checkWord . braceExpand) $ arguments t
@@ -885,8 +936,9 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
["", "/", "/*", "/*/*"] >>= (\x -> map (++x) paths) ["", "/", "/*", "/*/*"] >>= (\x -> map (++x) paths)
prop_checkLetUsage1 = verify checkLetUsage "let a=1" -- |
prop_checkLetUsage2 = verifyNot checkLetUsage "(( a=1 ))" -- >>> prop $ verify checkLetUsage "let a=1"
-- >>> prop $ verifyNot checkLetUsage "(( a=1 ))"
checkLetUsage = CommandCheck (Exactly "let") f checkLetUsage = CommandCheck (Exactly "let") f
where where
f t = whenShell [Bash,Ksh] $ do f t = whenShell [Bash,Ksh] $ do
@@ -906,15 +958,16 @@ missingDestination handler token = do
any (\x -> x /= "" && x `isPrefixOf` "target-directory") $ any (\x -> x /= "" && x `isPrefixOf` "target-directory") $
map snd args map snd args
prop_checkMvArguments1 = verify checkMvArguments "mv 'foo bar'" -- |
prop_checkMvArguments2 = verifyNot checkMvArguments "mv foo bar" -- >>> prop $ verify checkMvArguments "mv 'foo bar'"
prop_checkMvArguments3 = verifyNot checkMvArguments "mv 'foo bar'{,bak}" -- >>> prop $ verifyNot checkMvArguments "mv foo bar"
prop_checkMvArguments4 = verifyNot checkMvArguments "mv \"$@\"" -- >>> prop $ verifyNot checkMvArguments "mv 'foo bar'{,bak}"
prop_checkMvArguments5 = verifyNot checkMvArguments "mv -t foo bar" -- >>> prop $ verifyNot checkMvArguments "mv \"$@\""
prop_checkMvArguments6 = verifyNot checkMvArguments "mv --target-directory=foo bar" -- >>> prop $ verifyNot checkMvArguments "mv -t foo bar"
prop_checkMvArguments7 = verifyNot checkMvArguments "mv --target-direc=foo bar" -- >>> prop $ verifyNot checkMvArguments "mv --target-directory=foo bar"
prop_checkMvArguments8 = verifyNot checkMvArguments "mv --version" -- >>> prop $ verifyNot checkMvArguments "mv --target-direc=foo bar"
prop_checkMvArguments9 = verifyNot checkMvArguments "mv \"${!var}\"" -- >>> prop $ verifyNot checkMvArguments "mv --version"
-- >>> prop $ verifyNot checkMvArguments "mv \"${!var}\""
checkMvArguments = CommandCheck (Basename "mv") $ missingDestination f checkMvArguments = CommandCheck (Basename "mv") $ missingDestination f
where where
f t = err (getId t) 2224 "This mv has no destination. Check the arguments." f t = err (getId t) 2224 "This mv has no destination. Check the arguments."
@@ -928,9 +981,10 @@ checkLnArguments = CommandCheck (Basename "ln") $ missingDestination f
f t = warn (getId t) 2226 "This ln has no destination. Check the arguments, or specify '.' explicitly." 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 $ verify checkFindRedirections "find . -exec echo {} > file \\;"
prop_checkFindRedirections3 = verifyNot checkFindRedirections "find . -execdir sh -c 'foo > file' \\;" -- >>> prop $ verifyNot checkFindRedirections "find . -exec echo {} \\; > file"
-- >>> prop $ verifyNot checkFindRedirections "find . -execdir sh -c 'foo > file' \\;"
checkFindRedirections = CommandCheck (Basename "find") f checkFindRedirections = CommandCheck (Basename "find") f
where where
f t = do f t = do
@@ -945,17 +999,18 @@ checkFindRedirections = CommandCheck (Basename "find") f
"Redirection applies to the find command itself. Rewrite to work per action (or move to end)." "Redirection applies to the find command itself. Rewrite to work per action (or move to end)."
_ -> return () _ -> return ()
prop_checkWhich = verify checkWhich "which '.+'" -- >>> prop $ verify checkWhich "which '.+'"
checkWhich = CommandCheck (Basename "which") $ checkWhich = CommandCheck (Basename "which") $
\t -> info (getId t) 2230 "which is non-standard. Use builtin 'command -v' instead." \t -> info (getId $ getCommandTokenOrThis t) 2230 "which is non-standard. Use builtin 'command -v' instead."
prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file" -- |
prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input" -- >>> prop $ verify checkSudoRedirect "sudo echo 3 > /proc/file"
prop_checkSudoRedirect3 = verify checkSudoRedirect "sudo cmd >> file" -- >>> prop $ verify checkSudoRedirect "sudo cmd < input"
prop_checkSudoRedirect4 = verify checkSudoRedirect "sudo cmd &> file" -- >>> prop $ verify checkSudoRedirect "sudo cmd >> file"
prop_checkSudoRedirect5 = verifyNot checkSudoRedirect "sudo cmd 2>&1" -- >>> prop $ verify checkSudoRedirect "sudo cmd &> file"
prop_checkSudoRedirect6 = verifyNot checkSudoRedirect "sudo cmd 2> log" -- >>> prop $ verifyNot checkSudoRedirect "sudo cmd 2>&1"
prop_checkSudoRedirect7 = verifyNot checkSudoRedirect "sudo cmd > /dev/null 2>&1" -- >>> prop $ verifyNot checkSudoRedirect "sudo cmd 2> log"
-- >>> prop $ verifyNot checkSudoRedirect "sudo cmd > /dev/null 2>&1"
checkSudoRedirect = CommandCheck (Basename "sudo") f checkSudoRedirect = CommandCheck (Basename "sudo") f
where where
f t = do f t = do
@@ -979,13 +1034,14 @@ checkSudoRedirect = CommandCheck (Basename "sudo") f
warnAbout _ = return () warnAbout _ = return ()
special file = concat (oversimplify file) == "/dev/null" special file = concat (oversimplify file) == "/dev/null"
prop_checkSudoArgs1 = verify checkSudoArgs "sudo cd /root" -- |
prop_checkSudoArgs2 = verify checkSudoArgs "sudo export x=3" -- >>> prop $ verify checkSudoArgs "sudo cd /root"
prop_checkSudoArgs3 = verifyNot checkSudoArgs "sudo ls /usr/local/protected" -- >>> prop $ verify checkSudoArgs "sudo export x=3"
prop_checkSudoArgs4 = verifyNot checkSudoArgs "sudo ls && export x=3" -- >>> prop $ verifyNot checkSudoArgs "sudo ls /usr/local/protected"
prop_checkSudoArgs5 = verifyNot checkSudoArgs "sudo echo ls" -- >>> prop $ verifyNot checkSudoArgs "sudo ls && export x=3"
prop_checkSudoArgs6 = verifyNot checkSudoArgs "sudo -n -u export ls" -- >>> prop $ verifyNot checkSudoArgs "sudo echo ls"
prop_checkSudoArgs7 = verifyNot checkSudoArgs "sudo docker export foo" -- >>> prop $ verifyNot checkSudoArgs "sudo -n -u export ls"
-- >>> prop $ verifyNot checkSudoArgs "sudo docker export foo"
checkSudoArgs = CommandCheck (Basename "sudo") f checkSudoArgs = CommandCheck (Basename "sudo") f
where where
f t = potentially $ do f t = potentially $ do
@@ -999,5 +1055,14 @@ checkSudoArgs = CommandCheck (Basename "sudo") f
-- This mess is why ShellCheck prefers not to know. -- This mess is why ShellCheck prefers not to know.
parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:" parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:"
return [] -- |
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) -- >>> prop $ verify checkSourceArgs "#!/bin/sh\n. script arg"
-- >>> prop $ verifyNot checkSourceArgs "#!/bin/sh\n. script"
-- >>> prop $ 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 ()

View File

@@ -17,9 +17,8 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where module ShellCheck.Checks.ShellSupport (checker) where
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.ASTLib import ShellCheck.ASTLib
@@ -33,8 +32,6 @@ import Data.Char
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import qualified Data.Map as Map import qualified Data.Map as Map
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
data ForShell = ForShell [Shell] (Token -> Analysis) data ForShell = ForShell [Shell] (Token -> Analysis)
@@ -67,9 +64,10 @@ testChecker (ForShell _ t) =
verify c s = producesComments (testChecker c) s == Just True verify c s = producesComments (testChecker c) s == Just True
verifyNot c s = producesComments (testChecker c) s == Just False verifyNot c s = producesComments (testChecker c) s == Just False
prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))" -- |
prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar" -- >>> prop $ verify checkForDecimals "((3.14*c))"
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar" -- >>> prop $ verify checkForDecimals "foo[1.2]=bar"
-- >>> prop $ verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
checkForDecimals = ForShell [Sh, Dash, Bash] f checkForDecimals = ForShell [Sh, Dash, Bash] f
where where
f t@(TA_Expansion id _) = potentially $ do f t@(TA_Expansion id _) = potentially $ do
@@ -80,62 +78,63 @@ checkForDecimals = ForShell [Sh, Dash, Bash] f
f _ = return () f _ = return ()
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)" -- |
prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]" -- >>> prop $ verify checkBashisms "while read a; do :; done < <(a)"
prop_checkBashisms3 = verify checkBashisms "echo $((i++))" -- >>> prop $ verify checkBashisms "[ foo -nt bar ]"
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)" -- >>> prop $ verify checkBashisms "echo $((i++))"
prop_checkBashisms5 = verify checkBashisms "source file" -- >>> prop $ verify checkBashisms "rm !(*.hs)"
prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]" -- >>> prop $ verify checkBashisms "source file"
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}" -- >>> prop $ verify checkBashisms "[ \"$a\" == 42 ]"
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}" -- >>> prop $ verify checkBashisms "echo ${var[1]}"
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}" -- >>> prop $ verify checkBashisms "echo ${!var[@]}"
prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}" -- >>> prop $ verify checkBashisms "echo ${!var*}"
prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}" -- >>> prop $ verify checkBashisms "echo ${var:4:12}"
prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}" -- >>> prop $ verifyNot checkBashisms "echo ${var:-4}"
prop_checkBashisms13= verify checkBashisms "exec -c env" -- >>> prop $ verify checkBashisms "echo ${var//foo/bar}"
prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \"" -- >>> prop $ verify checkBashisms "exec -c env"
prop_checkBashisms15= verify checkBashisms "let n++" -- >>> prop $ verify checkBashisms "echo -n \"Foo: \""
prop_checkBashisms16= verify checkBashisms "echo $RANDOM" -- >>> prop $ verify checkBashisms "let n++"
prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))" -- >>> prop $ verify checkBashisms "echo $RANDOM"
prop_checkBashisms18= verify checkBashisms "foo &> /dev/null" -- >>> prop $ verify checkBashisms "echo $((RANDOM%6+1))"
prop_checkBashisms19= verify checkBashisms "foo > file*.txt" -- >>> prop $ verify checkBashisms "foo &> /dev/null"
prop_checkBashisms20= verify checkBashisms "read -ra foo" -- >>> prop $ verify checkBashisms "foo > file*.txt"
prop_checkBashisms21= verify checkBashisms "[ -a foo ]" -- >>> prop $ verify checkBashisms "read -ra foo"
prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]" -- >>> prop $ verify checkBashisms "[ -a foo ]"
prop_checkBashisms23= verify checkBashisms "trap mything ERR INT" -- >>> prop $ verifyNot checkBashisms "[ foo -a bar ]"
prop_checkBashisms24= verifyNot checkBashisms "trap mything INT TERM" -- >>> prop $ verify checkBashisms "trap mything ERR INT"
prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123" -- >>> prop $ verifyNot checkBashisms "trap mything INT TERM"
prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM" -- >>> prop $ verify checkBashisms "cat < /dev/tcp/host/123"
prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*" -- >>> prop $ verify checkBashisms "trap mything ERR SIGTERM"
prop_checkBashisms28= verify checkBashisms "exec {n}>&2" -- >>> prop $ verify checkBashisms "echo *[^0-9]*"
prop_checkBashisms29= verify checkBashisms "echo ${!var}" -- >>> prop $ verify checkBashisms "exec {n}>&2"
prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\"" -- >>> prop $ verify checkBashisms "echo ${!var}"
prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\"" -- >>> prop $ verify checkBashisms "printf -v '%s' \"$1\""
prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]" -- >>> prop $ verify checkBashisms "printf '%q' \"$1\""
prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo" -- >>> prop $ verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo" -- >>> prop $ verify checkBashisms "#!/bin/sh\necho -n foo"
prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo" -- >>> prop $ verifyNot checkBashisms "#!/bin/dash\necho -n foo"
prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar" -- >>> prop $ verifyNot checkBashisms "#!/bin/dash\nlocal foo"
prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME" -- >>> prop $ verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM" -- >>> prop $ verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }" -- >>> prop $ verify checkBashisms "RANDOM=9; echo $RANDOM"
prop_checkBashisms40= verify checkBashisms "echo $(<file)" -- >>> prop $ verify checkBashisms "foo-bar() { true; }"
prop_checkBashisms41= verify checkBashisms "echo `<file`" -- >>> prop $ verify checkBashisms "echo $(<file)"
prop_checkBashisms42= verify checkBashisms "trap foo int" -- >>> prop $ verify checkBashisms "echo `<file`"
prop_checkBashisms43= verify checkBashisms "trap foo sigint" -- >>> prop $ verify checkBashisms "trap foo int"
prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int" -- >>> prop $ verify checkBashisms "trap foo sigint"
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT" -- >>> prop $ verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT" -- >>> prop $ verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null" -- >>> prop $ verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/dash\necho $LINENO" -- >>> prop $ verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE" -- >>> prop $ verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file" -- >>> prop $ verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1" -- >>> prop $ verify checkBashisms "#!/bin/sh\ncmd >& file"
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2" -- >>> prop $ verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n" -- >>> prop $ verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar" -- >>> prop $ verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
prop_checkBashisms55= verify checkBashisms "#!/bin/sh\necho ${@%foo}" -- >>> prop $ verify checkBashisms "#!/bin/sh\nfoo+=bar"
prop_checkBashisms56= verifyNot checkBashisms "#!/bin/sh\necho ${##}" -- >>> prop $ verify checkBashisms "#!/bin/sh\necho ${@%foo}"
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\necho ${##}"
checkBashisms = ForShell [Sh, Dash] $ \t -> do checkBashisms = ForShell [Sh, Dash] $ \t -> do
params <- ask params <- ask
kludge params t kludge params t
@@ -302,11 +301,11 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is") (re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")
] ]
bashVars = [ bashVars = [
"LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS" "DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
] ]
bashDynamicVars = [ "RANDOM", "SECONDS" ] bashDynamicVars = [ "RANDOM", "SECONDS" ]
dashVars = [ "LINENO" ] dashVars = [ ]
isBashVariable var = isBashVariable var =
(var `elem` bashDynamicVars (var `elem` bashDynamicVars
|| var `elem` bashVars && not (isAssigned var)) || var `elem` bashVars && not (isAssigned var))
@@ -317,8 +316,9 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
Assignment (_, _, name, _) -> name == var Assignment (_, _, name, _) -> name == var
_ -> False _ -> False
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')" -- |
prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')" -- >>> prop $ verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
-- >>> prop $ verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
checkEchoSed = ForShell [Bash, Ksh] f checkEchoSed = ForShell [Bash, Ksh] f
where where
f (T_Pipeline id _ [a, b]) = f (T_Pipeline id _ [a, b]) =
@@ -344,10 +344,11 @@ checkEchoSed = ForShell [Bash, Ksh] f
f _ = return () f _ = return ()
prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}" -- |
prop_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}" -- >>> prop $ verify checkBraceExpansionVars "echo {1..$n}"
prop_checkBraceExpansionVars3 = verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg" -- >>> prop $ verifyNot checkBraceExpansionVars "echo {1,3,$n}"
prop_checkBraceExpansionVars4 = verify checkBraceExpansionVars "echo {$i..100}" -- >>> prop $ verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg"
-- >>> prop $ verify checkBraceExpansionVars "echo {$i..100}"
checkBraceExpansionVars = ForShell [Bash] f checkBraceExpansionVars = ForShell [Bash] f
where where
f t@(T_BraceExpansion id list) = mapM_ check list f t@(T_BraceExpansion id list) = mapM_ check list
@@ -372,12 +373,13 @@ checkBraceExpansionVars = ForShell [Bash] f
return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval" return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval"
prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3" -- |
prop_checkMultiDimensionalArrays2 = verifyNot checkMultiDimensionalArrays "foo[a]=3" -- >>> prop $ verify checkMultiDimensionalArrays "foo[a][b]=3"
prop_checkMultiDimensionalArrays3 = verify checkMultiDimensionalArrays "foo=( [a][b]=c )" -- >>> prop $ verifyNot checkMultiDimensionalArrays "foo[a]=3"
prop_checkMultiDimensionalArrays4 = verifyNot checkMultiDimensionalArrays "foo=( [a]=c )" -- >>> prop $ verify checkMultiDimensionalArrays "foo=( [a][b]=c )"
prop_checkMultiDimensionalArrays5 = verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}" -- >>> prop $ verifyNot checkMultiDimensionalArrays "foo=( [a]=c )"
prop_checkMultiDimensionalArrays6 = verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}" -- >>> prop $ verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}"
-- >>> prop $ verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}"
checkMultiDimensionalArrays = ForShell [Bash] f checkMultiDimensionalArrays = ForShell [Bash] f
where where
f token = f token =
@@ -392,16 +394,17 @@ checkMultiDimensionalArrays = ForShell [Bash] f
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
isMultiDim t = getBracedModifier (bracedString t) `matches` re isMultiDim t = getBracedModifier (bracedString t) `matches` re
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '" -- |
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '" -- >>> prop $ verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '" -- >>> prop $ verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '" -- >>> prop $ verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '" -- >>> prop $ verify checkPS1Assignments "PS1=$'\\x1b[c '"
prop_checkPS14a= verify checkPS1Assignments "export PS1=$'\\e[3m; '" -- >>> prop $ verify checkPS1Assignments "PS1=$'\\e[3m; '"
prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '" -- >>> prop $ verify checkPS1Assignments "export PS1=$'\\e[3m; '"
prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '" -- >>> prop $ verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"
prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'" -- >>> prop $ verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
prop_checkPS18 = verifyNot checkPS1Assignments "PS1='\\[\\e\\]'" -- >>> prop $ verifyNot checkPS1Assignments "PS1='e033x1B'"
-- >>> prop $ verifyNot checkPS1Assignments "PS1='\\[\\e\\]'"
checkPS1Assignments = ForShell [Bash] f checkPS1Assignments = ForShell [Bash] f
where where
f token = case token of f token = case token of
@@ -417,7 +420,3 @@ checkPS1Assignments = ForShell [Bash] f
isJust $ matchRegex escapeRegex unenclosed isJust $ matchRegex escapeRegex unenclosed
enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033" escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -39,7 +39,7 @@ internalVariables = [
] ]
variablesWithoutSpaces = [ variablesWithoutSpaces = [
"$", "-", "?", "!", "$", "-", "?", "!", "#",
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO", "BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID", "OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES" "COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"

View File

@@ -30,17 +30,17 @@ data Formatter = Formatter {
footer :: IO () footer :: IO ()
} }
sourceFile (PositionedComment pos _ _) = posFile pos sourceFile = posFile . pcStartPos
lineNo (PositionedComment pos _ _) = posLine pos lineNo = posLine . pcStartPos
endLineNo (PositionedComment _ end _) = posLine end endLineNo = posLine . pcEndPos
colNo (PositionedComment pos _ _) = posColumn pos colNo = posColumn . pcStartPos
endColNo (PositionedComment _ end _) = posColumn end endColNo = posColumn . pcEndPos
codeNo (PositionedComment _ _ (Comment _ code _)) = code codeNo = cCode . pcComment
messageText (PositionedComment _ _ (Comment _ _ t)) = t messageText = cMessage . pcComment
severityText :: PositionedComment -> String severityText :: PositionedComment -> String
severityText (PositionedComment _ _ (Comment c _ _)) = severityText pc =
case c of case cSeverity (pcComment pc) of
ErrorC -> "error" ErrorC -> "error"
WarningC -> "warning" WarningC -> "warning"
InfoC -> "info" InfoC -> "info"
@@ -51,11 +51,14 @@ makeNonVirtual comments contents =
map fix comments map fix comments
where where
ls = lines contents ls = lines contents
fix c@(PositionedComment start end comment) = PositionedComment start { fix c = c {
posColumn = realignColumn lineNo colNo c pcStartPos = (pcStartPos c) {
} end { posColumn = realignColumn lineNo colNo c
posColumn = realignColumn endLineNo endColNo c }
} comment , pcEndPos = (pcEndPos c) {
posColumn = realignColumn endLineNo endColNo c
}
}
realignColumn lineNo colNo c = realignColumn lineNo colNo c =
if lineNo c > 0 && lineNo c <= fromIntegral (length ls) if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c) then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)

View File

@@ -39,8 +39,24 @@ format = do
footer = finish ref footer = finish ref
} }
instance ToJSON (PositionedComment) where instance ToJSON Replacement where
toJSON comment@(PositionedComment start end (Comment level code string)) = toJSON replacement =
let start = repStartPos replacement
end = repEndPos replacement
str = repString replacement in
object [
"line" .= posLine start,
"endLine" .= posLine end,
"column" .= posColumn start,
"endColumn" .= posColumn end,
"replaceWith" .= str
]
instance ToJSON PositionedComment where
toJSON comment =
let start = pcStartPos comment
end = pcEndPos comment
c = pcComment comment in
object [ object [
"file" .= posFile start, "file" .= posFile start,
"line" .= posLine start, "line" .= posLine start,
@@ -48,11 +64,15 @@ instance ToJSON (PositionedComment) where
"column" .= posColumn start, "column" .= posColumn start,
"endColumn" .= posColumn end, "endColumn" .= posColumn end,
"level" .= severityText comment, "level" .= severityText comment,
"code" .= code, "code" .= cCode c,
"message" .= string "message" .= cMessage c,
"fix" .= pcFix comment
] ]
toEncoding comment@(PositionedComment start end (Comment level code string)) = toEncoding comment =
let start = pcStartPos comment
end = pcEndPos comment
c = pcComment comment in
pairs ( pairs (
"file" .= posFile start "file" .= posFile start
<> "line" .= posLine start <> "line" .= posLine start
@@ -60,10 +80,16 @@ instance ToJSON (PositionedComment) where
<> "column" .= posColumn start <> "column" .= posColumn start
<> "endColumn" .= posColumn end <> "endColumn" .= posColumn end
<> "level" .= severityText comment <> "level" .= severityText comment
<> "code" .= code <> "code" .= cCode c
<> "message" .= string <> "message" .= cMessage c
<> "fix" .= pcFix comment
) )
instance ToJSON Fix where
toJSON fix = object [
"replacements" .= fixReplacements fix
]
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
collectResult ref result _ = collectResult ref result _ =
modifyIORef ref (\x -> crComments result ++ x) modifyIORef ref (\x -> crComments result ++ x)
@@ -71,4 +97,3 @@ collectResult ref result _ =
finish ref = do finish ref = do
list <- readIORef ref list <- readIORef ref
BL.putStrLn $ encode list BL.putStrLn $ encode list

View File

@@ -22,18 +22,28 @@ module ShellCheck.Formatter.TTY (format) where
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Formatter.Format import ShellCheck.Formatter.Format
import Control.Monad
import Data.IORef
import Data.List import Data.List
import Data.Maybe
import GHC.Exts import GHC.Exts
import System.Info
import System.IO 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 :: FormatterOptions -> IO Formatter
format options = return Formatter { format options = do
header = return (), topErrorRef <- newIORef []
footer = return (), return Formatter {
onFailure = outputError options, header = return (),
onResult = outputResult options footer = outputWiki topErrorRef,
} onFailure = outputError options,
onResult = outputResult options topErrorRef
}
colorForLevel level = colorForLevel level =
case level of case level of
@@ -45,13 +55,60 @@ colorForLevel level =
"source" -> 0 -- none "source" -> 0 -- none
_ -> 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 outputError options file error = do
color <- getColorFunc $ foColorOption options color <- getColorFunc $ foColorOption options
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
outputResult options result sys = do outputResult options ref result sys = do
color <- getColorFunc $ foColorOption options color <- getColorFunc $ foColorOption options
let comments = crComments result let comments = crComments result
appendComments ref comments (fromIntegral $ foWikiLinkCount options)
let fileGroups = groupWith sourceFile comments let fileGroups = groupWith sourceFile comments
mapM_ (outputForFile color sys) fileGroups mapM_ (outputForFile color sys) fileGroups
@@ -62,8 +119,8 @@ outputForFile color sys comments = do
let fileLines = lines contents let fileLines = lines contents
let lineCount = fromIntegral $ length fileLines let lineCount = fromIntegral $ length fileLines
let groups = groupWith lineNo comments let groups = groupWith lineNo comments
mapM_ (\x -> do mapM_ (\commentsForLine -> do
let lineNum = lineNo (head x) let lineNum = lineNo (head commentsForLine)
let line = if lineNum < 1 || lineNum > lineCount let line = if lineNum < 1 || lineNum > lineCount
then "" then ""
else fileLines !! fromIntegral (lineNum - 1) else fileLines !! fromIntegral (lineNum - 1)
@@ -71,16 +128,74 @@ outputForFile color sys comments = do
putStrLn $ color "message" $ putStrLn $ color "message" $
"In " ++ fileName ++" line " ++ show lineNum ++ ":" "In " ++ fileName ++" line " ++ show lineNum ++ ":"
putStrLn (color "source" line) putStrLn (color "source" line)
mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) x mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) commentsForLine
putStrLn "" putStrLn ""
showFixedString color comments lineNum line
) groups ) groups
hasApplicableFix lineNum comment = fromMaybe False $ do
replacements <- fixReplacements <$> pcFix comment
guard $ all (\c -> onSameLine (repStartPos c) && onSameLine (repEndPos c)) replacements
return True
where
onSameLine pos = posLine pos == lineNum
-- FIXME: Work correctly with multiple replacements
showFixedString color comments lineNum line =
case filter (hasApplicableFix lineNum) comments of
(first:_) -> do
-- in the spirit of error prone
putStrLn $ color "message" "Did you mean: "
putStrLn $ fixedString first line
putStrLn ""
_ -> return ()
-- need to do something smart about sorting by end index
fixedString :: PositionedComment -> String -> String
fixedString comment line =
case (pcFix comment) of
Nothing -> ""
Just rs ->
applyReplacement (fixReplacements rs) line 0
where
applyReplacement [] s _ = s
applyReplacement (rep:xs) s offset =
let replacementString = repString rep
start = (posColumn . repStartPos) rep
end = (posColumn . repEndPos) rep
z = doReplace start end s replacementString
len_r = (fromIntegral . length) replacementString in
applyReplacement xs z (offset + (end - start) + len_r)
-- FIXME: Work correctly with tabs
-- start and end comes from pos, which is 1 based
-- doReplace 0 0 "1234" "A" -> "A1234" -- technically not valid
-- doReplace 1 1 "1234" "A" -> "A1234"
-- doReplace 1 2 "1234" "A" -> "A234"
-- doReplace 3 3 "1234" "A" -> "12A34"
-- doReplace 4 4 "1234" "A" -> "123A4"
-- doReplace 5 5 "1234" "A" -> "1234A"
doReplace start end o r =
let si = fromIntegral (start-1)
ei = fromIntegral (end-1)
(x, xs) = splitAt si o
(y, z) = splitAt (ei - si) xs
in
x ++ r ++ z
cuteIndent :: PositionedComment -> String cuteIndent :: PositionedComment -> String
cuteIndent comment = cuteIndent comment =
replicate (fromIntegral $ colNo comment - 1) ' ' ++ 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 getColorFunc colorOption = do
term <- hIsTerminalDevice stdout term <- hIsTerminalDevice stdout

View File

@@ -17,10 +17,51 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
module ShellCheck.Interface where {-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
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, asTokenPositions)
, 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, pcFix)
, ColorOption(ColorAuto, ColorAlways, ColorNever)
, TokenComment(tcId, tcComment, tcFix)
, emptyCheckResult
, newParseResult
, newAnalysisSpec
, newAnalysisResult
, newFormatterOptions
, newPosition
, newTokenComment
, mockedSystemInterface
, newParseSpec
, emptyCheckSpec
, newPositionedComment
, newComment
, Fix(fixReplacements)
, newFix
, Replacement(repStartPos, repEndPos, repString)
, newReplacement
) where
import ShellCheck.AST import ShellCheck.AST
import Control.DeepSeq
import Control.Monad.Identity import Control.Monad.Identity
import Data.Monoid
import GHC.Generics (Generic)
import qualified Data.Map as Map import qualified Data.Map as Map
@@ -35,7 +76,8 @@ data CheckSpec = CheckSpec {
csScript :: String, csScript :: String,
csCheckSourced :: Bool, csCheckSourced :: Bool,
csExcludedWarnings :: [Integer], csExcludedWarnings :: [Integer],
csShellTypeOverride :: Maybe Shell csShellTypeOverride :: Maybe Shell,
csMinSeverity :: Severity
} deriving (Show, Eq) } deriving (Show, Eq)
data CheckResult = CheckResult { data CheckResult = CheckResult {
@@ -43,44 +85,85 @@ data CheckResult = CheckResult {
crComments :: [PositionedComment] crComments :: [PositionedComment]
} deriving (Show, Eq) } deriving (Show, Eq)
emptyCheckResult :: CheckResult
emptyCheckResult = CheckResult {
crFilename = "",
crComments = []
}
emptyCheckSpec :: CheckSpec emptyCheckSpec :: CheckSpec
emptyCheckSpec = CheckSpec { emptyCheckSpec = CheckSpec {
csFilename = "", csFilename = "",
csScript = "", csScript = "",
csCheckSourced = False, csCheckSourced = False,
csExcludedWarnings = [], csExcludedWarnings = [],
csShellTypeOverride = Nothing csShellTypeOverride = Nothing,
csMinSeverity = StyleC
}
newParseSpec :: ParseSpec
newParseSpec = ParseSpec {
psFilename = "",
psScript = "",
psCheckSourced = False,
psShellTypeOverride = Nothing
} }
-- Parser input and output -- Parser input and output
data ParseSpec = ParseSpec { data ParseSpec = ParseSpec {
psFilename :: String, psFilename :: String,
psScript :: String, psScript :: String,
psCheckSourced :: Bool psCheckSourced :: Bool,
psShellTypeOverride :: Maybe Shell
} deriving (Show, Eq) } deriving (Show, Eq)
data ParseResult = ParseResult { data ParseResult = ParseResult {
prComments :: [PositionedComment], prComments :: [PositionedComment],
prTokenPositions :: Map.Map Id Position, prTokenPositions :: Map.Map Id (Position, Position),
prRoot :: Maybe Token prRoot :: Maybe Token
} deriving (Show, Eq) } deriving (Show, Eq)
newParseResult :: ParseResult
newParseResult = ParseResult {
prComments = [],
prTokenPositions = Map.empty,
prRoot = Nothing
}
-- Analyzer input and output -- Analyzer input and output
data AnalysisSpec = AnalysisSpec { data AnalysisSpec = AnalysisSpec {
asScript :: Token, asScript :: Token,
asShellType :: Maybe Shell, asShellType :: Maybe Shell,
asExecutionMode :: ExecutionMode, asExecutionMode :: ExecutionMode,
asCheckSourced :: Bool asCheckSourced :: Bool,
asTokenPositions :: Map.Map Id (Position, Position)
}
newAnalysisSpec token = AnalysisSpec {
asScript = token,
asShellType = Nothing,
asExecutionMode = Executed,
asCheckSourced = False,
asTokenPositions = Map.empty
} }
newtype AnalysisResult = AnalysisResult { newtype AnalysisResult = AnalysisResult {
arComments :: [TokenComment] arComments :: [TokenComment]
} }
newAnalysisResult = AnalysisResult {
arComments = []
}
-- Formatter options -- Formatter options
newtype FormatterOptions = FormatterOptions { data FormatterOptions = FormatterOptions {
foColorOption :: ColorOption foColorOption :: ColorOption,
foWikiLinkCount :: Integer
}
newFormatterOptions = FormatterOptions {
foColorOption = ColorAuto,
foWikiLinkCount = 3
} }
@@ -91,16 +174,81 @@ data ExecutionMode = Executed | Sourced deriving (Show, Eq)
type ErrorMessage = String type ErrorMessage = String
type Code = Integer type Code = Integer
data Severity = ErrorC | WarningC | InfoC | StyleC deriving (Show, Eq, Ord) data Severity = ErrorC | WarningC | InfoC | StyleC
deriving (Show, Eq, Ord, Generic, NFData)
data Position = Position { data Position = Position {
posFile :: String, -- Filename posFile :: String, -- Filename
posLine :: Integer, -- 1 based source line posLine :: Integer, -- 1 based source line
posColumn :: Integer -- 1 based source column, where tabs are 8 posColumn :: Integer -- 1 based source column, where tabs are 8
} deriving (Show, Eq) } deriving (Show, Eq, Generic, NFData)
data Comment = Comment Severity Code String deriving (Show, Eq) newPosition :: Position
data PositionedComment = PositionedComment Position Position Comment deriving (Show, Eq) newPosition = Position {
data TokenComment = TokenComment Id Comment deriving (Show, Eq) posFile = "",
posLine = 1,
posColumn = 1
}
data Comment = Comment {
cSeverity :: Severity,
cCode :: Code,
cMessage :: String
} deriving (Show, Eq, Generic, NFData)
newComment :: Comment
newComment = Comment {
cSeverity = StyleC,
cCode = 0,
cMessage = ""
}
-- only support single line for now
data Replacement = Replacement {
repStartPos :: Position,
repEndPos :: Position,
repString :: String
} deriving (Show, Eq, Generic, NFData)
newReplacement = Replacement {
repStartPos = newPosition,
repEndPos = newPosition,
repString = ""
}
data Fix = Fix {
fixReplacements :: [Replacement]
} deriving (Show, Eq, Generic, NFData)
newFix = Fix {
fixReplacements = []
}
data PositionedComment = PositionedComment {
pcStartPos :: Position,
pcEndPos :: Position,
pcComment :: Comment,
pcFix :: Maybe Fix
} deriving (Show, Eq, Generic, NFData)
newPositionedComment :: PositionedComment
newPositionedComment = PositionedComment {
pcStartPos = newPosition,
pcEndPos = newPosition,
pcComment = newComment,
pcFix = Nothing
}
data TokenComment = TokenComment {
tcId :: Id,
tcComment :: Comment,
tcFix :: Maybe Fix
} deriving (Show, Eq, Generic, NFData)
newTokenComment = TokenComment {
tcId = Id 0,
tcComment = newComment,
tcFix = Nothing
}
data ColorOption = data ColorOption =
ColorAuto ColorAuto

File diff suppressed because it is too large Load Diff

View File

@@ -1,35 +1,3 @@
# This file was automatically generated by stack init resolver: lts-12.9
# 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
# Local packages, usually specified by relative directory name
packages: packages:
- '.' - '.'
# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3)
extra-deps: []
# Override default flag values for local packages and extra-deps
flags: {}
# Extra package databases containing global packages
extra-package-dbs: []
# Control whether we use the GHC we find on the path
# system-ghc: true
# Require a specific version of stack, using version ranges
# require-stack-version: -any # Default
# require-stack-version: >= 1.0.0
# Override the architecture used by stack, especially useful on Windows
# arch: i386
# arch: x86_64
# Extra directories used by stack for building
# extra-include-dirs: [/path/to/dir]
# extra-lib-dirs: [/path/to/dir]
# Allow a newer minor version of GHC than the snapshot specifies
# compiler-check: newer-minor

View File

@@ -1,77 +1,2 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# This file strips all unit tests from ShellCheck, removing # This file was deprecated by the doctest build.
# the dependency on QuickCheck and Template Haskell and
# reduces the binary size considerably.
set -o pipefail
sponge() {
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() {
echo "-- 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"

12
test/doctests.hs Normal file
View File

@@ -0,0 +1,12 @@
module Main where
import Build_doctests (flags, pkgs, module_sources)
import Data.Foldable (traverse_)
import Test.DocTest (doctest)
main :: IO ()
main = do
traverse_ putStrLn args
doctest args
where
args = flags ++ pkgs ++ module_sources

View File

@@ -1,24 +0,0 @@
module Main where
import Control.Monad
import System.Exit
import qualified ShellCheck.Checker
import qualified ShellCheck.Analytics
import qualified ShellCheck.AnalyzerLib
import qualified ShellCheck.Parser
import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.ShellSupport
main = do
putStrLn "Running ShellCheck tests..."
results <- sequence [
ShellCheck.Checker.runTests,
ShellCheck.Checks.Commands.runTests,
ShellCheck.Checks.ShellSupport.runTests,
ShellCheck.Analytics.runTests,
ShellCheck.AnalyzerLib.runTests,
ShellCheck.Parser.runTests
]
if and results
then exitSuccess
else exitFailure

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"