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

3
.gitignore vendored
View File

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

View File

@@ -27,7 +27,7 @@ do
zip "${file%.*}.zip" README.txt LICENSE.txt "$file"
done
for file in *.linux
for file in *.linux-x86_64
do
base="${file%.*}"
cp "$file" "shellcheck"
@@ -35,6 +35,14 @@ do
rm "shellcheck"
done
for file in *.linux-armv6hf
do
base="${file%.*}"
cp "$file" "shellcheck"
tar -cJf "$base.linux.armv6hf.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in ./*
do
sha512sum "$file" > "$file.sha512sum"

14
.snapsquid.conf Normal file
View File

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

View File

@@ -11,6 +11,7 @@ before_install:
- TAGS=""
- test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
- test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
- echo "Tags are $TAGS"
script:
- mkdir deploy
@@ -29,18 +30,21 @@ script:
- docker rm "$id"
- ls -l shellcheck
- ./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
- name="$DOCKER_BASE-alpine"
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
- sed -e '/DELETE-MARKER/,$d' Dockerfile > Dockerfile.alpine
- docker build -f Dockerfile.alpine -t "$name:current" .
- docker run "$name:current" sh -c 'shellcheck --version'
# Linux armv6hf static executable
- docker run -v "$PWD:/mnt" koalaman/armv6hf-builder -c 'compile-shellcheck'
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux-armv6hf"; done
- rm -f shellcheck || true
# Windows .exe
- docker pull koalaman/winghc
- docker run --user="$UID" --rm -v "$PWD:/appdata" koalaman/winghc cuib
- docker run --user="$UID" -v "$PWD:/appdata" koalaman/winghc cuib
- for tag in $TAGS; do cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe"; done
- rm -rf dist || true
- rm -rf dist shellcheck || true
# Misc packaging
- ./.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
### Added
- SC2233/SC2234/SC2235: Suggest removing or replacing (..) around tests

View File

