104 Commits

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

    read -a foo -r

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

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

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

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

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

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

In doing this, withNextId can now track both the entry and exit position
of every read* parser which uses it, enabling the tracking of end
columns throughout the application.
2018-06-07 21:55:41 -07:00
Vidar Holen
379321d1f3 Show tags in travis output 2018-06-07 20:40:44 -07:00
Vidar Holen
0adea473fd Update CHANGELOG after release 2018-06-07 20:19:00 -07:00
32 changed files with 1331 additions and 490 deletions

View File

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

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,27 @@
## 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,5 +1,5 @@
# Build-only image
FROM ubuntu:17.10 AS build
FROM ubuntu:18.04 AS build
USER root
WORKDIR /opt/shellCheck
@@ -9,13 +9,13 @@ RUN apt-get update && apt-get install -y ghc cabal-install
# Install Haskell deps
# (This is a separate copy/run so that source changes don't require rebuilding)
COPY ShellCheck.cabal ./
RUN cabal update && cabal install --dependencies-only
RUN cabal update && cabal install --dependencies-only --ghc-options="-optlo-Os -split-sections"
# Copy source and build it
COPY LICENSE Setup.hs shellcheck.hs ./
COPY src src
RUN cabal build Paths_ShellCheck && \
ghc -optl-static -optl-pthread -isrc -idist/build/autogen --make shellcheck && \
ghc -optl-static -optl-pthread -isrc -idist/build/autogen --make shellcheck -split-sections -optc-Wl,--gc-sections -optlo-Os && \
strip --strip-all shellcheck
RUN mkdir -p /out/bin && \

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

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

View File

@@ -1,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,6 +28,8 @@ Extra-Source-Files:
shellcheck.1.md
-- built with a cabal sdist hook
shellcheck.1
-- convenience script for stripping tests
striptests
-- tests
test/shellcheck.hs
@@ -35,7 +37,7 @@ custom-setup
setup-depends:
base >= 4 && <5,
process >= 1.0 && <1.7,
Cabal >= 1.10 && <2.3
Cabal >= 1.10 && <2.5
source-repository head
type: git

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# quickrun runs ShellCheck in an interpreted mode.
# This allows testing changes without recompiling.

View File

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

View File

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

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

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

View File

@@ -168,6 +168,8 @@ nodeChecks = [
,checkPipeToNowhere
,checkForLoopGlobVariables
,checkSubshelledTests
,checkInvertedStringTest
,checkRedirectionToCommand
]
@@ -396,7 +398,7 @@ checkPipePitfalls _ (T_Pipeline id _ commands) = do
mapM_ (f . (\ n -> take (length l) $ drop n commands)) indices
return . not . null $ indices
for' l f = for l (first f)
first func (x:_) = func (getId x)
first func (x:_) = func (getId $ getCommandTokenOrThis x)
first _ _ = return ()
hasShortParameter char = any (\x -> "-" `isPrefixOf` x && char `elem` x)
hasParameter string =
@@ -430,17 +432,22 @@ prop_checkShebang4 = verifyNotTree checkShebang "#shellcheck shell=sh\nfoo"
prop_checkShebang5 = verifyTree checkShebang "#!/usr/bin/env ash"
prop_checkShebang6 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=dash\n"
prop_checkShebang7 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=sh\n"
prop_checkShebang8 = verifyTree checkShebang "#!bin/sh\ntrue"
prop_checkShebang9 = verifyNotTree checkShebang "# shellcheck shell=sh\ntrue"
prop_checkShebang10= verifyNotTree checkShebang "#!foo\n# shellcheck shell=sh ignore=SC2239\ntrue"
checkShebang params (T_Annotation _ list t) =
if any isOverride list then [] else checkShebang params t
where
isOverride (ShellOverride _) = True
isOverride _ = False
checkShebang params (T_Script id sb _) = execWriter $
checkShebang params (T_Script id sb _) = execWriter $ do
unless (shellTypeSpecified params) $ do
when (sb == "") $
err id 2148 "Tips depend on target shell and yours is unknown. Add a shebang."
when (executableFromShebang sb == "ash") $
warn id 2187 "Ash scripts will be checked as Dash. Add '# shellcheck shell=dash' to silence."
unless (null sb || "/" `isPrefixOf` sb) $
err id 2239 "Ensure the shebang uses an absolute path to the interpreter."
prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done"
@@ -709,6 +716,7 @@ prop_checkArrayWithoutIndex5 = verifyTree checkArrayWithoutIndex "a[0]=foo; echo
prop_checkArrayWithoutIndex6 = verifyTree checkArrayWithoutIndex "echo $PIPESTATUS"
prop_checkArrayWithoutIndex7 = verifyTree checkArrayWithoutIndex "a=(a b); a+=c"
prop_checkArrayWithoutIndex8 = verifyTree checkArrayWithoutIndex "declare -a foo; foo=bar;"
prop_checkArrayWithoutIndex9 = verifyTree checkArrayWithoutIndex "read -r -a arr <<< 'foo bar'; echo \"$arr\""
checkArrayWithoutIndex params _ =
doVariableFlowAnalysis readF writeF defaultMap (variableFlow params)
where
@@ -1019,7 +1027,7 @@ checkQuotedCondRegex _ (TC_Binary _ _ "=~" _ rhs) =
error t =
unless (isConstantNonRe t) $
err (getId t) 2076
"Don't quote rhs of =~, it'll match literally rather than as a regex."
"Don't quote right-hand side of =~, it'll match literally rather than as a regex."
re = mkRegex "[][*.+()|]"
hasMetachars s = s `matches` re
isConstantNonRe t = fromMaybe False $ do
@@ -1029,13 +1037,16 @@ checkQuotedCondRegex _ _ = return ()
prop_checkGlobbedRegex1 = verify checkGlobbedRegex "[[ $foo =~ *foo* ]]"
prop_checkGlobbedRegex2 = verify checkGlobbedRegex "[[ $foo =~ f* ]]"
prop_checkGlobbedRegex2a = verify checkGlobbedRegex "[[ $foo =~ \\#* ]]"
prop_checkGlobbedRegex3 = verifyNot checkGlobbedRegex "[[ $foo =~ $foo ]]"
prop_checkGlobbedRegex4 = verifyNot checkGlobbedRegex "[[ $foo =~ ^c.* ]]"
prop_checkGlobbedRegex5 = verifyNot checkGlobbedRegex "[[ $foo =~ \\* ]]"
prop_checkGlobbedRegex6 = verifyNot checkGlobbedRegex "[[ $foo =~ (o*) ]]"
prop_checkGlobbedRegex7 = verifyNot checkGlobbedRegex "[[ $foo =~ \\*foo ]]"
prop_checkGlobbedRegex8 = verifyNot checkGlobbedRegex "[[ $foo =~ x\\* ]]"
checkGlobbedRegex _ (TC_Binary _ DoubleBracket "=~" _ rhs) =
let s = concat $ oversimplify rhs in
when (isConfusedGlobRegex s) $
warn (getId rhs) 2049 "=~ is for regex. Use == for globs."
warn (getId rhs) 2049 "=~ is for regex, but this looks like a glob. Use = instead."
checkGlobbedRegex _ _ = return ()
@@ -1186,11 +1197,12 @@ prop_checkComparisonAgainstGlob2 = verifyNot checkComparisonAgainstGlob "[[ $cow
prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = *foo* ]"
prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]"
prop_checkComparisonAgainstGlob5 = verify checkComparisonAgainstGlob "[[ $cow != $bar ]]"
prop_checkComparisonAgainstGlob6 = verify checkComparisonAgainstGlob "[ $f != /* ]"
checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _]))
| op `elem` ["=", "==", "!="] =
warn id 2053 $ "Quote the rhs of " ++ op ++ " in [[ ]] to prevent glob matching."
warn id 2053 $ "Quote the right-hand side of " ++ op ++ " in [[ ]] to prevent glob matching."
checkComparisonAgainstGlob _ (TC_Binary _ SingleBracket op _ word)
| (op == "=" || op == "==") && isGlob word =
| op `elem` ["=", "==", "!="] && isGlob word =
err (getId word) 2081 "[ .. ] can't match globs. Use [[ .. ]] or case statement."
checkComparisonAgainstGlob _ _ = return ()
@@ -1324,7 +1336,7 @@ prop_checkBackticks1 = verify checkBackticks "echo `foo`"
prop_checkBackticks2 = verifyNot checkBackticks "echo $(foo)"
prop_checkBackticks3 = verifyNot checkBackticks "echo `#inlined comment` foo"
checkBackticks _ (T_Backticked id list) | not (null list) =
style id 2006 "Use $(..) instead of legacy `..`."
style id 2006 "Use $(...) notation instead of legacy backticked `...`."
checkBackticks _ _ = return ()
prop_checkIndirectExpansion1 = verify checkIndirectExpansion "${foo$n}"
@@ -1598,6 +1610,7 @@ prop_checkSpacefulness32= verifyNotTree checkSpacefulness "var=$1; [ -v var ]"
prop_checkSpacefulness33= verifyTree checkSpacefulness "for file; do echo $file; done"
prop_checkSpacefulness34= verifyTree checkSpacefulness "declare foo$n=$1"
prop_checkSpacefulness35= verifyNotTree checkSpacefulness "echo ${1+\"$1\"}"
prop_checkSpacefulness36= verifyNotTree checkSpacefulness "arg=$#; echo $arg"
checkSpacefulness params t =
doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
@@ -1879,6 +1892,7 @@ prop_checkUnassignedReferences32= verifyNotTree checkUnassignedReferences "if [[
prop_checkUnassignedReferences33= verifyNotTree checkUnassignedReferences "f() { local -A foo; echo \"${foo[@]}\"; }"
prop_checkUnassignedReferences34= verifyNotTree checkUnassignedReferences "declare -A foo; (( foo[bar] ))"
prop_checkUnassignedReferences35= verifyNotTree checkUnassignedReferences "echo ${arr[foo-bar]:?fail}"
prop_checkUnassignedReferences36= verifyNotTree checkUnassignedReferences "read -a foo -r <<<\"foo bar\"; echo \"$foo\""
checkUnassignedReferences params t = warnings
where
(readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)
@@ -2069,6 +2083,7 @@ prop_checkCdAndBack1 = verify checkCdAndBack "for f in *; do cd $f; git pull; cd
prop_checkCdAndBack2 = verifyNot checkCdAndBack "for f in *; do cd $f || continue; git pull; cd ..; done"
prop_checkCdAndBack3 = verifyNot checkCdAndBack "while [[ $PWD != / ]]; do cd ..; done"
prop_checkCdAndBack4 = verify checkCdAndBack "cd $tmp; foo; cd -"
prop_checkCdAndBack5 = verifyNot checkCdAndBack "cd ..; foo; cd .."
checkCdAndBack params = doLists
where
shell = shellType params
@@ -2091,10 +2106,20 @@ checkCdAndBack params = doLists
getCmd (T_Pipeline id _ [x]) = getCommandName x
getCmd _ = Nothing
findCdPair list =
case list of
(a:b:rest) ->
if isCdRevert b && not (isCdRevert a)
then return $ getId b
else findCdPair (b:rest)
_ -> Nothing
doList list =
let cds = filter ((== Just "cd") . getCmd) list in
when (length cds >= 2 && isCdRevert (last cds)) $
info (getId $ last cds) 2103 message
potentially $ do
cd <- findCdPair cds
return $ info cd 2103 message
message = "Use a ( subshell ) to avoid having to cd back."
@@ -2472,7 +2497,7 @@ prop_checkReadWithoutR1 = verify checkReadWithoutR "read -a foo"
prop_checkReadWithoutR2 = verifyNot checkReadWithoutR "read -ar foo"
checkReadWithoutR _ t@T_SimpleCommand {} | t `isUnqualifiedCommand` "read" =
unless ("r" `elem` map snd (getAllFlags t)) $
info (getId t) 2162 "read without -r will mangle backslashes."
info (getId $ getCommandTokenOrThis t) 2162 "read without -r will mangle backslashes."
checkReadWithoutR _ _ = return ()
prop_checkUncheckedCd1 = verifyTree checkUncheckedCdPushdPopd "cd ~/src; rm -r foo"
@@ -2501,6 +2526,7 @@ prop_checkUncheckedPopd5 = verifyTree checkUncheckedCdPushdPopd "if true; then p
prop_checkUncheckedPopd6 = verifyTree checkUncheckedCdPushdPopd "popd"
prop_checkUncheckedPopd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\npopd\nrm bar"
prop_checkUncheckedPopd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; popd; rm bar"
prop_checkUncheckedPopd9 = verifyNotTree checkUncheckedCdPushdPopd "popd -n foo"
checkUncheckedCdPushdPopd params root =
if hasSetE params then
@@ -2510,7 +2536,7 @@ checkUncheckedCdPushdPopd params root =
checkElement t@T_SimpleCommand {} =
when(name t `elem` ["cd", "pushd", "popd"]
&& not (isSafeDir t)
&& not (name t == "pushd" && ("n" `elem` map snd (getAllFlags t)))
&& not (name t `elem` ["pushd", "popd"] && ("n" `elem` map snd (getAllFlags t)))
&& not (isCondition $ getPath (parentMap params) t)) $
warn (getId t) 2164 "Use 'cd ... || exit' or 'cd ... || return' in case cd fails."
checkElement _ = return ()
@@ -2685,26 +2711,31 @@ prop_checkUnmatchableCases5 = verify checkUnmatchableCases "case $f in *.txt) tr
prop_checkUnmatchableCases6 = verifyNot checkUnmatchableCases "case $f in ?*) true;; *) false;; esac"
prop_checkUnmatchableCases7 = verifyNot checkUnmatchableCases "case $f in $(x)) true;; asdf) false;; esac"
prop_checkUnmatchableCases8 = verify checkUnmatchableCases "case $f in cow) true;; bar|cow) false;; esac"
prop_checkUnmatchableCases9 = verifyNot checkUnmatchableCases "case $f in x) true;;& x) false;; esac"
checkUnmatchableCases _ t =
case t of
T_CaseExpression _ word list -> do
let patterns = concatMap snd3 list
-- Check all patterns for whether they can ever match
let allpatterns = concatMap snd3 list
-- Check only the non-fallthrough branches for shadowing
let breakpatterns = concatMap snd3 $ filter (\x -> fst3 x == CaseBreak) list
if isConstant word
then warn (getId word) 2194
"This word is constant. Did you forget the $ on a variable?"
else potentially $ do
pg <- wordToPseudoGlob word
return $ mapM_ (check pg) patterns
return $ mapM_ (check pg) allpatterns
let exactGlobs = tupMap wordToExactPseudoGlob patterns
let fuzzyGlobs = tupMap wordToPseudoGlob patterns
let exactGlobs = tupMap wordToExactPseudoGlob breakpatterns
let fuzzyGlobs = tupMap wordToPseudoGlob breakpatterns
let dominators = zip exactGlobs (tails $ drop 1 fuzzyGlobs)
mapM_ checkDoms dominators
_ -> return ()
where
fst3 (x,_,_) = x
snd3 (_,x,_) = x
check target candidate = potentially $ do
candidateGlob <- wordToPseudoGlob candidate
@@ -2837,6 +2868,7 @@ prop_checkPipeToNowhere5 = verifyNot checkPipeToNowhere "echo foo | xargs du"
prop_checkPipeToNowhere6 = verifyNot checkPipeToNowhere "ls | echo $(cat)"
prop_checkPipeToNowhere7 = verifyNot checkPipeToNowhere "echo foo | var=$(cat) ls"
prop_checkPipeToNowhere8 = verify checkPipeToNowhere "foo | true"
prop_checkPipeToNowhere9 = verifyNot checkPipeToNowhere "mv -i f . < /dev/stdin"
checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity ()
checkPipeToNowhere _ t =
case t of
@@ -2849,15 +2881,25 @@ checkPipeToNowhere _ t =
name <- getCommandBasename cmd
guard $ name `elem` nonReadingCommands
guard . not $ hasAdditionalConsumers cmd
-- Confusing echo for cat is so common that it's worth a special case
let suggestion =
if name == "echo"
then "Did you want 'cat' instead?"
else "Wrong command or missing xargs?"
return $ warn (getId cmd) 2216 $
"Piping to '" ++ name ++ "', a command that doesn't read stdin. Wrong command or missing xargs?"
"Piping to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion
checkRedir cmd = potentially $ do
name <- getCommandBasename cmd
guard $ name `elem` nonReadingCommands
guard . not $ hasAdditionalConsumers cmd
guard . not $ name `elem` ["cp", "mv", "rm"] && cmd `hasFlag` "i"
let suggestion =
if name == "echo"
then "Did you want 'cat' instead?"
else "Bad quoting, wrong command or missing xargs?"
return $ warn (getId cmd) 2217 $
"Redirecting to '" ++ name ++ "', a command that doesn't read stdin. Bad quoting or missing xargs?"
"Redirecting to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion
-- Could any words in a SimpleCommand consume stdin (e.g. echo "$(cat)")?
hasAdditionalConsumers t = fromMaybe True $ do
@@ -2926,9 +2968,10 @@ checkForLoopGlobVariables _ t =
prop_checkSubshelledTests1 = verify checkSubshelledTests "a && ( [ b ] || ! [ c ] )"
prop_checkSubshelledTests2 = verify checkSubshelledTests "( [ a ] )"
prop_checkSubshelledTests3 = verify checkSubshelledTests "( [ a ] && [ b ] || test c )"
prop_checkSubshelledTests4 = verify checkSubshelledTests "( [ a ] && { [ b ] && [ c ]; } )"
checkSubshelledTests params t =
case t of
T_Subshell id list | isSubshelledTest t ->
T_Subshell id list | all isTestStructure list ->
case () of
-- Special case for if (test) and while (test)
_ | isCompoundCondition (getPath (parentMap params) t) ->
@@ -2948,19 +2991,24 @@ checkSubshelledTests params t =
[c] | isTestCommand c -> True
_ -> False
isSubshelledTest t =
isTestStructure t =
case t of
T_Subshell _ list -> all isSubshelledTest list
T_AndIf _ a b -> isSubshelledTest a && isSubshelledTest b
T_OrIf _ a b -> isSubshelledTest a && isSubshelledTest b
T_Annotation _ _ t -> isSubshelledTest t
T_Banged _ t -> isTestStructure t
T_AndIf _ a b -> isTestStructure a && isTestStructure b
T_OrIf _ a b -> isTestStructure a && isTestStructure b
T_Pipeline _ [] [T_Redirecting _ _ cmd] ->
case cmd of
T_BraceGroup _ ts -> all isTestStructure ts
T_Subshell _ ts -> all isTestStructure ts
_ -> isTestCommand t
_ -> isTestCommand t
isTestCommand t =
case t of
T_Banged _ t -> isTestCommand t
T_Pipeline _ [] [T_Redirecting _ _ T_Condition {}] -> True
T_Pipeline _ [] [T_Redirecting _ _ cmd] -> cmd `isCommand` "test"
T_Pipeline _ [] [T_Redirecting _ _ cmd] ->
case cmd of
T_Condition {} -> True
_ -> cmd `isCommand` "test"
_ -> False
-- Check if a T_Subshell is used as a condition, e.g. if ( test )
@@ -2981,5 +3029,35 @@ checkSubshelledTests params t =
T_Annotation {} -> True
_ -> False
prop_checkInvertedStringTest1 = verify checkInvertedStringTest "[ ! -z $var ]"
prop_checkInvertedStringTest2 = verify checkInvertedStringTest "! [[ -n $var ]]"
prop_checkInvertedStringTest3 = verifyNot checkInvertedStringTest "! [ -x $var ]"
prop_checkInvertedStringTest4 = verifyNot checkInvertedStringTest "[[ ! -w $var ]]"
prop_checkInvertedStringTest5 = verifyNot checkInvertedStringTest "[ -z $var ]"
checkInvertedStringTest _ t =
case t of
TC_Unary _ _ "!" (TC_Unary _ _ op _) ->
case op of
"-n" -> style (getId t) 2236 "Use -z instead of ! -n."
"-z" -> style (getId t) 2236 "Use -n instead of ! -z."
_ -> return ()
T_Banged _ (T_Pipeline _ _
[T_Redirecting _ _ (T_Condition _ _ (TC_Unary _ _ op _))]) ->
case op of
"-n" -> style (getId t) 2237 "Use [ -z .. ] instead of ! [ -n .. ]."
"-z" -> style (getId t) 2237 "Use [ -n .. ] instead of ! [ -z .. ]."
_ -> return ()
_ -> return ()
prop_checkRedirectionToCommand1 = verify checkRedirectionToCommand "ls > rm"
prop_checkRedirectionToCommand2 = verifyNot checkRedirectionToCommand "ls > 'rm'"
prop_checkRedirectionToCommand3 = verifyNot checkRedirectionToCommand "ls > myfile"
checkRedirectionToCommand _ t =
case t of
T_IoFile _ _ (T_NormalWord id [T_Literal _ str]) | str `elem` commonCommands ->
unless (str == "file") $ -- This would be confusing
warn id 2238 "Redirecting to/from command name instead of file. Did you want pipes/xargs (or quote to ignore)?"
_ -> return ()
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -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