@@ -1,22 +1,55 @@
# Build-only image
FROM ubuntu:17.10 AS build
FROM ubuntu:18.04 AS build
USER root
WORKDIR /opt/shellCheck
# Install OS deps
RUN apt-get update && apt-get install -y ghc cabal-install
# Install OS deps, including GHC from HVR-PPA
# 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
# (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 ./
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 LICENSE Setup.hs shellcheck.hs ./
COPY LICENSE Setup.hs shellcheck.hs shellcheck.1.md ./
COPY src src
RUN cabal build Paths_ShellCheck && \
ghc -optl-static -optl-pthread -isrc -idist/build/autogen --make shellcheck && \
strip --strip-all shellcheck
COPY test test
# This SED is the only "nastyness" we have to do
# 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 && \
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
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:
![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
@@ -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).
* 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).
@@ -133,6 +133,10 @@ On OS X with homebrew:
brew install shellcheck
On OpenBSD:
pkg_add shellcheck
On openSUSE
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:
* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
* [Linux, armv6hf](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-stable.zip)
or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, 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 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
- 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"
- shellcheck() { "shellcheck-$scversion/shellcheck" "$@"; }
- shellcheck --version
*Pre-requisite*: the program 'xz' needs to be installed on the system.
To install it on debian/ubuntu/linux mint, run `apt install xz-utils`.
To install it on Redhat/Fedora/CentOS, run `yum -y install xz`.
script:
- shellcheck *.sh
```bash
export scversion="stable" # or "v0.4.7", or "latest"
wget "https://storage.googleapis.com/shellcheck/shellcheck-${scversion}.linux.x86_64.tar.xz"
tar --xz -xvf shellcheck-"${scversion}".linux.x86_64.tar.xz
cp shellcheck-"${scversion}"/shellcheck /usr/bin/
shellcheck --version
```
## Compiling from source
@@ -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.
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 (
HookedBuildInfo,
emptyHookedBuildInfo )
@@ -9,12 +14,42 @@ import Distribution.Simple (
import Distribution.Simple.Setup ( SDistFlags )
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
where
myHooks = simpleUserHooks { preSDist = myPreSDist }
#endif
-- | This hook will be executed before e.g. @cabal sdist@. It runs
-- pandoc to create the man page from shellcheck.1.md. If the pandoc
-- command is not found, this will fail with an error message:
@@ -27,10 +62,20 @@ main = defaultMainWithHooks myHooks
--
myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo
myPreSDist _ _ = do
putStrLn "Building the man page (shellcheck.1) with pandoc..."
putStrLn pandoc_cmd
result <- system pandoc_cmd
putStrLn $ "pandoc exited with " ++ show result
exists <- doesFileExist "shellcheck.1"
if exists
then do
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
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"

View File

@@ -1,5 +1,5 @@
Name: ShellCheck
Version: 0.5.0
Version: 0.6.0
Synopsis: Shell script analysis tool
License: GPL-3
License-file: LICENSE
@@ -28,14 +28,14 @@ Extra-Source-Files:
shellcheck.1.md
-- built with a cabal sdist hook
shellcheck.1
-- tests
test/shellcheck.hs
custom-setup
setup-depends:
base >= 4 && <5,
process >= 1.0 && <1.7,
Cabal >= 1.10 && <2.3
base >= 4 && <5,
directory >= 1.2 && <1.4,
process >= 1.0 && <1.7,
cabal-doctest >= 1.0.6 && <1.1,
Cabal >= 1.10 && <2.5
source-repository head
type: git
@@ -53,11 +53,11 @@ library
base > 4.6.0.1 && < 5,
bytestring,
containers >= 0.5,
deepseq >= 1.4.0.0,
directory,
mtl >= 2.2.1,
parsec,
regex-tdfa,
QuickCheck >= 2.7.4,
-- When cabal supports it, move this to setup-depends:
process
exposed-modules:
@@ -89,27 +89,29 @@ executable shellcheck
aeson,
base >= 4 && < 5,
bytestring,
deepseq >= 1.4.0.0,
ShellCheck,
containers,
directory,
mtl >= 2.2.1,
parsec >= 3.0,
QuickCheck >= 2.7.4,
regex-tdfa
main-is: shellcheck.hs
test-suite test-shellcheck
type: exitcode-stdio-1.0
build-depends:
aeson,
base >= 4 && < 5,
bytestring,
ShellCheck,
containers,
directory,
mtl >= 2.2.1,
parsec,
QuickCheck >= 2.7.4,
regex-tdfa
main-is: test/shellcheck.hs
-- Marker to add flags for static linking
-- STATIC
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.
# This allows testing changes without recompiling.
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
# quicktest runs the ShellCheck unit tests in an interpreted mode.
# This allows running tests without compiling, which can be faster.
# shellcheck disable=SC2091
# 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.
(
var=$(echo 'liftM and $ sequence [
ShellCheck.Analytics.runTests
,ShellCheck.Parser.runTests
,ShellCheck.Checker.runTests
,ShellCheck.Checks.Commands.runTests
,ShellCheck.Checks.ShellSupport.runTests
,ShellCheck.AnalyzerLib.runTests
]' | tr -d '\n' | cabal repl 2>&1 | tee /dev/stderr)
if [[ $var == *$'\nTrue'* ]]
then
exit 0
else
grep -C 3 -e "Fail" -e "Tracing" <<< "$var"
exit 1
fi
) 2>&1
$(find dist -type f -name doctests)
# Note: if you have build the project with new-build
#
# % cabal new-build -w ghc-8.4.3 --enable-tests
#
# and have cabal-plan installed (e.g. with cabal new-install cabal-plan),
# then you can quicktest with
#
# % $(cabal-plan list-bin doctests)
#
# Once the test executable exists, we can simply run it to perform doctests
# which use GHCi under the hood.

View File

@@ -56,6 +56,11 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
standard output. Subsequent **-f** options are ignored, see **FORMATS**
below for more information.
**-S**\ *SEVERITY*,\ **--severity=***severity*
: Specify minimum severity of errors to consider. Valid values are *error*,
*warning*, *info* and *style*. The default is *style*.
**-s**\ *shell*,\ **--shell=***shell*
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
@@ -66,6 +71,11 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
: Print version information and exit.
**-W** *NUM*,\ **--wiki-link-count=NUM**
: For TTY output, show *NUM* wiki links to more information about mentioned
warnings. Set to 0 to disable them entirely.
**-x**,\ **--external-sources**
: Follow 'source' statements even when the file is not specified as input.

View File

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

View File

@@ -37,9 +37,16 @@ parts:
source: ./
build-packages:
- cabal-install
- squid3
build: |
# See comments in .snapsquid.conf
[ "$http_proxy" ] && {
squid3 -f .snapsquid.conf
export http_proxy="http://localhost:8888"
sleep 3
}
cabal sandbox init
cabal update
cabal update || cat /var/log/squid/*
cabal install -j
install: |
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
module ShellCheck.AST where
import GHC.Generics (Generic)
import Control.Monad.Identity
import Control.DeepSeq
import Text.Parsec
import qualified ShellCheck.Regex as Re
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 Dashed = Dashed | Undashed deriving (Show, Eq)

View File

@@ -23,6 +23,7 @@ import ShellCheck.AST
import Control.Monad.Writer
import Control.Monad
import Data.Char
import Data.Functor
import Data.List
import Data.Maybe
@@ -226,8 +227,43 @@ getLiteralStringExt more = g
g (T_SingleQuoted _ s) = return s
g (T_Literal _ s) = return s
g (T_ParamSubSpecialChar _ s) = return s
g (T_DollarSingleQuoted _ s) = return $ decodeEscapes s
g x = more x
-- Bash style $'..' decoding
decodeEscapes ('\\':c:cs) =
case c of
'a' -> '\a' : rest
'b' -> '\b' : rest
'e' -> '\x1B' : rest
'f' -> '\f' : rest
'n' -> '\n' : rest
'r' -> '\r' : rest
't' -> '\t' : rest
'v' -> '\v' : rest
'\'' -> '\'' : rest
'"' -> '"' : rest
'\\' -> '\\' : rest
'x' ->
case cs of
(x:y:more) ->
if isHexDigit x && isHexDigit y
then chr (16*(digitToInt x) + (digitToInt y)) : rest
else '\\':c:rest
_ | isOctDigit c ->
let digits = take 3 $ takeWhile isOctDigit (c:cs)
num = parseOct digits
in (if num < 256 then chr num else '?') : rest
_ -> '\\' : c : rest
where
rest = decodeEscapes cs
parseOct = f 0
where
f n "" = n
f n (c:rest) = f (n * 8 + digitToInt c) rest
decodeEscapes (c:cs) = c : decodeEscapes cs
decodeEscapes [] = []
-- Is this token a string literal?
isLiteral t = isJust $ getLiteralString t
@@ -257,17 +293,27 @@ getCommand t =
T_Annotation _ _ t -> getCommand t
_ -> Nothing
-- Maybe get the command name of a token representing a command
getCommandName t = do
-- Maybe get the command name string of a token representing a command
getCommandName :: Token -> Maybe String
getCommandName = fst . getCommandNameAndToken
-- Get the command name token from a command, i.e.
-- the token representing 'ls' in 'ls -la 2> foo'.
-- If it can't be determined, return the original token.
getCommandTokenOrThis = snd . getCommandNameAndToken
getCommandNameAndToken :: Token -> (Maybe String, Token)
getCommandNameAndToken t = fromMaybe (Nothing, t) $ do
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
s <- getLiteralString w
if "busybox" `isSuffixOf` s || "builtin" == s
then
case rest of
(applet:_) -> getLiteralString applet
_ -> return s
(applet:_) -> return (getLiteralString applet, applet)
_ -> return (Just s, w)
else
return s
return (Just s, w)
-- If a command substitution is a single command, get its name.
-- $(date +%s) = Just "date"

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
analyzeScript :: AnalysisSpec -> AnalysisResult
analyzeScript spec = AnalysisResult {
analyzeScript spec = newAnalysisResult {
arComments =
filterByAnnotation spec params . nub $
runAnalytics spec

View File

@@ -18,7 +18,6 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.AnalyzerLib where
import ShellCheck.AST
import ShellCheck.ASTLib
@@ -28,6 +27,7 @@ import ShellCheck.Parser
import ShellCheck.Regex
import Control.Arrow (first)
import Control.DeepSeq
import Control.Monad.Identity
import Control.Monad.RWS
import Control.Monad.State
@@ -38,8 +38,9 @@ import qualified Data.Map as Map
import Data.Maybe
import Data.Semigroup
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (maxSuccess, quickCheckWithResult, stdArgs)
prop :: Bool -> IO ()
prop False = putStrLn "FAIL"
prop True = return ()
type Analysis = AnalyzerM ()
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
shellType :: Shell, -- The shell type, such as Bash or Ksh
shellTypeSpecified :: Bool, -- True if shell type was forced via flags
rootNode :: Token -- The root node of the AST
}
rootNode :: Token, -- The root node of the AST
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
data Cache = Cache {}
@@ -109,35 +111,42 @@ data DataSource =
data VariableState = Dead Token String | Alive deriving (Show)
defaultSpec root = AnalysisSpec {
asScript = root,
defaultSpec pr = spec {
asShellType = Nothing,
asCheckSourced = False,
asExecutionMode = Executed
}
asExecutionMode = Executed,
asTokenPositions = prTokenPositions pr
} where spec = newAnalysisSpec (fromJust $ prRoot pr)
pScript s =
let
pSpec = ParseSpec {
pSpec = newParseSpec {
psFilename = "script",
psScript = s,
psCheckSourced = False
psScript = s
}
in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec
in runIdentity $ parseScript (mockedSystemInterface []) pSpec
-- For testing. If parsed, returns whether there are any comments
producesComments :: Checker -> String -> Maybe Bool
producesComments c s = do
root <- pScript s
let spec = defaultSpec root
let pr = pScript s
prRoot pr
let spec = defaultSpec pr
let params = makeParameters spec
return . not . null $ runChecker params c
makeComment :: Severity -> Id -> Code -> String -> TokenComment
makeComment severity id code note =
TokenComment id $ Comment severity code note
newTokenComment {
tcId = id,
tcComment = newComment {
cSeverity = severity,
cCode = code,
cMessage = note
}
}
addComment note = tell [note]
addComment note = note `deepseq` tell [note]
warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m ()
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
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 =
let params = Parameters {
rootNode = root,
@@ -159,7 +182,8 @@ makeParameters spec =
shellTypeSpecified = isJust $ asShellType spec,
parentMap = getParentTree root,
variableFlow = getVariableFlow params root
variableFlow = getVariableFlow params root,
tokenPositions = asTokenPositions spec
} in params
where root = asScript spec
@@ -191,16 +215,16 @@ containsLastpipe root =
_ -> False
prop_determineShell0 = determineShell (fromJust $ pScript "#!/bin/sh") == Sh
prop_determineShell1 = determineShell (fromJust $ pScript "#!/usr/bin/env ksh") == Ksh
prop_determineShell2 = determineShell (fromJust $ pScript "") == Bash
prop_determineShell3 = determineShell (fromJust $ pScript "#!/bin/sh -e") == Sh
prop_determineShell4 = determineShell (fromJust $ pScript
"#!/bin/ksh\n#shellcheck shell=sh\nfoo") == Sh
prop_determineShell5 = determineShell (fromJust $ pScript
"#shellcheck shell=sh\nfoo") == Sh
prop_determineShell6 = determineShell (fromJust $ pScript "#! /bin/sh") == Sh
prop_determineShell7 = determineShell (fromJust $ pScript "#! /bin/ash") == Dash
-- |
-- >>> prop $ determineShellTest "#!/bin/sh" == Sh
-- >>> prop $ determineShellTest "#!/usr/bin/env ksh" == Ksh
-- >>> prop $ determineShellTest "" == Bash
-- >>> prop $ determineShellTest "#!/bin/sh -e" == Sh
-- >>> prop $ determineShellTest "#!/bin/ksh\n#shellcheck shell=sh\nfoo" == Sh
-- >>> prop $ determineShellTest "#shellcheck shell=sh\nfoo" == Sh
-- >>> prop $ determineShellTest "#! /bin/sh" == Sh
-- >>> prop $ determineShellTest "#! /bin/ash" == Dash
determineShellTest = determineShell . fromJust . prRoot . pScript
determineShell t = fromMaybe Bash $ do
shellString <- foldl mplus Nothing $ getCandidates t
shellForExecutable shellString
@@ -235,9 +259,10 @@ getParentTree t =
where
pre t = modify (first ((:) t))
post t = do
(_:rest, map) <- get
case rest of [] -> put (rest, map)
(x:_) -> put (rest, Map.insert (getId t) x map)
(x, map) <- get
case x of
_:rest -> case rest of [] -> put (rest, map)
(x:_) -> put (rest, Map.insert (getId t) x map)
-- Given a root node, make a map from Id to Token
getTokenMap :: Token -> Map.Map Id Token
@@ -520,12 +545,22 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
getReferencedVariableCommand _ = []
-- The function returns a tuple consisting of four items describing an assignment.
-- Given e.g. declare foo=bar
-- (
-- BaseCommand :: Token, -- The command/structure assigning the variable, i.e. declare foo=bar
-- AssignmentToken :: Token, -- The specific part that assigns this variable, i.e. foo=bar
-- VariableName :: String, -- The variable name, i.e. foo
-- VariableValue :: DataType -- A description of the value being assigned, i.e. "Literal string with value foo"
-- )
getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
case x of
"read" ->
let params = map getLiteral rest in
catMaybes . takeWhile isJust . reverse $ params
let params = map getLiteral rest
readArrayVars = getReadArrayVariables rest
in
catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params
"getopts" ->
case rest of
opts:var:_ -> maybeToList $ getLiteral var
@@ -568,10 +603,14 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
where
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
getLiteral t = do
getLiteralOfDataType t d = do
s <- getLiteralString t
when ("-" `isPrefixOf` s) $ fail "argument"
return (base, t, s, DataString SourceExternal)
return (base, t, s, d)
getLiteral t = getLiteralOfDataType t (DataString SourceExternal)
getLiteralArray t = getLiteralOfDataType t (DataArray SourceExternal)
getModifierParamString = getModifierParam DataString
@@ -613,6 +652,11 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
guard $ isVariableName name
return (base, lastArg, name, DataArray SourceExternal)
-- get all the array variables used in read, e.g. read -a arr
getReadArrayVariables args = do
map (getLiteralArray . snd)
(filter (\(x,_) -> getLiteralString x == Just "-a") (zip (args) (tail args)))
getModifiedVariableCommand _ = []
getIndexReferences s = fromMaybe [] $ do
@@ -622,10 +666,11 @@ getIndexReferences s = fromMaybe [] $ do
where
re = mkRegex "(\\[.*\\])"
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
-- |
-- >>> prop $ getOffsetReferences ":bar" == ["bar"]
-- >>> prop $ getOffsetReferences ":bar:baz" == ["bar", "baz"]
-- >>> prop $ getOffsetReferences "[foo]:bar" == ["bar"]
-- >>> prop $ getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
getOffsetReferences mods = fromMaybe [] $ do
-- if mods start with [, then drop until ]
match <- matchRegex re mods
@@ -700,9 +745,15 @@ isUnqualifiedCommand token str = isCommandMatch token (== str)
isCommandMatch token matcher = fromMaybe False $
fmap matcher (getCommandName token)
-- |
-- 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 ('*':_) = True
isConfusedGlobRegex [x,'*'] | x /= '\\' = True
@@ -712,9 +763,10 @@ isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
isVariableChar x = isVariableStartChar x || isDigit x
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
prop_isVariableName1 = isVariableName "_fo123"
prop_isVariableName2 = not $ isVariableName "4"
prop_isVariableName3 = not $ isVariableName "test: "
-- |
-- >>> prop $ isVariableName "_fo123"
-- >>> prop $ not $ isVariableName "4"
-- >>> prop $ not $ isVariableName "test: "
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
isVariableName _ = False
@@ -723,27 +775,28 @@ getVariablesFromLiteralToken token =
-- Try to get referenced variables from a literal string like "$foo"
-- Ignores tons of cases like arithmetic evaluation and array indices.
prop_getVariablesFromLiteral1 =
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
-- >>> prop $ getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
getVariablesFromLiteral string =
map (!! 0) $ matchAllSubgroups variableRegex string
where
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
-- |
-- Get the variable name from an expansion like ${var:-foo}
prop_getBracedReference1 = getBracedReference "foo" == "foo"
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
prop_getBracedReference3 = getBracedReference "#" == "#"
prop_getBracedReference4 = getBracedReference "##" == "#"
prop_getBracedReference5 = getBracedReference "#!" == "!"
prop_getBracedReference6 = getBracedReference "!#" == "#"
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
prop_getBracedReference11= getBracedReference "!os*" == ""
prop_getBracedReference12= getBracedReference "!os?bar**" == ""
prop_getBracedReference13= getBracedReference "foo[bar]" == "foo"
--
-- >>> prop $ getBracedReference "foo" == "foo"
-- >>> prop $ getBracedReference "#foo" == "foo"
-- >>> prop $ getBracedReference "#" == "#"
-- >>> prop $ getBracedReference "##" == "#"
-- >>> prop $ getBracedReference "#!" == "!"
-- >>> prop $ getBracedReference "!#" == "#"
-- >>> prop $ getBracedReference "!foo#?" == "foo"
-- >>> prop $ getBracedReference "foo-bar" == "foo"
-- >>> prop $ getBracedReference "foo:-bar" == "foo"
-- >>> prop $ getBracedReference "foo: -1" == "foo"
-- >>> prop $ getBracedReference "!os*" == ""
-- >>> prop $ getBracedReference "!os?bar**" == ""
-- >>> prop $ getBracedReference "foo[bar]" == "foo"
getBracedReference s = fromMaybe s $
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
where
@@ -766,9 +819,10 @@ getBracedReference s = fromMaybe s $
return ""
nameExpansion _ = Nothing
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
-- |
-- >>> prop $ getBracedModifier "foo:bar:baz" == ":bar:baz"
-- >>> prop $ getBracedModifier "!var:-foo" == ":-foo"
-- >>> prop $ getBracedModifier "foo[bar]" == "[bar]"
getBracedModifier s = fromMaybe "" . listToMaybe $ do
let var = getBracedReference s
a <- dropModifier s
@@ -785,10 +839,13 @@ getBracedModifier s = fromMaybe "" . listToMaybe $ do
-- Run an action in a Maybe (or do nothing).
-- Example:
--
-- @
-- potentially $ do
-- s <- getLiteralString cmd
-- guard $ s `elem` ["--recursive", "-r"]
-- return $ warn .. "Something something recursive"
-- @
potentially :: Monad m => Maybe (m ()) -> m ()
potentially = fromMaybe (return ())
@@ -812,10 +869,9 @@ filterByAnnotation asSpec params =
filter (not . shouldIgnore)
where
token = asScript asSpec
idFor (TokenComment id _) = id
shouldIgnore note =
any (shouldIgnoreFor (getCode note)) $
getPath parents (T_Bang $ idFor note)
getPath parents (T_Bang $ tcId note)
shouldIgnoreFor num (T_Annotation _ anns _) =
any hasNum anns
where
@@ -824,7 +880,7 @@ filterByAnnotation asSpec params =
shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec
shouldIgnoreFor _ _ = False
parents = parentMap params
getCode (TokenComment _ (Comment _ c _)) = c
getCode = cCode . tcComment
-- Is this a ${#anything}, to get string length or array count?
isCountingReference (T_DollarBraced id token) =
@@ -874,6 +930,3 @@ getOpts flagTokenizer string cmd = process flags
else do
more <- process rest2
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
module ShellCheck.Checker (checkScript) where
import ShellCheck.Interface
import ShellCheck.Parser
@@ -35,58 +34,75 @@ import qualified System.IO
import Prelude hiding (readFile)
import Control.Monad
import Test.QuickCheck.All
tokenToPosition map (TokenComment id c) = fromMaybe fail $ do
position <- Map.lookup id map
return $ PositionedComment position position c
tokenToPosition startMap t = fromMaybe fail $ do
span <- Map.lookup (tcId t) startMap
return $ newPositionedComment {
pcStartPos = fst span,
pcEndPos = snd span,
pcComment = tcComment t,
pcFix = tcFix t
}
where
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
checkScript sys spec = do
results <- checkScript (csScript spec)
return CheckResult {
return emptyCheckResult {
crFilename = csFilename spec,
crComments = results
}
where
checkScript contents = do
result <- parseScript sys ParseSpec {
result <- parseScript sys newParseSpec {
psFilename = csFilename spec,
psScript = contents,
psCheckSourced = csCheckSourced spec
psCheckSourced = csCheckSourced spec,
psShellTypeOverride = csShellTypeOverride spec
}
let parseMessages = prComments result
let 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 =
fromMaybe [] $
(arComments . analyzeScript . analysisSpec)
<$> prRoot result
let translator = tokenToPosition (prTokenPositions result)
let translator = tokenToPosition tokenPositions
return . nub . sortMessages . filter shouldInclude $
(parseMessages ++ map translator analysisMessages)
shouldInclude (PositionedComment _ _ (Comment _ code _)) =
code `notElem` csExcludedWarnings spec
shouldInclude pc =
let code = cCode (pcComment pc)
severity = cSeverity (pcComment pc)
in
code `notElem` csExcludedWarnings spec &&
severity <= csMinSeverity spec
sortMessages = sortBy (comparing order)
order (PositionedComment pos _ (Comment severity code message)) =
(posFile pos, posLine pos, posColumn pos, severity, code, message)
getPosition (PositionedComment pos _ _) = pos
order pc =
let pos = pcStartPos pc
comment = pcComment pc in
(posFile pos,
posLine pos,
posColumn pos,
cSeverity comment,
cCode comment,
cMessage comment)
getPosition = pcStartPos
analysisSpec root =
AnalysisSpec {
asScript = root,
asShellType = csShellTypeOverride spec,
asCheckSourced = csCheckSourced spec,
asExecutionMode = Executed
}
getErrors sys spec =
sort . map getCode . crComments $
runIdentity (checkScript sys spec)
where
getCode (PositionedComment _ _ (Comment _ code _)) = code
getCode = cCode . pcComment
check = checkWithIncludes []
@@ -106,97 +122,132 @@ checkRecursive includes src =
csCheckSourced = True
}
prop_findsParseIssue = check "echo \"$12\"" == [1037]
prop_commentDisablesParseIssue1 =
null $ check "#shellcheck disable=SC1037\necho \"$12\""
prop_commentDisablesParseIssue2 =
null $ check "#shellcheck disable=SC1037\n#lol\necho \"$12\""
prop_findsAnalysisIssue =
check "echo $1" == [2086]
prop_commentDisablesAnalysisIssue1 =
null $ check "#shellcheck disable=SC2086\necho $1"
prop_commentDisablesAnalysisIssue2 =
null $ check "#shellcheck disable=SC2086\n#lol\necho $1"
prop_optionDisablesIssue1 =
null $ getErrors
(mockedSystemInterface [])
emptyCheckSpec {
csScript = "echo $1",
csExcludedWarnings = [2148, 2086]
}
prop_optionDisablesIssue2 =
null $ getErrors
(mockedSystemInterface [])
emptyCheckSpec {
csScript = "echo \"$10\"",
csExcludedWarnings = [2148, 1037]
}
prop_canParseDevNull =
[] == check "source /dev/null"
prop_failsWhenNotSourcing =
[1091, 2154] == check "source lol; echo \"$bar\""
prop_worksWhenSourcing =
null $ checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\""
prop_worksWhenDotting =
null $ checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\""
prop_noInfiniteSourcing =
[] == checkWithIncludes [("lib", "source lib")] "source lib"
prop_canSourceBadSyntax =
[1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
prop_cantSourceDynamic =
[1090] == checkWithIncludes [("lib", "")] ". \"$1\""
prop_cantSourceDynamic2 =
[1090] == checkWithIncludes [("lib", "")] "source ~/foo"
prop_canSourceDynamicWhenRedirected =
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
prop_recursiveAnalysis =
[2086] == checkRecursive [("lib", "echo $1")] "source lib"
prop_recursiveParsing =
[1037] == checkRecursive [("lib", "echo \"$10\"")] "source lib"
prop_sourceDirectiveDoesntFollowFile =
null $ checkWithIncludes
[("foo", "source bar"), ("bar", "baz=3")]
"#shellcheck source=foo\n. \"$1\"; echo \"$baz\""
prop_filewideAnnotationBase = [2086] == check "#!/bin/sh\necho $1"
prop_filewideAnnotation1 = null $
check "#!/bin/sh\n# shellcheck disable=2086\necho $1"
prop_filewideAnnotation2 = null $
check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation3 = null $
check "#!/bin/sh\n#unerlated\n# shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation4 = null $
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
prop_filewideAnnotation5 = null $
check "#!/bin/sh\n\n\n\n#shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation6 = null $
check "#shellcheck shell=sh\n#unrelated\n#shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation7 = null $
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
prop_filewideAnnotationBase2 = [2086, 2181] == check "true\n[ $? == 0 ] && echo $1"
prop_filewideAnnotation8 = null $
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
return []
runTests = $quickCheckAll
-- | Dummy binding for doctest to run
--
-- >>> check "echo \"$12\""
-- [1037]
--
-- >>> check "#shellcheck disable=SC1037\necho \"$12\""
-- []
--
-- >>> check "#shellcheck disable=SC1037\n#lol\necho \"$12\""
-- []
--
-- >>> check "echo $1"
-- [2086]
--
-- >>> check "#shellcheck disable=SC2086\necho $1"
-- []
--
-- >>> check "#shellcheck disable=SC2086\n#lol\necho $1"
-- []
--
-- >>> :{
-- getErrors
-- (mockedSystemInterface [])
-- emptyCheckSpec {
-- csScript = "echo $1",
-- csExcludedWarnings = [2148, 2086]
-- }
-- :}
-- []
--
-- >>> :{
-- getErrors
-- (mockedSystemInterface [])
-- emptyCheckSpec {
-- csScript = "echo \"$10\"",
-- csExcludedWarnings = [2148, 1037]
-- }
-- :}
-- []
--
-- >>> check "#!/usr/bin/python\ntrue $1\n"
-- [1071]
--
-- >>> :{
-- getErrors
-- (mockedSystemInterface [])
-- emptyCheckSpec {
-- csScript = "#!/usr/bin/python\ntrue\n",
-- csShellTypeOverride = Just Sh
-- }
-- :}
-- []
--
-- >>> check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
-- []
--
-- >>> check "source /dev/null"
-- []
--
-- >>> check "source lol; echo \"$bar\""
-- [1091,2154]
--
-- >>> checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\""
-- []
--
-- >>> checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\""
-- []
--
-- >>> checkWithIncludes [("lib", "source lib")] "source lib"
-- []
--
-- >>> checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
-- [1094,2086]
--
-- >>> checkWithIncludes [("lib", "")] ". \"$1\""
-- [1090]
--
-- >>> checkWithIncludes [("lib", "")] "source ~/foo"
-- [1090]
--
-- >>> checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
-- []
--
-- >>> checkRecursive [("lib", "echo $1")] "source lib"
-- [2086]
--
-- >>> checkRecursive [("lib", "echo \"$10\"")] "source lib"
-- [1037]
--
-- >>> checkWithIncludes [("foo", "source bar"), ("bar", "baz=3")] "#shellcheck source=foo\n. \"$1\"; echo \"$baz\""
-- []
--
-- >>> check "#!/bin/sh\necho $1"
-- [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
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
-- This module contains checks that examine specific commands by name.
module ShellCheck.Checks.Commands (checker , ShellCheck.Checks.Commands.runTests) where
module ShellCheck.Checks.Commands (checker) where
import ShellCheck.AST
import ShellCheck.ASTLib
@@ -37,8 +35,6 @@ import Data.Char
import Data.List
import Data.Maybe
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
deriving (Eq, Ord)
@@ -46,7 +42,6 @@ data CommandName = Exactly String | Basename String
data CommandCheck =
CommandCheck CommandName (Token -> Analysis)
verify :: CommandCheck -> String -> Bool
verify f s = producesComments (getChecker [f]) s == Just True
verifyNot f s = producesComments (getChecker [f]) s == Just False
@@ -61,6 +56,7 @@ commandChecks = [
,checkGrepRe
,checkTrapQuotes
,checkReturn
,checkExit
,checkFindExecWithSingleArgument
,checkUnusedEchoEscapes
,checkInjectableFindSh
@@ -92,6 +88,7 @@ commandChecks = [
,checkWhich
,checkSudoRedirect
,checkSudoArgs
,checkSourceArgs
]
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
@@ -128,19 +125,21 @@ getChecker list = Checker {
checker :: Parameters -> Checker
checker params = getChecker commandChecks
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'"
prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'"
prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'"
prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'"
prop_checkTr4 = verifyNot checkTr "ls [a-z]"
prop_checkTr5 = verify checkTr "tr foo bar"
prop_checkTr6 = verify checkTr "tr 'hello' 'world'"
prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
-- |
-- >>> prop $ verify checkTr "tr [a-f] [A-F]"
-- >>> prop $ verify checkTr "tr 'a-z' 'A-Z'"
-- >>> prop $ verify checkTr "tr '[a-z]' '[A-Z]'"
-- >>> prop $ verifyNot checkTr "tr -d '[:lower:]'"
-- >>> prop $ verifyNot checkTr "tr -d '[:upper:]'"
-- >>> prop $ verifyNot checkTr "tr -d '|/_[:upper:]'"
-- >>> prop $ verifyNot checkTr "ls [a-z]"
-- >>> prop $ verify checkTr "tr foo bar"
-- >>> prop $ verify checkTr "tr 'hello' 'world'"
-- >>> prop $ verifyNot checkTr "tr aeiou _____"
-- >>> prop $ verifyNot checkTr "a-z n-za-m"
-- >>> 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)
where
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?
when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $
info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)."
unless ("[:" `isPrefixOf` s) $
unless ("[:" `isPrefixOf` s || "[=" `isPrefixOf` s) $
when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $
info (getId word) 2021 "Don't use [] around classes in tr, it replaces literal square brackets."
Nothing -> return ()
@@ -161,9 +160,10 @@ checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
let relevant = filter isAlpha s
in relevant /= nub relevant
prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php"
prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)"
prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"
-- |
-- >>> prop $ verify 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
acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
acceptsGlob _ = False
@@ -176,35 +176,37 @@ checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
f (b:r)
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
prop_checkNeedlessExpr2 = verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``"
prop_checkNeedlessExpr3 = verifyNot checkNeedlessExpr "foo=$(expr foo : regex)"
prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)"
-- |
-- >>> prop $ verify checkNeedlessExpr "foo=$(expr 3 + 2)"
-- >>> prop $ verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``"
-- >>> prop $ verifyNot checkNeedlessExpr "foo=$(expr foo : regex)"
-- >>> prop $ verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)"
checkNeedlessExpr = CommandCheck (Basename "expr") f where
f t =
when (all (`notElem` exceptions) (words $ arguments t)) $
style (getId t) 2003
style (getId $ getCommandTokenOrThis t) 2003
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
-- These operators are hard to replicate in POSIX
exceptions = [ ":", "<", ">", "<=", ">=" ]
words = mapMaybe getLiteralString
prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3"
prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3"
prop_checkGrepRe3 = verify checkGrepRe "grep --regex=*.mp3 file"
prop_checkGrepRe4 = verifyNot checkGrepRe "grep foo *.mp3"
prop_checkGrepRe5 = verifyNot checkGrepRe "grep-v --regex=moo *"
prop_checkGrepRe6 = verifyNot checkGrepRe "grep foo \\*.mp3"
prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file"
prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg"
prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"
prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file"
prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo"
prop_checkGrepRe12= verifyNot checkGrepRe "grep -F 'Foo*' file"
prop_checkGrepRe13= verifyNot checkGrepRe "grep -- -foo bar*"
prop_checkGrepRe14= verifyNot checkGrepRe "grep -e -foo bar*"
prop_checkGrepRe15= verifyNot checkGrepRe "grep --regex -foo bar*"
-- |
-- >>> prop $ verify checkGrepRe "cat foo | grep *.mp3"
-- >>> prop $ verify checkGrepRe "grep -Ev cow*test *.mp3"
-- >>> prop $ verify checkGrepRe "grep --regex=*.mp3 file"
-- >>> prop $ verifyNot checkGrepRe "grep foo *.mp3"
-- >>> prop $ verifyNot checkGrepRe "grep-v --regex=moo *"
-- >>> prop $ verifyNot checkGrepRe "grep foo \\*.mp3"
-- >>> prop $ verify checkGrepRe "grep *foo* file"
-- >>> prop $ verify checkGrepRe "ls | grep foo*.jpg"
-- >>> prop $ verifyNot checkGrepRe "grep '[0-9]*' file"
-- >>> prop $ verifyNot checkGrepRe "grep '^aa*' file"
-- >>> prop $ verifyNot checkGrepRe "grep --include=*.png foo"
-- >>> prop $ verifyNot checkGrepRe "grep -F 'Foo*' file"
-- >>> prop $ verifyNot checkGrepRe "grep -- -foo bar*"
-- >>> prop $ verifyNot checkGrepRe "grep -e -foo bar*"
-- >>> prop $ verifyNot checkGrepRe "grep --regex -foo bar*"
checkGrepRe = CommandCheck (Basename "grep") check where
check cmd = f cmd (arguments cmd)
@@ -255,10 +257,11 @@ checkGrepRe = CommandCheck (Basename "grep") check where
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT"
prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT"
prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
-- |
-- >>> prop $ verify checkTrapQuotes "trap \"echo $num\" INT"
-- >>> prop $ verify checkTrapQuotes "trap \"echo `ls`\" INT"
-- >>> prop $ verifyNot checkTrapQuotes "trap 'echo $num' INT"
-- >>> prop $ verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
f (x:_) = checkTrap x
f _ = return ()
@@ -272,22 +275,37 @@ checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
checkExpansions _ = return ()
prop_checkReturn1 = verifyNot checkReturn "return"
prop_checkReturn2 = verifyNot checkReturn "return 1"
prop_checkReturn3 = verifyNot checkReturn "return $var"
prop_checkReturn4 = verifyNot checkReturn "return $((a|b))"
prop_checkReturn5 = verify checkReturn "return -1"
prop_checkReturn6 = verify checkReturn "return 1000"
prop_checkReturn7 = verify checkReturn "return 'hello world'"
checkReturn = CommandCheck (Exactly "return") (f . arguments)
-- |
-- >>> prop $ verifyNot checkReturn "return"
-- >>> prop $ verifyNot checkReturn "return 1"
-- >>> prop $ verifyNot checkReturn "return $var"
-- >>> prop $ verifyNot checkReturn "return $((a|b))"
-- >>> prop $ verify checkReturn "return -1"
-- >>> prop $ verify checkReturn "return 1000"
-- >>> 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
f (first:second:_) =
err (getId second) 2151
"Only one integer 0-255 can be returned. Use stdout for other data."
multi (getId first)
f [value] =
when (isInvalid $ literal value) $
err (getId value) 2152
"Can only return 0-255. Other data should be written to stdout."
invalid (getId value)
f _ = return ()
isInvalid s = s == "" || any (not . isDigit) s || length s > 5
@@ -301,9 +319,10 @@ checkReturn = CommandCheck (Exactly "return") (f . arguments)
lit _ = return "WTF"
prop_checkFindExecWithSingleArgument1 = verify checkFindExecWithSingleArgument "find . -exec 'cat {} | wc -l' \\;"
prop_checkFindExecWithSingleArgument2 = verify checkFindExecWithSingleArgument "find . -execdir 'cat {} | wc -l' +"
prop_checkFindExecWithSingleArgument3 = verifyNot checkFindExecWithSingleArgument "find . -exec wc -l {} \\;"
-- |
-- >>> prop $ verify checkFindExecWithSingleArgument "find . -exec 'cat {} | wc -l' \\;"
-- >>> prop $ verify checkFindExecWithSingleArgument "find . -execdir 'cat {} | wc -l' +"
-- >>> prop $ verifyNot checkFindExecWithSingleArgument "find . -exec wc -l {} \\;"
checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments)
where
f = void . sequence . mapMaybe check . tails
@@ -319,36 +338,30 @@ checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments)
commandRegex = mkRegex "[ |;]"
prop_checkUnusedEchoEscapes1 = verify checkUnusedEchoEscapes "echo 'foo\\nbar\\n'"
prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'"
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
checkUnusedEchoEscapes = CommandCheck (Basename "echo") (f . arguments)
-- |
-- >>> prop $ verify checkUnusedEchoEscapes "echo 'foo\\nbar\\n'"
-- >>> prop $ verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'"
-- >>> prop $ verify checkUnusedEchoEscapes "echo \"n:\\t42\""
-- >>> prop $ verifyNot checkUnusedEchoEscapes "echo lol"
-- >>> prop $ verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
checkUnusedEchoEscapes = CommandCheck (Basename "echo") f
where
isDashE = mkRegex "^-.*e"
hasEscapes = mkRegex "\\\\[rnt]"
f args | concat (concatMap oversimplify allButLast) `matches` isDashE =
return ()
where allButLast = reverse . drop 1 . reverse $ args
f args = mapM_ checkEscapes args
f cmd =
whenShell [Sh, Bash, Ksh] $
unless (cmd `hasFlag` "e") $
mapM_ examine $ arguments cmd
checkEscapes (T_NormalWord _ args) =
mapM_ checkEscapes args
checkEscapes (T_DoubleQuoted id args) =
mapM_ checkEscapes args
checkEscapes (T_Literal id str) = examine id str
checkEscapes (T_SingleQuoted id str) = examine id str
checkEscapes _ = return ()
examine id str =
examine token = do
let str = onlyLiteralString token
when (str `matches` hasEscapes) $
info id 2028 "echo won't expand escape sequences. Consider printf."
info (getId token) 2028 "echo may not expand escape sequences. Use printf."
prop_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
prop_checkInjectableFindSh2 = verify checkInjectableFindSh "find . -execdir bash -c 'rm \"{}\"' ';'"
prop_checkInjectableFindSh3 = verifyNot checkInjectableFindSh "find . -exec sh -c 'rm \"$@\"' _ {} \\;"
-- |
-- >>> prop $ verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
-- >>> prop $ verify checkInjectableFindSh "find . -execdir bash -c 'rm \"{}\"' ';'"
-- >>> prop $ verifyNot checkInjectableFindSh "find . -exec sh -c 'rm \"$@\"' _ {} \\;"
checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments)
where
check args = do
@@ -371,9 +384,10 @@ checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments)
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_checkFindActionPrecedence3 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au'"
-- |
-- >>> prop $ verify checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au' -exec rm {} +"
-- >>> 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)
where
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."
prop_checkMkdirDashPM0 = verify checkMkdirDashPM "mkdir -p -m 0755 a/b"
prop_checkMkdirDashPM1 = verify checkMkdirDashPM "mkdir -pm 0755 $dir"
prop_checkMkdirDashPM2 = verify checkMkdirDashPM "mkdir -vpm 0755 a/b"
prop_checkMkdirDashPM3 = verify checkMkdirDashPM "mkdir -pm 0755 -v a/b"
prop_checkMkdirDashPM4 = verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b"
prop_checkMkdirDashPM5 = verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b"
prop_checkMkdirDashPM6 = verify checkMkdirDashPM "mkdir -p --mode=0755 a/b"
prop_checkMkdirDashPM7 = verify checkMkdirDashPM "mkdir --parents -m 0755 a/b"
prop_checkMkdirDashPM8 = verifyNot checkMkdirDashPM "mkdir -p a/b"
prop_checkMkdirDashPM9 = verifyNot checkMkdirDashPM "mkdir -m 0755 a/b"
prop_checkMkdirDashPM10 = verifyNot checkMkdirDashPM "mkdir a/b"
prop_checkMkdirDashPM11 = verifyNot checkMkdirDashPM "mkdir --parents a/b"
prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b"
prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b"
prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"
prop_checkMkdirDashPM15 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../bin"
prop_checkMkdirDashPM16 = verify checkMkdirDashPM "mkdir -p -m 0755 ../bin/laden"
prop_checkMkdirDashPM17 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./bin"
prop_checkMkdirDashPM18 = verify checkMkdirDashPM "mkdir -p -m 0755 ./bin/laden"
prop_checkMkdirDashPM19 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./../bin"
prop_checkMkdirDashPM20 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 .././bin"
prop_checkMkdirDashPM21 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../../bin"
-- |
-- >>> prop $ verify checkMkdirDashPM "mkdir -p -m 0755 a/b"
-- >>> prop $ verify checkMkdirDashPM "mkdir -pm 0755 $dir"
-- >>> prop $ verify checkMkdirDashPM "mkdir -vpm 0755 a/b"
-- >>> prop $ verify checkMkdirDashPM "mkdir -pm 0755 -v a/b"
-- >>> prop $ verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b"
-- >>> prop $ verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b"
-- >>> prop $ verify checkMkdirDashPM "mkdir -p --mode=0755 a/b"
-- >>> prop $ verify checkMkdirDashPM "mkdir --parents -m 0755 a/b"
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p a/b"
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir -m 0755 a/b"
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir a/b"
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir --parents a/b"
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b"
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b"
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../bin"
-- >>> prop $ verify checkMkdirDashPM "mkdir -p -m 0755 ../bin/laden"
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./bin"
-- >>> prop $ verify checkMkdirDashPM "mkdir -p -m 0755 ./bin/laden"
-- >>> prop $ 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
where
check t = potentially $ do
@@ -427,13 +442,14 @@ checkMkdirDashPM = CommandCheck (Basename "mkdir") check
re = mkRegex "^(\\.\\.?\\/)+[^/]+$"
prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8"
prop_checkNonportableSignals2 = verifyNot checkNonportableSignals "trap f 0"
prop_checkNonportableSignals3 = verifyNot checkNonportableSignals "trap f 14"
prop_checkNonportableSignals4 = verify checkNonportableSignals "trap f SIGKILL"
prop_checkNonportableSignals5 = verify checkNonportableSignals "trap f 9"
prop_checkNonportableSignals6 = verify checkNonportableSignals "trap f stop"
prop_checkNonportableSignals7 = verifyNot checkNonportableSignals "trap 'stop' int"
-- |
-- >>> prop $ verify checkNonportableSignals "trap f 8"
-- >>> prop $ verifyNot checkNonportableSignals "trap f 0"
-- >>> prop $ verifyNot checkNonportableSignals "trap f 14"
-- >>> prop $ verify checkNonportableSignals "trap f SIGKILL"
-- >>> prop $ verify checkNonportableSignals "trap f 9"
-- >>> prop $ verify checkNonportableSignals "trap f stop"
-- >>> prop $ verifyNot checkNonportableSignals "trap 'stop' int"
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
where
f args = case args of
@@ -462,10 +478,11 @@ checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
"SIGKILL/SIGSTOP can not be trapped."
prop_checkInteractiveSu1 = verify checkInteractiveSu "su; rm file; su $USER"
prop_checkInteractiveSu2 = verify checkInteractiveSu "su foo; something; exit"
prop_checkInteractiveSu3 = verifyNot checkInteractiveSu "echo rm | su foo"
prop_checkInteractiveSu4 = verifyNot checkInteractiveSu "su root < script"
-- |
-- >>> prop $ verify checkInteractiveSu "su; rm file; su $USER"
-- >>> prop $ verify checkInteractiveSu "su foo; something; exit"
-- >>> prop $ verifyNot checkInteractiveSu "echo rm | su foo"
-- >>> prop $ verifyNot checkInteractiveSu "su root < script"
checkInteractiveSu = CommandCheck (Basename "su") f
where
f cmd = when (length (arguments cmd) <= 1) $ do
@@ -480,11 +497,13 @@ checkInteractiveSu = CommandCheck (Basename "su") f
undirected _ = True
-- |
-- 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_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""
prop_checkSshCmdStr4 = verifyNot checkSshCommandString "ssh -i key \"$host\""
--
-- >>> prop $ verify checkSshCommandString "ssh host \"echo $PS1\""
-- >>> prop $ verifyNot checkSshCommandString "ssh host \"ls foo\""
-- >>> prop $ verifyNot checkSshCommandString "ssh \"$host\""
-- >>> prop $ verifyNot checkSshCommandString "ssh -i key \"$host\""
checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
where
isOption x = "-" `isPrefixOf` (concat $ oversimplify x)
@@ -500,21 +519,25 @@ checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
checkArg _ = return ()
prop_checkPrintfVar1 = verify checkPrintfVar "printf \"Lol: $s\""
prop_checkPrintfVar2 = verifyNot checkPrintfVar "printf 'Lol: $s'"
prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)"
prop_checkPrintfVar4 = verifyNot checkPrintfVar "printf \"%${count}s\" var"
prop_checkPrintfVar5 = verify checkPrintfVar "printf '%s %s %s' foo bar"
prop_checkPrintfVar6 = verify checkPrintfVar "printf foo bar baz"
prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz"
prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\""
prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
prop_checkPrintfVar11= verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
prop_checkPrintfVar12= verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1"
prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
-- |
-- >>> prop $ verify checkPrintfVar "printf \"Lol: $s\""
-- >>> prop $ verifyNot checkPrintfVar "printf 'Lol: $s'"
-- >>> prop $ verify checkPrintfVar "printf -v cow $(cmd)"
-- >>> prop $ verifyNot checkPrintfVar "printf \"%${count}s\" var"
-- >>> prop $ verify checkPrintfVar "printf '%s %s %s' foo bar"
-- >>> prop $ verify checkPrintfVar "printf foo bar baz"
-- >>> prop $ verify checkPrintfVar "printf -- foo bar baz"
-- >>> prop $ verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\""
-- >>> prop $ verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
-- >>> prop $ verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
-- >>> prop $ verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
-- >>> prop $ verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
-- >>> prop $ verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
-- >>> 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
f (doubledash:rest) | getLiteralString doubledash == Just "--" = 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
'%':'%':rest -> countFormats rest
'%':'(':rest -> 1 + countFormats (dropWhile (/= ')') rest)
'%':'*':rest -> 2 + countFormats rest -- width is specified as an argument
'%':rest -> 1 + countFormats rest
'%':rest -> regexBasedCountFormats rest + countFormats (dropWhile (/= '%') rest)
_:rest -> countFormats rest
[] -> 0
regexBasedCountFormats rest =
maybe 1 (foldl (\acc group -> acc + (if group == "*" then 1 else 0)) 1) (matchRegex re rest)
where
-- constructed based on specifications in "man printf"
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*).?(\\d*|\\*)[diouxXfFeEgGaAcsb]"
-- \____ _____/\___ ____/ \____ ____/\________ ________/
-- V V V V
-- flags field width precision format character
-- field width and precision can be specified with a '*' instead of a digit,
-- in which case printf will accept one more argument for each '*' used
check format more = do
fromMaybe (return ()) $ do
string <- getLiteralString format
@@ -554,24 +586,26 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)"
prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`"
prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\""
prop_checkUuoeCmd4 = verify checkUuoeCmd "echo \"`date`\""
prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\""
prop_checkUuoeCmd6 = verifyNot checkUuoeCmd "echo \"$(<file)\""
-- |
-- >>> prop $ verify checkUuoeCmd "echo $(date)"
-- >>> prop $ verify checkUuoeCmd "echo `date`"
-- >>> prop $ verify checkUuoeCmd "echo \"$(date)\""
-- >>> prop $ verify checkUuoeCmd "echo \"`date`\""
-- >>> prop $ verifyNot checkUuoeCmd "echo \"The time is $(date)\""
-- >>> prop $ verifyNot checkUuoeCmd "echo \"$(<file)\""
checkUuoeCmd = CommandCheck (Exactly "echo") (f . arguments) where
msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'."
f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token)
f _ = return ()
prop_checkSetAssignment1 = verify checkSetAssignment "set foo 42"
prop_checkSetAssignment2 = verify checkSetAssignment "set foo = 42"
prop_checkSetAssignment3 = verify checkSetAssignment "set foo=42"
prop_checkSetAssignment4 = verifyNot checkSetAssignment "set -- if=/dev/null"
prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'"
prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"
-- |
-- >>> prop $ verify checkSetAssignment "set foo 42"
-- >>> prop $ verify checkSetAssignment "set foo = 42"
-- >>> prop $ verify checkSetAssignment "set foo=42"
-- >>> prop $ verifyNot checkSetAssignment "set -- if=/dev/null"
-- >>> prop $ verifyNot checkSetAssignment "set 'a=5'"
-- >>> prop $ verifyNot checkSetAssignment "set"
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
where
f (var:value:rest) =
@@ -591,10 +625,11 @@ checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
literal _ = "*"
prop_checkExportedExpansions1 = verify checkExportedExpansions "export $foo"
prop_checkExportedExpansions2 = verify checkExportedExpansions "export \"$foo\""
prop_checkExportedExpansions3 = verifyNot checkExportedExpansions "export foo"
prop_checkExportedExpansions4 = verifyNot checkExportedExpansions "export ${foo?}"
-- |
-- >>> prop $ verify checkExportedExpansions "export $foo"
-- >>> prop $ verify checkExportedExpansions "export \"$foo\""
-- >>> prop $ verifyNot checkExportedExpansions "export foo"
-- >>> prop $ verifyNot checkExportedExpansions "export ${foo?}"
checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
where
check t = potentially $ do
@@ -603,14 +638,15 @@ checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . argumen
return . warn (getId t) 2163 $
"This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
prop_checkReadExpansions1 = verify checkReadExpansions "read $var"
prop_checkReadExpansions2 = verify checkReadExpansions "read -r $var"
prop_checkReadExpansions3 = verifyNot checkReadExpansions "read -p $var"
prop_checkReadExpansions4 = verifyNot checkReadExpansions "read -rd $delim name"
prop_checkReadExpansions5 = verify checkReadExpansions "read \"$var\""
prop_checkReadExpansions6 = verify checkReadExpansions "read -a $var"
prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1"
prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}"
-- |
-- >>> prop $ verify checkReadExpansions "read $var"
-- >>> prop $ verify checkReadExpansions "read -r $var"
-- >>> prop $ verifyNot checkReadExpansions "read -p $var"
-- >>> prop $ verifyNot checkReadExpansions "read -rd $delim name"
-- >>> prop $ verify checkReadExpansions "read \"$var\""
-- >>> prop $ verify checkReadExpansions "read -a $var"
-- >>> prop $ verifyNot checkReadExpansions "read $1"
-- >>> prop $ verifyNot checkReadExpansions "read ${var?}"
checkReadExpansions = CommandCheck (Exactly "read") check
where
options = getGnuOpts "sreu:n:N:i:p:a:"
@@ -637,9 +673,10 @@ getSingleUnmodifiedVariable word =
in guard (contents == name) >> return t
_ -> Nothing
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'"
prop_checkAliasesUsesArgs3 = verify checkAliasesUsesArgs "alias a=\"echo \\${@}\""
-- |
-- >>> prop $ verify checkAliasesUsesArgs "alias a='cp $1 /a'"
-- >>> prop $ verifyNot checkAliasesUsesArgs "alias $1='foo'"
-- >>> prop $ verify checkAliasesUsesArgs "alias a=\"echo \\${@}\""
checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments)
where
re = mkRegex "\\$\\{?[0-9*@]"
@@ -651,9 +688,10 @@ checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments)
"Aliases can't use positional parameters. Use a function."
prop_checkAliasesExpandEarly1 = verify checkAliasesExpandEarly "alias foo=\"echo $PWD\""
prop_checkAliasesExpandEarly2 = verifyNot checkAliasesExpandEarly "alias -p"
prop_checkAliasesExpandEarly3 = verifyNot checkAliasesExpandEarly "alias foo='echo {1..10}'"
-- |
-- >>> prop $ verify checkAliasesExpandEarly "alias foo=\"echo $PWD\""
-- >>> prop $ verifyNot checkAliasesExpandEarly "alias -p"
-- >>> prop $ verifyNot checkAliasesExpandEarly "alias foo='echo {1..10}'"
checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
where
f = mapM_ checkArg
@@ -663,8 +701,8 @@ checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
checkArg _ = return ()
prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]"
prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo"
-- >>> prop $ verify checkUnsetGlobs "unset foo[1]"
-- >>> prop $ verifyNot checkUnsetGlobs "unset foo"
checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)
where
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."
prop_checkFindWithoutPath1 = verify checkFindWithoutPath "find -type f"
prop_checkFindWithoutPath2 = verify checkFindWithoutPath "find"
prop_checkFindWithoutPath3 = verifyNot checkFindWithoutPath "find . -type f"
prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"
prop_checkFindWithoutPath5 = verifyNot checkFindWithoutPath "find -O3 ."
prop_checkFindWithoutPath6 = verifyNot checkFindWithoutPath "find -D exec ."
-- |
-- >>> prop $ verify checkFindWithoutPath "find -type f"
-- >>> prop $ verify checkFindWithoutPath "find"
-- >>> prop $ verifyNot checkFindWithoutPath "find . -type f"
-- >>> prop $ verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"
-- >>> 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
where
f (T_SimpleCommand _ _ (cmd:args)) =
unless (hasPath args) $
f t@(T_SimpleCommand _ _ (cmd:args)) =
unless (t `hasFlag` "help" || hasPath args) $
info (getId cmd) 2185 "Some finds don't have a default path. Specify '.' explicitly."
-- This is a bit of a kludge. find supports flag arguments both before and after the path,
-- as well as multiple non-flag arguments that are not the path. We assume that all the
-- pre-path flags are single characters, which is generally the case except for -O3.
-- This is a bit of a kludge. find supports flag arguments both before and
-- after the path, as well as multiple non-flag arguments that are not the
-- path. We assume that all the pre-path flags are single characters from a
-- list of GNU and macOS flags.
hasPath (first:rest) =
let flag = fromJust $ getLiteralStringExt (const $ return "___") first in
not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest
hasPath [] = False
isLeadingFlag flag = length flag <= 2 || "-O" `isPrefixOf` flag
isLeadingFlag flag = length flag <= 2 || all (`elem` leadingFlagChars) flag
leadingFlagChars="-EHLPXdfsxO0123456789"
prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10"
prop_checkTimeParameters2 = verifyNot checkTimeParameters "time sleep 10"
prop_checkTimeParameters3 = verifyNot checkTimeParameters "time -p foo"
prop_checkTimeParameters4 = verifyNot checkTimeParameters "command time -f lol sleep 10"
-- |
-- >>> prop $ verify checkTimeParameters "time -f lol sleep 10"
-- >>> prop $ verifyNot checkTimeParameters "time sleep 10"
-- >>> prop $ verifyNot checkTimeParameters "time -p foo"
-- >>> prop $ verifyNot checkTimeParameters "command time -f lol sleep 10"
checkTimeParameters = CommandCheck (Exactly "time") f
where
f (T_SimpleCommand _ _ (cmd:args:_)) =
@@ -708,9 +752,10 @@ checkTimeParameters = CommandCheck (Exactly "time") f
f _ = return ()
prop_checkTimedCommand1 = verify checkTimedCommand "#!/bin/sh\ntime -p foo | bar"
prop_checkTimedCommand2 = verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar; )"
prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1"
-- |
-- >>> prop $ verify checkTimedCommand "#!/bin/sh\ntime -p foo | bar"
-- >>> prop $ verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar; )"
-- >>> prop $ verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1"
checkTimedCommand = CommandCheck (Exactly "time") f where
f (T_SimpleCommand _ _ (c:args@(_:_))) =
whenShell [Sh, Dash] $ do
@@ -734,32 +779,37 @@ checkTimedCommand = CommandCheck (Exactly "time") f where
T_SimpleCommand {} -> return True
_ -> 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 ->
whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local
path <- getPathM t
unless (any isFunction path) $
err (getId t) 2168 "'local' is only valid in functions."
err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
-- |
-- >>> prop $ verify checkDeprecatedTempfile "var=$(tempfile)"
-- >>> prop $ verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
\t -> warn (getId t) 2186 "tempfile is deprecated. Use mktemp instead."
\t -> warn (getId $ getCommandTokenOrThis t) 2186 "tempfile is deprecated. Use mktemp instead."
prop_checkDeprecatedEgrep = verify checkDeprecatedEgrep "egrep '.+'"
-- |
-- >>> prop $ verify checkDeprecatedEgrep "egrep '.+'"
checkDeprecatedEgrep = CommandCheck (Basename "egrep") $
\t -> info (getId t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."
\t -> info (getId $ getCommandTokenOrThis t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."
prop_checkDeprecatedFgrep = verify checkDeprecatedFgrep "fgrep '*' files"
-- |
-- >>> prop $ verify checkDeprecatedFgrep "fgrep '*' files"
checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $
\t -> info (getId t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
\t -> info (getId $ getCommandTokenOrThis t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
prop_checkWhileGetoptsCase1 = verify checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; esac; done"
prop_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; b) bar;; esac; done"
prop_checkWhileGetoptsCase3 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; b) bar;; *) :;esac; done"
prop_checkWhileGetoptsCase4 = verifyNot checkWhileGetoptsCase "while getopts 'a:123' x; do case $x in a) foo;; [0-9]) bar;; esac; done"
prop_checkWhileGetoptsCase5 = verifyNot checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; \\?) bar;; *) baz;; esac; done"
-- |
-- >>> prop $ verify checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; esac; done"
-- >>> prop $ verify checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; b) bar;; esac; done"
-- >>> prop $ verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; b) bar;; *) :;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
where
f :: Token -> Analysis
@@ -824,19 +874,20 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
T_Redirecting _ _ x@(T_CaseExpression {}) -> return x
_ -> Nothing
prop_checkCatastrophicRm1 = verify checkCatastrophicRm "rm -r $1/$2"
prop_checkCatastrophicRm2 = verify checkCatastrophicRm "rm -r /home/$foo"
prop_checkCatastrophicRm3 = verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*"
prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*"
prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"
prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*"
prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home"
prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"
prop_checkCatastrophicRm13= verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"
prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"
prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"
-- |
-- >>> prop $ verify checkCatastrophicRm "rm -r $1/$2"
-- >>> prop $ verify checkCatastrophicRm "rm -r /home/$foo"
-- >>> prop $ verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*"
-- >>> prop $ verify checkCatastrophicRm "rm -fr /home/$(whoami)/*"
-- >>> prop $ verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"
-- >>> prop $ verify checkCatastrophicRm "rm --recursive /etc/*$config*"
-- >>> prop $ verify checkCatastrophicRm "rm -rf /home"
-- >>> prop $ verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
-- >>> prop $ verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
-- >>> prop $ verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"
-- >>> prop $ verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"
-- >>> prop $ verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"
-- >>> prop $ verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"
checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
when (isRecursive t) $
mapM_ (mapM_ checkWord . braceExpand) $ arguments t
@@ -885,8 +936,9 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
["", "/", "/*", "/*/*"] >>= (\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
where
f t = whenShell [Bash,Ksh] $ do
@@ -906,15 +958,16 @@ missingDestination handler token = do
any (\x -> x /= "" && x `isPrefixOf` "target-directory") $
map snd args
prop_checkMvArguments1 = verify checkMvArguments "mv 'foo bar'"
prop_checkMvArguments2 = verifyNot checkMvArguments "mv foo bar"
prop_checkMvArguments3 = verifyNot checkMvArguments "mv 'foo bar'{,bak}"
prop_checkMvArguments4 = verifyNot checkMvArguments "mv \"$@\""
prop_checkMvArguments5 = verifyNot checkMvArguments "mv -t foo bar"
prop_checkMvArguments6 = verifyNot checkMvArguments "mv --target-directory=foo bar"
prop_checkMvArguments7 = verifyNot checkMvArguments "mv --target-direc=foo bar"
prop_checkMvArguments8 = verifyNot checkMvArguments "mv --version"
prop_checkMvArguments9 = verifyNot checkMvArguments "mv \"${!var}\""
-- |
-- >>> prop $ verify checkMvArguments "mv 'foo bar'"
-- >>> prop $ verifyNot checkMvArguments "mv foo bar"
-- >>> prop $ verifyNot checkMvArguments "mv 'foo bar'{,bak}"
-- >>> prop $ verifyNot checkMvArguments "mv \"$@\""
-- >>> prop $ verifyNot checkMvArguments "mv -t foo bar"
-- >>> prop $ verifyNot checkMvArguments "mv --target-directory=foo bar"
-- >>> prop $ verifyNot checkMvArguments "mv --target-direc=foo bar"
-- >>> prop $ verifyNot checkMvArguments "mv --version"
-- >>> prop $ verifyNot checkMvArguments "mv \"${!var}\""
checkMvArguments = CommandCheck (Basename "mv") $ missingDestination f
where
f t = err (getId t) 2224 "This mv has no destination. Check the arguments."
@@ -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."
prop_checkFindRedirections1 = verify checkFindRedirections "find . -exec echo {} > file \\;"
prop_checkFindRedirections2 = verifyNot checkFindRedirections "find . -exec echo {} \\; > file"
prop_checkFindRedirections3 = verifyNot checkFindRedirections "find . -execdir sh -c 'foo > file' \\;"
-- |
-- >>> prop $ verify checkFindRedirections "find . -exec echo {} > file \\;"
-- >>> prop $ verifyNot checkFindRedirections "find . -exec echo {} \\; > file"
-- >>> prop $ verifyNot checkFindRedirections "find . -execdir sh -c 'foo > file' \\;"
checkFindRedirections = CommandCheck (Basename "find") f
where
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)."
_ -> return ()
prop_checkWhich = verify checkWhich "which '.+'"
-- >>> prop $ verify checkWhich "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_checkSudoRedirect3 = verify checkSudoRedirect "sudo cmd >> file"
prop_checkSudoRedirect4 = verify checkSudoRedirect "sudo cmd &> file"
prop_checkSudoRedirect5 = verifyNot checkSudoRedirect "sudo cmd 2>&1"
prop_checkSudoRedirect6 = verifyNot checkSudoRedirect "sudo cmd 2> log"
prop_checkSudoRedirect7 = verifyNot checkSudoRedirect "sudo cmd > /dev/null 2>&1"
-- |
-- >>> prop $ verify checkSudoRedirect "sudo echo 3 > /proc/file"
-- >>> prop $ verify checkSudoRedirect "sudo cmd < input"
-- >>> prop $ verify checkSudoRedirect "sudo cmd >> file"
-- >>> prop $ verify checkSudoRedirect "sudo cmd &> file"
-- >>> prop $ verifyNot checkSudoRedirect "sudo cmd 2>&1"
-- >>> prop $ verifyNot checkSudoRedirect "sudo cmd 2> log"
-- >>> prop $ verifyNot checkSudoRedirect "sudo cmd > /dev/null 2>&1"
checkSudoRedirect = CommandCheck (Basename "sudo") f
where
f t = do
@@ -979,13 +1034,14 @@ checkSudoRedirect = CommandCheck (Basename "sudo") f
warnAbout _ = return ()
special file = concat (oversimplify file) == "/dev/null"
prop_checkSudoArgs1 = verify checkSudoArgs "sudo cd /root"
prop_checkSudoArgs2 = verify checkSudoArgs "sudo export x=3"
prop_checkSudoArgs3 = verifyNot checkSudoArgs "sudo ls /usr/local/protected"
prop_checkSudoArgs4 = verifyNot checkSudoArgs "sudo ls && export x=3"
prop_checkSudoArgs5 = verifyNot checkSudoArgs "sudo echo ls"
prop_checkSudoArgs6 = verifyNot checkSudoArgs "sudo -n -u export ls"
prop_checkSudoArgs7 = verifyNot checkSudoArgs "sudo docker export foo"
-- |
-- >>> prop $ verify checkSudoArgs "sudo cd /root"
-- >>> prop $ verify checkSudoArgs "sudo export x=3"
-- >>> prop $ verifyNot checkSudoArgs "sudo ls /usr/local/protected"
-- >>> prop $ verifyNot checkSudoArgs "sudo ls && export x=3"
-- >>> prop $ verifyNot checkSudoArgs "sudo echo ls"
-- >>> prop $ verifyNot checkSudoArgs "sudo -n -u export ls"
-- >>> prop $ verifyNot checkSudoArgs "sudo docker export foo"
checkSudoArgs = CommandCheck (Basename "sudo") f
where
f t = potentially $ do
@@ -999,5 +1055,14 @@ checkSudoArgs = CommandCheck (Basename "sudo") f
-- This mess is why ShellCheck prefers not to know.
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
module ShellCheck.Checks.ShellSupport (checker) where
import ShellCheck.AST
import ShellCheck.ASTLib
@@ -33,8 +32,6 @@ import Data.Char
import Data.List
import Data.Maybe
import qualified Data.Map as Map
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
data ForShell = ForShell [Shell] (Token -> Analysis)
@@ -67,9 +64,10 @@ testChecker (ForShell _ t) =
verify c s = producesComments (testChecker c) s == Just True
verifyNot c s = producesComments (testChecker c) s == Just False
prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
-- |
-- >>> prop $ verify checkForDecimals "((3.14*c))"
-- >>> prop $ verify checkForDecimals "foo[1.2]=bar"
-- >>> prop $ verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
checkForDecimals = ForShell [Sh, Dash, Bash] f
where
f t@(TA_Expansion id _) = potentially $ do
@@ -80,62 +78,63 @@ checkForDecimals = ForShell [Sh, Dash, Bash] f
f _ = return ()
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"
prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
prop_checkBashisms5 = verify checkBashisms "source file"
prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}"
prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}"
prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}"
prop_checkBashisms13= verify checkBashisms "exec -c env"
prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \""
prop_checkBashisms15= verify checkBashisms "let n++"
prop_checkBashisms16= verify checkBashisms "echo $RANDOM"
prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))"
prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"
prop_checkBashisms19= verify checkBashisms "foo > file*.txt"
prop_checkBashisms20= verify checkBashisms "read -ra foo"
prop_checkBashisms21= verify checkBashisms "[ -a foo ]"
prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]"
prop_checkBashisms23= verify checkBashisms "trap mything ERR INT"
prop_checkBashisms24= verifyNot checkBashisms "trap mything INT TERM"
prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123"
prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM"
prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*"
prop_checkBashisms28= verify checkBashisms "exec {n}>&2"
prop_checkBashisms29= verify checkBashisms "echo ${!var}"
prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\""
prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\""
prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo"
prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo"
prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo"
prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM"
prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }"
prop_checkBashisms40= verify checkBashisms "echo $(<file)"
prop_checkBashisms41= verify checkBashisms "echo `<file`"
prop_checkBashisms42= verify checkBashisms "trap foo int"
prop_checkBashisms43= verify checkBashisms "trap foo sigint"
prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/dash\necho $LINENO"
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"
prop_checkBashisms55= verify checkBashisms "#!/bin/sh\necho ${@%foo}"
prop_checkBashisms56= verifyNot checkBashisms "#!/bin/sh\necho ${##}"
-- |
-- >>> prop $ verify checkBashisms "while read a; do :; done < <(a)"
-- >>> prop $ verify checkBashisms "[ foo -nt bar ]"
-- >>> prop $ verify checkBashisms "echo $((i++))"
-- >>> prop $ verify checkBashisms "rm !(*.hs)"
-- >>> prop $ verify checkBashisms "source file"
-- >>> prop $ verify checkBashisms "[ \"$a\" == 42 ]"
-- >>> prop $ verify checkBashisms "echo ${var[1]}"
-- >>> prop $ verify checkBashisms "echo ${!var[@]}"
-- >>> prop $ verify checkBashisms "echo ${!var*}"
-- >>> prop $ verify checkBashisms "echo ${var:4:12}"
-- >>> prop $ verifyNot checkBashisms "echo ${var:-4}"
-- >>> prop $ verify checkBashisms "echo ${var//foo/bar}"
-- >>> prop $ verify checkBashisms "exec -c env"
-- >>> prop $ verify checkBashisms "echo -n \"Foo: \""
-- >>> prop $ verify checkBashisms "let n++"
-- >>> prop $ verify checkBashisms "echo $RANDOM"
-- >>> prop $ verify checkBashisms "echo $((RANDOM%6+1))"
-- >>> prop $ verify checkBashisms "foo &> /dev/null"
-- >>> prop $ verify checkBashisms "foo > file*.txt"
-- >>> prop $ verify checkBashisms "read -ra foo"
-- >>> prop $ verify checkBashisms "[ -a foo ]"
-- >>> prop $ verifyNot checkBashisms "[ foo -a bar ]"
-- >>> prop $ verify checkBashisms "trap mything ERR INT"
-- >>> prop $ verifyNot checkBashisms "trap mything INT TERM"
-- >>> prop $ verify checkBashisms "cat < /dev/tcp/host/123"
-- >>> prop $ verify checkBashisms "trap mything ERR SIGTERM"
-- >>> prop $ verify checkBashisms "echo *[^0-9]*"
-- >>> prop $ verify checkBashisms "exec {n}>&2"
-- >>> prop $ verify checkBashisms "echo ${!var}"
-- >>> prop $ verify checkBashisms "printf -v '%s' \"$1\""
-- >>> prop $ verify checkBashisms "printf '%q' \"$1\""
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
-- >>> prop $ verify checkBashisms "#!/bin/sh\necho -n foo"
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\necho -n foo"
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\nlocal foo"
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
-- >>> prop $ verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
-- >>> prop $ verify checkBashisms "RANDOM=9; echo $RANDOM"
-- >>> prop $ verify checkBashisms "foo-bar() { true; }"
-- >>> prop $ verify checkBashisms "echo $(<file)"
-- >>> prop $ verify checkBashisms "echo `<file`"
-- >>> prop $ verify checkBashisms "trap foo int"
-- >>> prop $ verify checkBashisms "trap foo sigint"
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
-- >>> prop $ verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
-- >>> prop $ verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
-- >>> prop $ verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
-- >>> prop $ verify checkBashisms "#!/bin/sh\ncmd >& file"
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
-- >>> prop $ verify checkBashisms "#!/bin/sh\nfoo+=bar"
-- >>> prop $ verify checkBashisms "#!/bin/sh\necho ${@%foo}"
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\necho ${##}"
checkBashisms = ForShell [Sh, Dash] $ \t -> do
params <- ask
kludge params t
@@ -302,11 +301,11 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")
]
bashVars = [
"LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
]
bashDynamicVars = [ "RANDOM", "SECONDS" ]
dashVars = [ "LINENO" ]
dashVars = [ ]
isBashVariable var =
(var `elem` bashDynamicVars
|| var `elem` bashVars && not (isAssigned var))
@@ -317,8 +316,9 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
Assignment (_, _, name, _) -> name == var
_ -> 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
where
f (T_Pipeline id _ [a, b]) =
@@ -344,10 +344,11 @@ checkEchoSed = ForShell [Bash, Ksh] f
f _ = return ()
prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}"
prop_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}"
prop_checkBraceExpansionVars3 = verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg"
prop_checkBraceExpansionVars4 = verify checkBraceExpansionVars "echo {$i..100}"
-- |
-- >>> prop $ verify checkBraceExpansionVars "echo {1..$n}"
-- >>> prop $ verifyNot checkBraceExpansionVars "echo {1,3,$n}"
-- >>> prop $ verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg"
-- >>> prop $ verify checkBraceExpansionVars "echo {$i..100}"
checkBraceExpansionVars = ForShell [Bash] f
where
f t@(T_BraceExpansion id list) = mapM_ check list
@@ -372,12 +373,13 @@ checkBraceExpansionVars = ForShell [Bash] f
return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval"
prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3"
prop_checkMultiDimensionalArrays2 = verifyNot checkMultiDimensionalArrays "foo[a]=3"
prop_checkMultiDimensionalArrays3 = verify checkMultiDimensionalArrays "foo=( [a][b]=c )"
prop_checkMultiDimensionalArrays4 = verifyNot checkMultiDimensionalArrays "foo=( [a]=c )"
prop_checkMultiDimensionalArrays5 = verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}"
prop_checkMultiDimensionalArrays6 = verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}"
-- |
-- >>> prop $ verify checkMultiDimensionalArrays "foo[a][b]=3"
-- >>> prop $ verifyNot checkMultiDimensionalArrays "foo[a]=3"
-- >>> prop $ verify checkMultiDimensionalArrays "foo=( [a][b]=c )"
-- >>> prop $ verifyNot checkMultiDimensionalArrays "foo=( [a]=c )"
-- >>> prop $ verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}"
-- >>> prop $ verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}"
checkMultiDimensionalArrays = ForShell [Bash] f
where
f token =
@@ -392,16 +394,17 @@ checkMultiDimensionalArrays = ForShell [Bash] f
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
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_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '"
prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '"
prop_checkPS14a= verify checkPS1Assignments "export PS1=$'\\e[3m; '"
prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"
prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'"
prop_checkPS18 = verifyNot checkPS1Assignments "PS1='\\[\\e\\]'"
-- |
-- >>> prop $ verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
-- >>> prop $ verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
-- >>> prop $ verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
-- >>> prop $ verify checkPS1Assignments "PS1=$'\\x1b[c '"
-- >>> prop $ verify checkPS1Assignments "PS1=$'\\e[3m; '"
-- >>> prop $ verify checkPS1Assignments "export PS1=$'\\e[3m; '"
-- >>> prop $ verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"
-- >>> prop $ verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
-- >>> prop $ verifyNot checkPS1Assignments "PS1='e033x1B'"
-- >>> prop $ verifyNot checkPS1Assignments "PS1='\\[\\e\\]'"
checkPS1Assignments = ForShell [Bash] f
where
f token = case token of
@@ -417,7 +420,3 @@ checkPS1Assignments = ForShell [Bash] f
isJust $ matchRegex escapeRegex unenclosed
enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,10 +17,51 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.Interface 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 Control.DeepSeq
import Control.Monad.Identity
import Data.Monoid
import GHC.Generics (Generic)
import qualified Data.Map as Map
@@ -35,7 +76,8 @@ data CheckSpec = CheckSpec {
csScript :: String,
csCheckSourced :: Bool,
csExcludedWarnings :: [Integer],
csShellTypeOverride :: Maybe Shell
csShellTypeOverride :: Maybe Shell,
csMinSeverity :: Severity
} deriving (Show, Eq)
data CheckResult = CheckResult {
@@ -43,44 +85,85 @@ data CheckResult = CheckResult {
crComments :: [PositionedComment]
} deriving (Show, Eq)
emptyCheckResult :: CheckResult
emptyCheckResult = CheckResult {
crFilename = "",
crComments = []
}
emptyCheckSpec :: CheckSpec
emptyCheckSpec = CheckSpec {
csFilename = "",
csScript = "",
csCheckSourced = False,
csExcludedWarnings = [],
csShellTypeOverride = Nothing
csShellTypeOverride = Nothing,
csMinSeverity = StyleC
}
newParseSpec :: ParseSpec
newParseSpec = ParseSpec {
psFilename = "",
psScript = "",
psCheckSourced = False,
psShellTypeOverride = Nothing
}
-- Parser input and output
data ParseSpec = ParseSpec {
psFilename :: String,
psScript :: String,
psCheckSourced :: Bool
psCheckSourced :: Bool,
psShellTypeOverride :: Maybe Shell
} deriving (Show, Eq)
data ParseResult = ParseResult {
prComments :: [PositionedComment],
prTokenPositions :: Map.Map Id Position,
prTokenPositions :: Map.Map Id (Position, Position),
prRoot :: Maybe Token
} deriving (Show, Eq)
newParseResult :: ParseResult
newParseResult = ParseResult {
prComments = [],
prTokenPositions = Map.empty,
prRoot = Nothing
}
-- Analyzer input and output
data AnalysisSpec = AnalysisSpec {
asScript :: Token,
asShellType :: Maybe Shell,
asExecutionMode :: ExecutionMode,
asCheckSourced :: Bool
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 {
arComments :: [TokenComment]
}
newAnalysisResult = AnalysisResult {
arComments = []
}
-- Formatter options
newtype FormatterOptions = FormatterOptions {
foColorOption :: ColorOption
data FormatterOptions = FormatterOptions {
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 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 {
posFile :: String, -- Filename
posLine :: Integer, -- 1 based source line
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)
data PositionedComment = PositionedComment Position Position Comment deriving (Show, Eq)
data TokenComment = TokenComment Id Comment deriving (Show, Eq)
newPosition :: Position
newPosition = Position {
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 =
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
# 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
resolver: lts-12.9
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
# This file strips all unit tests from ShellCheck, removing
# the dependency on QuickCheck and Template Haskell and
# reduces the binary size considerably.
set -o pipefail
sponge() {
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
# This file was deprecated by the doctest build.

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"