@@ -109,19 +109,17 @@ data DataSource =
data VariableState = Dead Token String | Alive deriving (Show)
defaultSpec root = AnalysisSpec {
asScript = root,
defaultSpec root = spec {
asShellType = Nothing,
asCheckSourced = False,
asExecutionMode = Executed
}
} where spec = newAnalysisSpec root
pScript s =
let
pSpec = ParseSpec {
pSpec = newParseSpec {
psFilename = "script",
psScript = s,
psCheckSourced = False
psScript = s
}
in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec
@@ -135,7 +133,14 @@ producesComments c s = do
makeComment :: Severity -> Id -> Code -> String -> TokenComment
makeComment severity id code note =
TokenComment id $ Comment severity code note
newTokenComment {
tcId = id,
tcComment = newComment {
cSeverity = severity,
cCode = code,
cMessage = note
}
}
addComment note = tell [note]
@@ -235,9 +240,10 @@ getParentTree t =
where
pre t = modify (first ((:) t))
post t = do
(_:rest, map) <- get
case rest of [] -> put (rest, map)
(x:_) -> put (rest, Map.insert (getId t) x map)
(x, map) <- get
case x of
_:rest -> case rest of [] -> put (rest, map)
(x:_) -> put (rest, Map.insert (getId t) x map)
-- Given a root node, make a map from Id to Token
getTokenMap :: Token -> Map.Map Id Token
@@ -520,12 +526,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 +584,14 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
where
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
getLiteral t = do
getLiteralOfDataType t d = do
s <- getLiteralString t
when ("-" `isPrefixOf` s) $ fail "argument"
return (base, t, s, DataString SourceExternal)
return (base, t, s, d)
getLiteral t = getLiteralOfDataType t (DataString SourceExternal)
getLiteralArray t = getLiteralOfDataType t (DataArray SourceExternal)
getModifierParamString = getModifierParam DataString
@@ -613,6 +633,11 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
guard $ isVariableName name
return (base, lastArg, name, DataArray SourceExternal)
-- get all the array variables used in read, e.g. read -a arr
getReadArrayVariables args = do
map (getLiteralArray . snd)
(filter (\(x,_) -> getLiteralString x == Just "-a") (zip (args) (tail args)))
getModifiedVariableCommand _ = []
getIndexReferences s = fromMaybe [] $ do
@@ -812,10 +837,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 +848,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) =

View File

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

View File

@@ -61,6 +61,7 @@ commandChecks = [
,checkGrepRe
,checkTrapQuotes
,checkReturn
,checkExit
,checkFindExecWithSingleArgument
,checkUnusedEchoEscapes
,checkInjectableFindSh
@@ -92,6 +93,7 @@ commandChecks = [
,checkWhich
,checkSudoRedirect
,checkSudoArgs
,checkSourceArgs
]
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
@@ -141,6 +143,7 @@ prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
prop_checkTr12= verifyNot checkTr "tr '[=e=]' 'e'"
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
where
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
@@ -152,7 +155,7 @@ checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
Just s -> do -- Eliminate false positives by only looking for dupes in SET2?
when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $
info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)."
unless ("[:" `isPrefixOf` s) $
unless ("[:" `isPrefixOf` s || "[=" `isPrefixOf` s) $
when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $
info (getId word) 2021 "Don't use [] around classes in tr, it replaces literal square brackets."
Nothing -> return ()
@@ -183,7 +186,7 @@ prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)
checkNeedlessExpr = CommandCheck (Basename "expr") f where
f t =
when (all (`notElem` exceptions) (words $ arguments t)) $
style (getId t) 2003
style (getId $ getCommandTokenOrThis t) 2003
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
-- These operators are hard to replicate in POSIX
exceptions = [ ":", "<", ">", "<=", ">=" ]
@@ -279,15 +282,28 @@ prop_checkReturn4 = verifyNot checkReturn "return $((a|b))"
prop_checkReturn5 = verify checkReturn "return -1"
prop_checkReturn6 = verify checkReturn "return 1000"
prop_checkReturn7 = verify checkReturn "return 'hello world'"
checkReturn = CommandCheck (Exactly "return") (f . arguments)
checkReturn = CommandCheck (Exactly "return") (returnOrExit
(\c -> err c 2151 "Only one integer 0-255 can be returned. Use stdout for other data.")
(\c -> err c 2152 "Can only return 0-255. Other data should be written to stdout."))
prop_checkExit1 = verifyNot checkExit "exit"
prop_checkExit2 = verifyNot checkExit "exit 1"
prop_checkExit3 = verifyNot checkExit "exit $var"
prop_checkExit4 = verifyNot checkExit "exit $((a|b))"
prop_checkExit5 = verify checkExit "exit -1"
prop_checkExit6 = verify checkExit "exit 1000"
prop_checkExit7 = verify checkExit "exit 'hello world'"
checkExit = CommandCheck (Exactly "exit") (returnOrExit
(\c -> err c 2241 "The exit status can only be one integer 0-255. Use stdout for other data.")
(\c -> err c 2242 "Can only exit with status 0-255. Other data should be written to stdout/stderr."))
returnOrExit multi invalid = (f . arguments)
where
f (first:second:_) =
err (getId second) 2151
"Only one integer 0-255 can be returned. Use stdout for other data."
multi (getId first)
f [value] =
when (isInvalid $ literal value) $
err (getId value) 2152
"Can only return 0-255. Other data should be written to stdout."
invalid (getId value)
f _ = return ()
isInvalid s = s == "" || any (not . isDigit) s || length s > 5
@@ -324,26 +340,18 @@ prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\n
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
checkUnusedEchoEscapes = CommandCheck (Basename "echo") (f . arguments)
checkUnusedEchoEscapes = CommandCheck (Basename "echo") f
where
isDashE = mkRegex "^-.*e"
hasEscapes = mkRegex "\\\\[rnt]"
f args | concat (concatMap oversimplify allButLast) `matches` isDashE =
return ()
where allButLast = reverse . drop 1 . reverse $ args
f args = mapM_ checkEscapes args
f cmd =
whenShell [Sh, Bash, Ksh] $
unless (cmd `hasFlag` "e") $
mapM_ examine $ arguments cmd
checkEscapes (T_NormalWord _ args) =
mapM_ checkEscapes args
checkEscapes (T_DoubleQuoted id args) =
mapM_ checkEscapes args
checkEscapes (T_Literal id str) = examine id str
checkEscapes (T_SingleQuoted id str) = examine id str
checkEscapes _ = return ()
examine id str =
examine token = do
let str = onlyLiteralString token
when (str `matches` hasEscapes) $
info id 2028 "echo won't expand escape sequences. Consider printf."
info (getId token) 2028 "echo may not expand escape sequences. Use printf."
prop_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
@@ -515,6 +523,9 @@ prop_checkPrintfVar12= verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1"
prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
prop_checkPrintfVar16= verifyNot checkPrintfVar "printf $'string'"
prop_checkPrintfVar17= verify checkPrintfVar "printf '%-*s\\n' 1"
prop_checkPrintfVar18= verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
@@ -525,11 +536,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
@@ -678,20 +698,24 @@ prop_checkFindWithoutPath3 = verifyNot checkFindWithoutPath "find . -type f"
prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"
prop_checkFindWithoutPath5 = verifyNot checkFindWithoutPath "find -O3 ."
prop_checkFindWithoutPath6 = verifyNot checkFindWithoutPath "find -D exec ."
prop_checkFindWithoutPath7 = verifyNot checkFindWithoutPath "find --help"
prop_checkFindWithoutPath8 = verifyNot checkFindWithoutPath "find -Hx . -print"
checkFindWithoutPath = CommandCheck (Basename "find") f
where
f (T_SimpleCommand _ _ (cmd:args)) =
unless (hasPath args) $
f t@(T_SimpleCommand _ _ (cmd:args)) =
unless (t `hasFlag` "help" || hasPath args) $
info (getId cmd) 2185 "Some finds don't have a default path. Specify '.' explicitly."
-- This is a bit of a kludge. find supports flag arguments both before and after the path,
-- as well as multiple non-flag arguments that are not the path. We assume that all the
-- pre-path flags are single characters, which is generally the case except for -O3.
-- This is a bit of a kludge. find supports flag arguments both before and
-- after the path, as well as multiple non-flag arguments that are not the
-- path. We assume that all the pre-path flags are single characters from a
-- list of GNU and macOS flags.
hasPath (first:rest) =
let flag = fromJust $ getLiteralStringExt (const $ return "___") first in
not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest
hasPath [] = False
isLeadingFlag flag = length flag <= 2 || "-O" `isPrefixOf` flag
isLeadingFlag flag = length flag <= 2 || all (`elem` leadingFlagChars) flag
leadingFlagChars="-EHLPXdfsxO0123456789"
prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10"
@@ -740,20 +764,20 @@ checkLocalScope = CommandCheck (Exactly "local") $ \t ->
whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local
path <- getPathM t
unless (any isFunction path) $
err (getId t) 2168 "'local' is only valid in functions."
err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
\t -> warn (getId t) 2186 "tempfile is deprecated. Use mktemp instead."
\t -> warn (getId $ getCommandTokenOrThis t) 2186 "tempfile is deprecated. Use mktemp instead."
prop_checkDeprecatedEgrep = verify checkDeprecatedEgrep "egrep '.+'"
checkDeprecatedEgrep = CommandCheck (Basename "egrep") $
\t -> info (getId t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."
\t -> info (getId $ getCommandTokenOrThis t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."
prop_checkDeprecatedFgrep = verify checkDeprecatedFgrep "fgrep '*' files"
checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $
\t -> info (getId t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
\t -> info (getId $ getCommandTokenOrThis t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
prop_checkWhileGetoptsCase1 = verify checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; esac; done"
prop_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; b) bar;; esac; done"
@@ -947,7 +971,7 @@ checkFindRedirections = CommandCheck (Basename "find") f
prop_checkWhich = 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"
@@ -999,5 +1023,16 @@ checkSudoArgs = CommandCheck (Basename "sudo") f
-- This mess is why ShellCheck prefers not to know.
parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:"
prop_checkSourceArgs1 = verify checkSourceArgs "#!/bin/sh\n. script arg"
prop_checkSourceArgs2 = verifyNot checkSourceArgs "#!/bin/sh\n. script"
prop_checkSourceArgs3 = verifyNot checkSourceArgs "#!/bin/bash\n. script arg"
checkSourceArgs = CommandCheck (Exactly ".") f
where
f t = whenShell [Sh, Dash] $
case arguments t of
(file:arg1:_) -> warn (getId arg1) 2240 $
"The dot command does not support arguments in sh/dash. Set them as variables."
_ -> return ()
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -127,7 +127,7 @@ prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/dash\necho $LINENO"
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
@@ -302,11 +302,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))

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

@@ -40,7 +40,10 @@ format = do
}
instance ToJSON (PositionedComment) where
toJSON comment@(PositionedComment start end (Comment level code string)) =
toJSON comment =
let start = pcStartPos comment
end = pcEndPos comment
c = pcComment comment in
object [
"file" .= posFile start,
"line" .= posLine start,
@@ -48,11 +51,14 @@ instance ToJSON (PositionedComment) where
"column" .= posColumn start,
"endColumn" .= posColumn end,
"level" .= severityText comment,
"code" .= code,
"message" .= string
"code" .= cCode c,
"message" .= cMessage c
]
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,8 +66,8 @@ instance ToJSON (PositionedComment) where
<> "column" .= posColumn start
<> "endColumn" .= posColumn end
<> "level" .= severityText comment
<> "code" .= code
<> "message" .= string
<> "code" .= cCode c
<> "message" .= cMessage c
)
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg

View File

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

View File

@@ -17,7 +17,39 @@
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
module ShellCheck.Interface
(
SystemInterface(..)
, CheckSpec(csFilename, csScript, csCheckSourced, csExcludedWarnings, csShellTypeOverride, csMinSeverity)
, CheckResult(crFilename, crComments)
, ParseSpec(psFilename, psScript, psCheckSourced, psShellTypeOverride)
, ParseResult(prComments, prTokenPositions, prRoot)
, AnalysisSpec(asScript, asShellType, asExecutionMode, asCheckSourced)
, AnalysisResult(arComments)
, FormatterOptions(foColorOption, foWikiLinkCount)
, Shell(Ksh, Sh, Bash, Dash)
, ExecutionMode(Executed, Sourced)
, ErrorMessage
, Code
, Severity(ErrorC, WarningC, InfoC, StyleC)
, Position(posFile, posLine, posColumn)
, Comment(cSeverity, cCode, cMessage)
, PositionedComment(pcStartPos , pcEndPos , pcComment)
, ColorOption(ColorAuto, ColorAlways, ColorNever)
, TokenComment(tcId, tcComment)
, emptyCheckResult
, newParseResult
, newAnalysisSpec
, newAnalysisResult
, newFormatterOptions
, newPosition
, newTokenComment
, mockedSystemInterface
, newParseSpec
, emptyCheckSpec
, newPositionedComment
, newComment
) where
import ShellCheck.AST
import Control.Monad.Identity
@@ -35,7 +67,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,28 +76,51 @@ 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,
@@ -73,14 +129,30 @@ data AnalysisSpec = AnalysisSpec {
asCheckSourced :: Bool
}
newAnalysisSpec token = AnalysisSpec {
asScript = token,
asShellType = Nothing,
asExecutionMode = Executed,
asCheckSourced = False
}
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
}
@@ -98,9 +170,48 @@ data Position = Position {
posColumn :: Integer -- 1 based source column, where tabs are 8
} deriving (Show, Eq)
data Comment = Comment Severity Code String deriving (Show, Eq)
data PositionedComment = PositionedComment Position Position Comment deriving (Show, Eq)
data TokenComment = TokenComment Id Comment deriving (Show, Eq)
newPosition :: Position
newPosition = Position {
posFile = "",
posLine = 1,
posColumn = 1
}
data Comment = Comment {
cSeverity :: Severity,
cCode :: Code,
cMessage :: String
} deriving (Show, Eq)
newComment :: Comment
newComment = Comment {
cSeverity = StyleC,
cCode = 0,
cMessage = ""
}
data PositionedComment = PositionedComment {
pcStartPos :: Position,
pcEndPos :: Position,
pcComment :: Comment
} deriving (Show, Eq)
newPositionedComment :: PositionedComment
newPositionedComment = PositionedComment {
pcStartPos = newPosition,
pcEndPos = newPosition,
pcComment = newComment
}
data TokenComment = TokenComment {
tcId :: Id,
tcComment :: Comment
} deriving (Show, Eq)
newTokenComment = TokenComment {
tcId = Id 0,
tcComment = newComment
}
data ColorOption =
ColorAuto

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@
set -o pipefail
sponge() {
local data
data="$(cat)"
printf '%s\n' "$data" > "$1"
}
@@ -22,7 +23,7 @@ modify() {
}
detestify() {
echo "-- AUTOGENERATED from ShellCheck by striptests. Do not modify."
printf '%s\n' '-- AUTOGENERATED from ShellCheck by striptests. Do not modify.'
awk '
BEGIN {
state = 0;
@@ -52,7 +53,7 @@ detestify() {
if [[ ! -e ShellCheck.cabal ]]
if [[ ! -e 'ShellCheck.cabal' ]]
then
echo "Run me from the ShellCheck directory." >&2
exit 1
@@ -64,7 +65,7 @@ then
exit 2
fi
modify ShellCheck.cabal sed -e '
modify 'ShellCheck.cabal' sed -e '
/QuickCheck/d
/^test-suite/{ s/.*//; q; }
'

35
test/buildtest Executable file
View File

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

80
test/distrotest Executable file
View File

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

27
test/stacktest Executable file
View File

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