mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 00:39:19 +08:00
Compare commits
234 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
cb57b4a74f | ||
|
e3f0243c0e | ||
|
66b5f13c6f | ||
|
a7a404a5a8 | ||
|
0761f5c923 | ||
|
b55149b22d | ||
|
4097bb5154 | ||
|
1b207b3d43 | ||
|
135b4aa485 | ||
|
cb76951ad2 | ||
|
705e476e4c | ||
|
e705552c97 | ||
|
198aa4fc3d | ||
|
f4044fbcc7 | ||
|
2827b35696 | ||
|
de95c376ea | ||
|
5e1b1e010a | ||
|
620c9c2023 | ||
|
359b1467a2 | ||
|
df0a0d41fa | ||
|
b815242506 | ||
|
07b5aa2971 | ||
|
f7b82658f4 | ||
|
e0e46e979a | ||
|
79319558a5 | ||
|
8d13add1ed | ||
|
8940e60300 | ||
|
5f1c969546 | ||
|
dadfdfde97 | ||
|
3e2cb26119 | ||
|
1a6ae4f19e | ||
|
95a376aad1 | ||
|
a06d7c1841 | ||
|
5202072a34 | ||
|
72af1cfd59 | ||
|
228af7df54 | ||
|
6db392511b | ||
|
07f04e13ce | ||
|
493ecd6f73 | ||
|
f0a2e688c4 | ||
|
0cee8a993d | ||
|
3d03b0ab3b | ||
|
488d6dcb41 | ||
|
d02a9bbcce | ||
|
165e408114 | ||
|
932e2b3538 | ||
|
76b1482f64 | ||
|
49250eadae | ||
|
3fe11927bb | ||
|
b16da4b242 | ||
|
c8e0797350 | ||
|
15aaacf715 | ||
|
5ef4229f61 | ||
|
afada43978 | ||
|
8be76b13b9 | ||
|
581be5878b | ||
|
0f835a5a2c | ||
|
4b0a35d4c9 | ||
|
51e0c1be62 | ||
|
d8a32da07f | ||
|
0d1a34a291 | ||
|
5005dc0fa1 | ||
|
b8ee7436e5 | ||
|
da8e450386 | ||
|
c3ac4c3d87 | ||
|
03ce3b15b6 | ||
|
10edba3ab8 | ||
|
797b424917 | ||
|
84e678e9ff | ||
|
3a672968f3 | ||
|
8c7efae393 | ||
|
f91b5bc270 | ||
|
b01f1128c7 | ||
|
db33294838 | ||
|
75fb4da387 | ||
|
366262af18 | ||
|
6869c2fa18 | ||
|
868a7be33e | ||
|
7138abff4b | ||
|
9d3e79b576 | ||
|
402e635f86 | ||
|
91cbcddd9d | ||
|
963b39b002 | ||
|
0cc45447d3 | ||
|
32a53f21b5 | ||
|
12b8720bd8 | ||
|
7adeaccd11 | ||
|
b63483d44c | ||
|
4111ce8fde | ||
|
b9a9eb2529 | ||
|
e717802de1 | ||
|
1699c9e9ba | ||
|
bfc32200e2 | ||
|
52e8a42d9d | ||
|
00360af672 | ||
|
8ff35fb4af | ||
|
29e8c0a16e | ||
|
3848788c2d | ||
|
0c459ae2cb | ||
|
e496b413bd | ||
|
48ac654a93 | ||
|
4470fe715c | ||
|
379321d1f3 | ||
|
0adea473fd | ||
|
a3be776f80 | ||
|
b5e5d249c4 | ||
|
efffc6150b | ||
|
cb4a0c0250 | ||
|
135cf5932f | ||
|
467dfe07b6 | ||
|
d140388bea | ||
|
884eff0c36 | ||
|
1d8047cce1 | ||
|
77546fba2f | ||
|
4a5ee06ce4 | ||
|
4dceecb1ed | ||
|
5b226e733b | ||
|
46c10c1571 | ||
|
1de8ba0210 | ||
|
407f6a63b9 | ||
|
7ee7448a70 | ||
|
48ebd41e22 | ||
|
0c88fbc76d | ||
|
b3362f1dc3 | ||
|
2c6bc43614 | ||
|
235bf6605f | ||
|
7029a713c7 | ||
|
cf608dc2f6 | ||
|
aa3b3fdc56 | ||
|
bca2ad4e18 | ||
|
719e1854e5 | ||
|
20ad7dc8de | ||
|
f84859ab90 | ||
|
08235a1cb2 | ||
|
728922d2b8 | ||
|
a953dd3454 | ||
|
ef6a5b97b9 | ||
|
8873a1732b | ||
|
5adfce72e1 | ||
|
12b3fdf661 | ||
|
bb4ce86fab | ||
|
7ec2fa2d3e | ||
|
5481ccd7f7 | ||
|
a1d8947297 | ||
|
683a30abde | ||
|
573936f353 | ||
|
ce7658ed86 | ||
|
0136d9ccce | ||
|
a4c6cea5e6 | ||
|
32af2783f0 | ||
|
08aab3c161 | ||
|
cf39adff75 | ||
|
da4072a118 | ||
|
08d2eef411 | ||
|
de257a6cf3 | ||
|
68c24925bc | ||
|
366dc5d3f8 | ||
|
1ed743e410 | ||
|
9a2aad16ad | ||
|
177cb10daa | ||
|
4aca1ff128 | ||
|
ffed7caff4 | ||
|
ef28200199 | ||
|
55216792c9 | ||
|
51e115cf47 | ||
|
764b242f1b | ||
|
c3b606c68a | ||
|
9f53109dfa | ||
|
795a881219 | ||
|
6dd5350e3b | ||
|
a5b359591c | ||
|
966194e387 | ||
|
71bcc80c2f | ||
|
48616225b3 | ||
|
99276cb9f5 | ||
|
f769d4e92c | ||
|
71df01c00f | ||
|
5364701914 | ||
|
fb97aca5a6 | ||
|
6b81a9924c | ||
|
cd7c077ecc | ||
|
b33607b048 | ||
|
969230f171 | ||
|
a98d69f4ff | ||
|
f71c142a44 | ||
|
9dfcf54f10 | ||
|
c8cd9dd09c | ||
|
8b8aeb4409 | ||
|
ee354ffce8 | ||
|
9fc3ddf849 | ||
|
ecb9d07f52 | ||
|
d16bf41c3d | ||
|
8d5e3a80ae | ||
|
34e0fa53c8 | ||
|
7fb27310e1 | ||
|
00d3c09ddb | ||
|
e8fc09414a | ||
|
b7a8b090d2 | ||
|
72044a79c6 | ||
|
6511dc0246 | ||
|
740441f2c4 | ||
|
b311563421 | ||
|
6d257bfa17 | ||
|
d8717c7046 | ||
|
7aa3a7ffc3 | ||
|
017af8333f | ||
|
f73d6f2332 | ||
|
a840f4e464 | ||
|
0f5e40c076 | ||
|
ccaacb108a | ||
|
56751413b4 | ||
|
ba5f20deda | ||
|
c86885427c | ||
|
7b3c4025fb | ||
|
3b004275cf | ||
|
72971fa52b | ||
|
dbdab5705f | ||
|
46a3019ed7 | ||
|
81978d15bd | ||
|
1d0db9267d | ||
|
a6fb9d1ef8 | ||
|
dc1e7c1bd4 | ||
|
5b14dba489 | ||
|
ee997fdec4 | ||
|
1badeff383 | ||
|
2d5ed23ca1 | ||
|
ec581cee90 | ||
|
bb32289ee3 | ||
|
31d6b063d9 | ||
|
3c5c74ff04 | ||
|
9657e8dda3 | ||
|
6ed60b403f | ||
|
8fa8823981 | ||
|
161801a86e |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,8 +1,8 @@
|
||||
#### For bugs
|
||||
- Rule Id (if any, e.g. SC1000):
|
||||
- My shellcheck version (`shellcheck --version` or "online"):
|
||||
- [ ] I read the issue's wiki page, e.g. https://github.com/koalaman/shellcheck/wiki/SC2086
|
||||
- [ ] I tried on shellcheck.net and verified that this is still a problem on the latest commit
|
||||
- [ ] It's not reproducible on shellcheck.net, but I think that's because it's an OS, configuration or encoding issue
|
||||
|
||||
#### For new checks and feature suggestions
|
||||
- [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this
|
||||
|
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
# Created by http://www.gitignore.io
|
||||
# Created by https://www.gitignore.io
|
||||
|
||||
### Haskell ###
|
||||
dist
|
||||
@@ -13,3 +13,10 @@ cabal-dev
|
||||
cabal.sandbox.config
|
||||
cabal.config
|
||||
.stack-work
|
||||
|
||||
### Snap ###
|
||||
/snap/.snapcraft/
|
||||
/stage/
|
||||
/parts/
|
||||
/prime/
|
||||
*.snap
|
||||
|
@@ -7,7 +7,7 @@ cd deploy
|
||||
cp ../LICENSE LICENSE.txt
|
||||
sed -e $'s/$/\r/' > README.txt << END
|
||||
This is a precompiled ShellCheck binary.
|
||||
http://www.shellcheck.net/
|
||||
https://www.shellcheck.net/
|
||||
|
||||
ShellCheck is a static analysis tool for shell scripts.
|
||||
It's licensed under the GNU General Public License v3.0.
|
||||
@@ -27,7 +27,7 @@ do
|
||||
zip "${file%.*}.zip" README.txt LICENSE.txt "$file"
|
||||
done
|
||||
|
||||
for file in *.linux
|
||||
for file in *.linux-x86_64
|
||||
do
|
||||
base="${file%.*}"
|
||||
cp "$file" "shellcheck"
|
||||
@@ -35,6 +35,14 @@ do
|
||||
rm "shellcheck"
|
||||
done
|
||||
|
||||
for file in *.linux-armv6hf
|
||||
do
|
||||
base="${file%.*}"
|
||||
cp "$file" "shellcheck"
|
||||
tar -cJf "$base.linux.armv6hf.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
|
||||
rm "shellcheck"
|
||||
done
|
||||
|
||||
for file in ./*
|
||||
do
|
||||
sha512sum "$file" > "$file.sha512sum"
|
||||
|
14
.snapsquid.conf
Normal file
14
.snapsquid.conf
Normal 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
|
||||
|
44
.travis.yml
44
.travis.yml
@@ -10,45 +10,53 @@ before_install:
|
||||
- DOCKER_BUILDS=""
|
||||
- TAGS=""
|
||||
- test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
|
||||
- test -n "$TRAVIS_TAG" && TAGS="$TAGS $TRAVIS_TAG" || true
|
||||
- test "$TRAVIS_BRANCH" = master && test -n "$TRAVIS_TAG" && TAGS="$TAGS stable" || true
|
||||
- test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
|
||||
- echo "Tags are $TAGS"
|
||||
|
||||
script:
|
||||
- mkdir deploy
|
||||
# Windows .exe
|
||||
- docker pull koalaman/winghc
|
||||
- docker run --user="$UID" --rm -v "$PWD:/appdata" koalaman/winghc cuib
|
||||
- for tag in $TAGS; do cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe"; done
|
||||
- rm -rf dist || true
|
||||
# Linux static executable
|
||||
- docker pull koalaman/scbuilder
|
||||
- docker run --user="$UID" --rm -v "$PWD:/mnt" koalaman/scbuilder
|
||||
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux"; done
|
||||
- ./shellcheck --version
|
||||
- rm -rf dist || true
|
||||
# Remove all tests to reduce binary size
|
||||
- ./striptests
|
||||
# Linux Docker image
|
||||
- name="$DOCKER_BASE"
|
||||
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
|
||||
- docker build -t "$name:current" .
|
||||
- docker run "$name:current" --version
|
||||
- printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
|
||||
- docker run -v "$PWD:/mnt" "$name:current" myscript
|
||||
# Copy static executable from docker image
|
||||
- id=$(docker create "$name:current")
|
||||
- docker cp "$id:/bin/shellcheck" "shellcheck"
|
||||
- docker rm "$id"
|
||||
- ls -l shellcheck
|
||||
- ./shellcheck myscript
|
||||
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux-x86_64"; done
|
||||
# Linux Alpine based Docker image
|
||||
- name="$DOCKER_BASE-alpine"
|
||||
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
|
||||
- sed 's/^FROM .*/FROM alpine:latest/' Dockerfile > Dockerfile.alpine
|
||||
- sed -e '/DELETE-MARKER/,$d' Dockerfile > Dockerfile.alpine
|
||||
- docker build -f Dockerfile.alpine -t "$name:current" .
|
||||
- docker run "$name:current" --version
|
||||
- docker run "$name:current" sh -c 'shellcheck --version'
|
||||
# Linux armv6hf static executable
|
||||
- docker run -v "$PWD:/mnt" koalaman/armv6hf-builder -c 'compile-shellcheck'
|
||||
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux-armv6hf"; done
|
||||
- rm -f shellcheck || true
|
||||
# Windows .exe
|
||||
- docker run --user="$UID" -v "$PWD:/appdata" koalaman/winghc cuib
|
||||
- for tag in $TAGS; do cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe"; done
|
||||
- rm -rf dist shellcheck || true
|
||||
# Misc packaging
|
||||
- ./.prepare_deploy
|
||||
|
||||
after_success:
|
||||
- docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
||||
- docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
||||
- for repo in $DOCKER_BUILDS;
|
||||
do
|
||||
for tag in $TAGS;
|
||||
do
|
||||
echo "Deploying $repo:current as $repo:$tag...";
|
||||
docker tag "$repo:current" "$repo:$tag";
|
||||
docker push "$repo:$tag";
|
||||
docker tag "$repo:current" "$repo:$tag" || exit 1;
|
||||
docker push "$repo:$tag" || exit 1;
|
||||
done;
|
||||
done;
|
||||
|
||||
|
51
CHANGELOG.md
51
CHANGELOG.md
@@ -1,3 +1,54 @@
|
||||
## v0.6.0 - 2018-12-02
|
||||
### Added
|
||||
- Command line option --severity/-S for filtering by minimum severity
|
||||
- Command line option --wiki-link-count/-W for showing wiki links
|
||||
- SC2152/SC2151: Warn about bad `exit` values like `1234` and `"foo"`
|
||||
- SC2236/SC2237: Suggest -n/-z instead of ! -z/-n
|
||||
- SC2238: Warn when redirecting to a known command name, e.g. ls > rm
|
||||
- SC2239: Warn if the shebang is not an absolute path, e.g. #!bin/sh
|
||||
- SC2240: Warn when passing additional arguments to dot (.) in sh/dash
|
||||
- SC1133: Better diagnostics when starting a line with |/||/&&
|
||||
|
||||
### Changed
|
||||
- Most warnings now have useful end positions
|
||||
- SC1117 about unknown double-quoted escape sequences has been retired
|
||||
|
||||
### Fixed
|
||||
- SC2021 no longer triggers for equivalence classes like `[=e=]`
|
||||
- SC2221/SC2222 no longer mistriggers on fall-through case branches
|
||||
- SC2081 about glob matches in `[ .. ]` now also triggers for `!=`
|
||||
- SC2086 no longer warns about spaces in `$#`
|
||||
- SC2164 no longer suggests subshells for `cd ..; cmd; cd ..`
|
||||
- `read -a` is now correctly considered an array assignment
|
||||
- SC2039 no longer warns about LINENO now that it's POSIX
|
||||
|
||||
## v0.5.0 - 2018-05-31
|
||||
### Added
|
||||
- SC2233/SC2234/SC2235: Suggest removing or replacing (..) around tests
|
||||
- SC2232: Warn about invalid arguments to sudo
|
||||
- SC2231: Suggest quoting expansions in for loop globs
|
||||
- SC2229: Warn about 'read $var'
|
||||
- SC2227: Warn about redirections in the middle of 'find' commands
|
||||
- SC2224/SC2225/SC2226: Warn when using mv/cp/ln without a destination
|
||||
- SC2223: Quote warning specific to `: ${var=value}`
|
||||
- SC1131: Warn when using `elseif` or `elsif`
|
||||
- SC1128: Warn about blanks/comments before shebang
|
||||
- SC1127: Warn about C-style comments
|
||||
|
||||
### Fixed
|
||||
- Annotations intended for a command's here documents now work
|
||||
- Escaped characters inside groups in =~ regexes now parse
|
||||
- Associative arrays are now respected in arithmetic contexts
|
||||
- SC1087 about `$var[@]` now correctly triggers on any index
|
||||
- Bad expansions in here documents are no longer ignored
|
||||
- FD move operations like {fd}>1- now parse correctly
|
||||
|
||||
### Changed
|
||||
- Here docs are now terminated as per spec, rather than by presumed intent
|
||||
- SC1073: 'else if' is now parsed correctly and not like 'elif'
|
||||
- SC2163: 'export $name' can now be silenced with 'export ${name?}'
|
||||
- SC2183: Now warns when printf arg count is not a multiple of format count
|
||||
|
||||
## v0.4.7 - 2017-12-08
|
||||
### Added
|
||||
- Statically linked binaries for Linux and Windows (see README.md)!
|
||||
|
34
Dockerfile
34
Dockerfile
@@ -1,10 +1,36 @@
|
||||
FROM scratch
|
||||
# Build-only image
|
||||
FROM ubuntu:18.04 AS build
|
||||
USER root
|
||||
WORKDIR /opt/shellCheck
|
||||
|
||||
# Install OS deps
|
||||
RUN apt-get update && apt-get install -y ghc cabal-install
|
||||
|
||||
# Install Haskell deps
|
||||
# (This is a separate copy/run so that source changes don't require rebuilding)
|
||||
COPY ShellCheck.cabal ./
|
||||
RUN cabal update && cabal install --dependencies-only --ghc-options="-optlo-Os -split-sections"
|
||||
|
||||
# Copy source and build it
|
||||
COPY LICENSE Setup.hs shellcheck.hs ./
|
||||
COPY src src
|
||||
RUN cabal build Paths_ShellCheck && \
|
||||
ghc -optl-static -optl-pthread -isrc -idist/build/autogen --make shellcheck -split-sections -optc-Wl,--gc-sections -optlo-Os && \
|
||||
strip --strip-all shellcheck
|
||||
|
||||
RUN mkdir -p /out/bin && \
|
||||
cp shellcheck /out/bin/
|
||||
|
||||
# Resulting Alpine image
|
||||
FROM alpine:latest
|
||||
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
|
||||
COPY --from=build /out /
|
||||
|
||||
# This file assumes ShellCheck has already been built.
|
||||
# See https://github.com/koalaman/scbuilder
|
||||
COPY shellcheck /bin/shellcheck
|
||||
# DELETE-MARKER (Remove everything below to keep the alpine image)
|
||||
|
||||
# Resulting ShellCheck image
|
||||
FROM scratch
|
||||
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
|
||||
WORKDIR /mnt
|
||||
COPY --from=build /out /
|
||||
ENTRYPOINT ["/bin/shellcheck"]
|
||||
|
18
LICENSE
18
LICENSE
@@ -1,7 +1,17 @@
|
||||
Employer mandated disclaimer:
|
||||
|
||||
I am providing code in the repository to you under an open source license.
|
||||
Because this is my personal repository, the license you receive to my code is
|
||||
from me and other individual contributors, and not my employer (Facebook).
|
||||
|
||||
- Vidar "koala_man" Holen
|
||||
|
||||
----
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
@@ -645,7 +655,7 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
@@ -664,11 +674,11 @@ might be different; for a GUI interface, you would use an "about box".
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
<https://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
|
102
README.md
102
README.md
@@ -1,8 +1,10 @@
|
||||
[](https://travis-ci.org/koalaman/shellcheck)
|
||||
|
||||
# ShellCheck - A shell script static analysis tool
|
||||
|
||||
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts:
|
||||
|
||||
.
|
||||

|
||||
|
||||
The goals of ShellCheck are
|
||||
|
||||
@@ -25,7 +27,7 @@ See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for ex
|
||||
- [In your editor](#in-your-editor)
|
||||
- [In your build or test suites](#in-your-build-or-test-suites)
|
||||
- [Installing](#installing)
|
||||
- [Travis CI Setup](#travis-ci-setup)
|
||||
- [Travis CI](#travis-ci)
|
||||
- [Compiling from source](#compiling-from-source)
|
||||
- [Installing Cabal](#installing-cabal)
|
||||
- [Compiling ShellCheck](#compiling-shellcheck)
|
||||
@@ -52,9 +54,9 @@ There are a number of ways to use ShellCheck!
|
||||
|
||||
### On the web
|
||||
|
||||
Paste a shell script on http://www.shellcheck.net for instant feedback.
|
||||
Paste a shell script on https://www.shellcheck.net for instant feedback.
|
||||
|
||||
[ShellCheck.net](http://www.shellcheck.net) is always synchronized to the latest git commit, and is the easiest way to give ShellCheck a go. Tell your friends!
|
||||
[ShellCheck.net](https://www.shellcheck.net) is always synchronized to the latest git commit, and is the easiest way to give ShellCheck a go. Tell your friends!
|
||||
|
||||
### From your terminal
|
||||
|
||||
@@ -68,7 +70,7 @@ You can see ShellCheck suggestions directly in a variety of editors.
|
||||
|
||||
.
|
||||
|
||||
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck):
|
||||
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck) or [Flymake](https://github.com/federicotdn/flymake-shellcheck):
|
||||
|
||||
.
|
||||
|
||||
@@ -99,7 +101,7 @@ On systems with Stack (installs to `~/.local/bin`):
|
||||
|
||||
stack update
|
||||
stack install ShellCheck
|
||||
|
||||
|
||||
On Debian based distros:
|
||||
|
||||
apt-get install shellcheck
|
||||
@@ -108,6 +110,8 @@ On Arch Linux based distros:
|
||||
|
||||
pacman -S shellcheck
|
||||
|
||||
or get the dependency free [shellcheck-static](https://aur.archlinux.org/packages/shellcheck-static/) from the AUR.
|
||||
|
||||
On Gentoo based distros:
|
||||
|
||||
emerge --ask shellcheck
|
||||
@@ -121,62 +125,78 @@ On Fedora based distros:
|
||||
|
||||
dnf install ShellCheck
|
||||
|
||||
On FreeBSD:
|
||||
|
||||
pkg install hs-ShellCheck
|
||||
|
||||
On OS X with homebrew:
|
||||
|
||||
brew install shellcheck
|
||||
|
||||
On OS X with MacPorts:
|
||||
On OpenBSD:
|
||||
|
||||
port install shellcheck
|
||||
pkg_add shellcheck
|
||||
|
||||
On openSUSE:Tumbleweed:
|
||||
On openSUSE
|
||||
|
||||
zypper in ShellCheck
|
||||
|
||||
On other openSUSE distributions:
|
||||
|
||||
Add OBS devel:languages:haskell repository from https://build.opensuse.org/project/repositories/devel:languages:haskell
|
||||
|
||||
zypper ar http://download.opensuse.org/repositories/devel:/languages:/haskell/openSUSE_$(version)/devel:languages:haskell.repo
|
||||
zypper in ShellCheck
|
||||
|
||||
Or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
|
||||
|
||||
On Solus:
|
||||
|
||||
eopkg install shellcheck
|
||||
|
||||
On Windows (via [scoop](http://scoop.sh)):
|
||||
|
||||
scoop install shellcheck
|
||||
|
||||
From Snap Store:
|
||||
|
||||
snap install --channel=edge shellcheck
|
||||
|
||||
From Docker Hub:
|
||||
|
||||
```sh
|
||||
docker pull koalaman/shellcheck:latest # Or :v0.4.6 for a release version
|
||||
docker pull koalaman/shellcheck:stable # Or :v0.4.7 for that version, or :latest for daily builds
|
||||
docker run -v "$PWD:/mnt" koalaman/shellcheck myscript
|
||||
```
|
||||
|
||||
or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend.
|
||||
or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend. It works exactly like a regular Alpine image, but has shellcheck preinstalled.
|
||||
|
||||
Alternatively, get freshly built binaries for the latest commit here:
|
||||
Alternatively, you can download pre-compiled binaries for the latest release here:
|
||||
|
||||
* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-latest.linux.x86_64.tar.xz) (statically linked)
|
||||
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-latest.zip)
|
||||
* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
|
||||
* [Linux, armv6hf](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
|
||||
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-stable.zip)
|
||||
|
||||
or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums and release builds.
|
||||
or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds.
|
||||
|
||||
## Travis CI Setup
|
||||
Distro packages already come with a `man` page. If you are building from source, it can be installed with:
|
||||
|
||||
If you want to use ShellCheck in Travis CI, you can most easily install it via `apt`:
|
||||
pandoc -s -t man shellcheck.1.md -o shellcheck.1
|
||||
sudo mv shellcheck.1 /usr/share/man/man1
|
||||
|
||||
```yml
|
||||
language: bash
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- debian-sid # Grab ShellCheck from the Debian repo
|
||||
packages:
|
||||
- shellcheck
|
||||
## Travis CI
|
||||
|
||||
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
|
||||
|
||||
If you still want to do so in order to upgrade at your leisure or ensure the latest release, follow the steps to install the shellcheck binary, bellow.
|
||||
|
||||
## Installing the shellcheck binary
|
||||
|
||||
*Pre-requisite*: the program 'xz' needs to be installed on the system.
|
||||
To install it on debian/ubuntu/linux mint, run `apt install xz-utils`.
|
||||
To install it on Redhat/Fedora/CentOS, run `yum -y install xz`.
|
||||
|
||||
```bash
|
||||
export scversion="stable" # or "v0.4.7", or "latest"
|
||||
wget "https://storage.googleapis.com/shellcheck/shellcheck-${scversion}.linux.x86_64.tar.xz"
|
||||
tar --xz -xvf shellcheck-"${scversion}".linux.x86_64.tar.xz
|
||||
cp shellcheck-"${scversion}"/shellcheck /usr/bin/
|
||||
shellcheck --version
|
||||
```
|
||||
|
||||
|
||||
## Compiling from source
|
||||
|
||||
This section describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile.
|
||||
@@ -293,6 +313,7 @@ alias archive='mv $1 /backup' # Defining aliases with arguments
|
||||
tr -cd '[a-zA-Z0-9]' # [] around ranges in tr
|
||||
exec foo; echo "Done!" # Misused 'exec'
|
||||
find -name \*.bak -o -name \*~ -delete # Implicit precedence in find
|
||||
# find . -exec foo > bar \; # Redirections in find
|
||||
f() { whoami; }; sudo f # External use of internal functions
|
||||
```
|
||||
|
||||
@@ -308,9 +329,13 @@ var$n="Hello" # Wrong indirect assignment
|
||||
echo ${var$n} # Wrong indirect reference
|
||||
var=(1, 2, 3) # Comma separated arrays
|
||||
array=( [index] = value ) # Incorrect index initialization
|
||||
echo $var[14] # Missing {} in array references
|
||||
echo "Argument 10 is $10" # Positional parameter misreference
|
||||
if $(myfunction); then ..; fi # Wrapping commands in $()
|
||||
else if othercondition; then .. # Using 'else if'
|
||||
f; f() { echo "hello world; } # Using function before definition
|
||||
[ false ] # 'false' being true
|
||||
if ( -f file ) # Using (..) instead of test
|
||||
```
|
||||
|
||||
### Style
|
||||
@@ -341,6 +366,8 @@ printf "%s\n" "Arguments: $@." # Concatenating strings and arrays
|
||||
var=World; echo "Hello " var # Unused lowercase variables
|
||||
echo "Hello $name" # Unassigned lowercase variables
|
||||
cmd | read bar; echo $bar # Assignments in subshells
|
||||
cat foo | cp bar # Piping to commands that don't read
|
||||
printf '%s: %s\n' foo # Mismatches in printf argument count
|
||||
```
|
||||
|
||||
### Robustness
|
||||
@@ -354,6 +381,7 @@ find . -exec sh -c 'a && b {}' \; # Find -exec shell injection
|
||||
printf "Hello $name" # Variables in printf format
|
||||
for f in $(ls *.txt); do # Iterating over ls output
|
||||
export MYVAR=$(cmd) # Masked exit codes
|
||||
case $version in 2.*) :;; 2.6.*) # Shadowed case branches
|
||||
```
|
||||
|
||||
### Portability
|
||||
@@ -388,6 +416,7 @@ var=42 echo $var # Expansion of inlined environment
|
||||
echo $((n/180*100)) # Unnecessary loss of precision
|
||||
ls *[:digit:].txt # Bad character class globs
|
||||
sed 's/foo/bar/' file > file # Redirecting to input
|
||||
while getopts "a" f; do case $f in "b") # Unhandled getopts flags
|
||||
```
|
||||
|
||||
## Testimonials
|
||||
@@ -422,6 +451,11 @@ The contributor retains the copyright.
|
||||
|
||||
ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE).
|
||||
|
||||
Copyright 2012-2015, Vidar 'koala_man' Holen and contributors.
|
||||
Copyright 2012-2018, Vidar 'koala_man' Holen and contributors.
|
||||
|
||||
Happy ShellChecking!
|
||||
|
||||
|
||||
## Other Resources
|
||||
* The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221).
|
||||
* ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)!
|
||||
|
2
Setup.hs
2
Setup.hs
@@ -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"
|
||||
|
@@ -1,12 +1,12 @@
|
||||
Name: ShellCheck
|
||||
Version: 0.4.7
|
||||
Version: 0.6.0
|
||||
Synopsis: Shell script analysis tool
|
||||
License: GPL-3
|
||||
License-file: LICENSE
|
||||
Category: Static Analysis
|
||||
Author: Vidar Holen
|
||||
Maintainer: vidar@vidarholen.net
|
||||
Homepage: http://www.shellcheck.net/
|
||||
Homepage: https://www.shellcheck.net/
|
||||
Build-Type: Custom
|
||||
Cabal-Version: >= 1.8
|
||||
Bug-reports: https://github.com/koalaman/shellcheck/issues
|
||||
@@ -28,19 +28,34 @@ Extra-Source-Files:
|
||||
shellcheck.1.md
|
||||
-- built with a cabal sdist hook
|
||||
shellcheck.1
|
||||
-- convenience script for stripping tests
|
||||
striptests
|
||||
-- tests
|
||||
test/shellcheck.hs
|
||||
|
||||
custom-setup
|
||||
setup-depends:
|
||||
base >= 4 && <5,
|
||||
process >= 1.0 && <1.7,
|
||||
Cabal >= 1.10 && <2.5
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: git://github.com/koalaman/shellcheck.git
|
||||
|
||||
library
|
||||
hs-source-dirs: src
|
||||
if impl(ghc < 8.0)
|
||||
build-depends:
|
||||
semigroups
|
||||
build-depends:
|
||||
base >= 4 && < 5,
|
||||
containers,
|
||||
-- GHC 7.6.3 (base 4.6.0.1) is buggy (#1131, #1119) in optimized mode.
|
||||
-- Just disable that version entirely to fail fast.
|
||||
aeson,
|
||||
base > 4.6.0.1 && < 5,
|
||||
bytestring,
|
||||
containers >= 0.5,
|
||||
directory,
|
||||
json,
|
||||
mtl >= 2.2.1,
|
||||
parsec,
|
||||
regex-tdfa,
|
||||
@@ -69,27 +84,34 @@ library
|
||||
Paths_ShellCheck
|
||||
|
||||
executable shellcheck
|
||||
if impl(ghc < 8.0)
|
||||
build-depends:
|
||||
semigroups
|
||||
build-depends:
|
||||
aeson,
|
||||
base >= 4 && < 5,
|
||||
bytestring,
|
||||
ShellCheck,
|
||||
containers,
|
||||
directory,
|
||||
json,
|
||||
mtl >= 2.2.1,
|
||||
parsec,
|
||||
regex-tdfa,
|
||||
QuickCheck >= 2.7.4
|
||||
parsec >= 3.0,
|
||||
QuickCheck >= 2.7.4,
|
||||
regex-tdfa
|
||||
main-is: shellcheck.hs
|
||||
|
||||
test-suite test-shellcheck
|
||||
type: exitcode-stdio-1.0
|
||||
build-depends:
|
||||
aeson,
|
||||
base >= 4 && < 5,
|
||||
bytestring,
|
||||
ShellCheck,
|
||||
containers,
|
||||
directory,
|
||||
json,
|
||||
mtl >= 2.2.1,
|
||||
parsec,
|
||||
regex-tdfa,
|
||||
QuickCheck >= 2.7.4
|
||||
QuickCheck >= 2.7.4,
|
||||
regex-tdfa
|
||||
main-is: test/shellcheck.hs
|
||||
|
||||
|
@@ -1,121 +0,0 @@
|
||||
{-
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Interface where
|
||||
|
||||
import ShellCheck.AST
|
||||
import Control.Monad.Identity
|
||||
import qualified Data.Map as Map
|
||||
|
||||
|
||||
newtype SystemInterface m = SystemInterface {
|
||||
-- Read a file by filename, or return an error
|
||||
siReadFile :: String -> m (Either ErrorMessage String)
|
||||
}
|
||||
|
||||
-- ShellCheck input and output
|
||||
data CheckSpec = CheckSpec {
|
||||
csFilename :: String,
|
||||
csScript :: String,
|
||||
csCheckSourced :: Bool,
|
||||
csExcludedWarnings :: [Integer],
|
||||
csShellTypeOverride :: Maybe Shell
|
||||
} deriving (Show, Eq)
|
||||
|
||||
data CheckResult = CheckResult {
|
||||
crFilename :: String,
|
||||
crComments :: [PositionedComment]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
emptyCheckSpec :: CheckSpec
|
||||
emptyCheckSpec = CheckSpec {
|
||||
csFilename = "",
|
||||
csScript = "",
|
||||
csCheckSourced = False,
|
||||
csExcludedWarnings = [],
|
||||
csShellTypeOverride = Nothing
|
||||
}
|
||||
|
||||
-- Parser input and output
|
||||
data ParseSpec = ParseSpec {
|
||||
psFilename :: String,
|
||||
psScript :: String,
|
||||
psCheckSourced :: Bool
|
||||
} deriving (Show, Eq)
|
||||
|
||||
data ParseResult = ParseResult {
|
||||
prComments :: [PositionedComment],
|
||||
prTokenPositions :: Map.Map Id Position,
|
||||
prRoot :: Maybe Token
|
||||
} deriving (Show, Eq)
|
||||
|
||||
-- Analyzer input and output
|
||||
data AnalysisSpec = AnalysisSpec {
|
||||
asScript :: Token,
|
||||
asShellType :: Maybe Shell,
|
||||
asExecutionMode :: ExecutionMode,
|
||||
asCheckSourced :: Bool
|
||||
}
|
||||
|
||||
newtype AnalysisResult = AnalysisResult {
|
||||
arComments :: [TokenComment]
|
||||
}
|
||||
|
||||
|
||||
-- Formatter options
|
||||
newtype FormatterOptions = FormatterOptions {
|
||||
foColorOption :: ColorOption
|
||||
}
|
||||
|
||||
|
||||
-- Supporting data types
|
||||
data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
|
||||
data ExecutionMode = Executed | Sourced deriving (Show, Eq)
|
||||
|
||||
type ErrorMessage = String
|
||||
type Code = Integer
|
||||
|
||||
data Severity = ErrorC | WarningC | InfoC | StyleC deriving (Show, Eq, Ord)
|
||||
data Position = Position {
|
||||
posFile :: String, -- Filename
|
||||
posLine :: Integer, -- 1 based source line
|
||||
posColumn :: Integer -- 1 based source column, where tabs are 8
|
||||
} deriving (Show, Eq)
|
||||
|
||||
data Comment = Comment Severity Code String deriving (Show, Eq)
|
||||
data PositionedComment = PositionedComment Position Position Comment deriving (Show, Eq)
|
||||
data TokenComment = TokenComment Id Comment deriving (Show, Eq)
|
||||
|
||||
data ColorOption =
|
||||
ColorAuto
|
||||
| ColorAlways
|
||||
| ColorNever
|
||||
deriving (Ord, Eq, Show)
|
||||
|
||||
-- For testing
|
||||
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
|
||||
mockedSystemInterface files = SystemInterface {
|
||||
siReadFile = rf
|
||||
}
|
||||
where
|
||||
rf file =
|
||||
case filter ((== file) . fst) files of
|
||||
[] -> return $ Left "File not included in mock."
|
||||
[(_, contents)] -> return $ Right contents
|
||||
|
@@ -1,7 +1,10 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
# TODO: Find a less trashy way to get the next available error code
|
||||
|
||||
shopt -s globstar
|
||||
if ! shopt -s globstar
|
||||
then
|
||||
echo "Error: This script depends on Bash 4." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for i in 1 2
|
||||
do
|
||||
|
4
quickrun
4
quickrun
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
# quickrun runs ShellCheck in an interpreted mode.
|
||||
# This allows testing changes without recompiling.
|
||||
|
||||
runghc -idist/build/autogen shellcheck.hs "$@"
|
||||
runghc -isrc -idist/build/autogen shellcheck.hs "$@"
|
||||
|
@@ -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.
|
||||
|
@@ -56,6 +56,11 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
||||
standard output. Subsequent **-f** options are ignored, see **FORMATS**
|
||||
below for more information.
|
||||
|
||||
**-S**\ *SEVERITY*,\ **--severity=***severity*
|
||||
|
||||
: Specify minimum severity of errors to consider. Valid values are *error*,
|
||||
*warning*, *info* and *style*. The default is *style*.
|
||||
|
||||
**-s**\ *shell*,\ **--shell=***shell*
|
||||
|
||||
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
|
||||
@@ -66,6 +71,11 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
||||
|
||||
: Print version information and exit.
|
||||
|
||||
**-W** *NUM*,\ **--wiki-link-count=NUM**
|
||||
|
||||
: For TTY output, show *NUM* wiki links to more information about mentioned
|
||||
warnings. Set to 0 to disable them entirely.
|
||||
|
||||
**-x**,\ **--external-sources**
|
||||
|
||||
: Follow 'source' statements even when the file is not specified as input.
|
||||
@@ -207,7 +217,7 @@ https://github.com/koalaman/shellcheck/issues
|
||||
# COPYRIGHT
|
||||
Copyright 2012-2015, Vidar Holen.
|
||||
Licensed under the GNU General Public License version 3 or later,
|
||||
see http://gnu.org/licenses/gpl.html
|
||||
see https://gnu.org/licenses/gpl.html
|
||||
|
||||
|
||||
# SEE ALSO
|
||||
|
153
shellcheck.hs
153
shellcheck.hs
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,37 +15,38 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Checker
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Regex
|
||||
import ShellCheck.Checker
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Regex
|
||||
|
||||
import ShellCheck.Formatter.Format
|
||||
import qualified ShellCheck.Formatter.CheckStyle
|
||||
import ShellCheck.Formatter.Format
|
||||
import qualified ShellCheck.Formatter.GCC
|
||||
import qualified ShellCheck.Formatter.JSON
|
||||
import qualified ShellCheck.Formatter.TTY
|
||||
|
||||
import Control.Exception
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Data.Bits
|
||||
import Data.Char
|
||||
import Data.Either
|
||||
import Data.Functor
|
||||
import Data.IORef
|
||||
import Data.List
|
||||
import qualified Data.Map as Map
|
||||
import Data.Maybe
|
||||
import Data.Monoid
|
||||
import Prelude hiding (catch)
|
||||
import System.Console.GetOpt
|
||||
import System.Directory
|
||||
import System.Environment
|
||||
import System.Exit
|
||||
import System.IO
|
||||
import Control.Exception
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Data.Bits
|
||||
import Data.Char
|
||||
import Data.Either
|
||||
import Data.Functor
|
||||
import Data.IORef
|
||||
import Data.List
|
||||
import qualified Data.Map as Map
|
||||
import Data.Maybe
|
||||
import Data.Monoid
|
||||
import Data.Semigroup (Semigroup (..))
|
||||
import Prelude hiding (catch)
|
||||
import System.Console.GetOpt
|
||||
import System.Directory
|
||||
import System.Environment
|
||||
import System.Exit
|
||||
import System.IO
|
||||
|
||||
data Flag = Flag String String
|
||||
data Status =
|
||||
@@ -56,22 +57,27 @@ data Status =
|
||||
| RuntimeException
|
||||
deriving (Ord, Eq, Show)
|
||||
|
||||
instance Semigroup Status where
|
||||
(<>) = max
|
||||
|
||||
instance Monoid Status where
|
||||
mempty = NoProblems
|
||||
mappend = max
|
||||
mappend = (Data.Semigroup.<>)
|
||||
|
||||
data Options = Options {
|
||||
checkSpec :: CheckSpec,
|
||||
externalSources :: Bool,
|
||||
formatterOptions :: FormatterOptions
|
||||
checkSpec :: CheckSpec,
|
||||
externalSources :: Bool,
|
||||
formatterOptions :: FormatterOptions,
|
||||
minSeverity :: Severity
|
||||
}
|
||||
|
||||
defaultOptions = Options {
|
||||
checkSpec = emptyCheckSpec,
|
||||
externalSources = False,
|
||||
formatterOptions = FormatterOptions {
|
||||
formatterOptions = newFormatterOptions {
|
||||
foColorOption = ColorAuto
|
||||
}
|
||||
},
|
||||
minSeverity = StyleC
|
||||
}
|
||||
|
||||
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
|
||||
@@ -89,8 +95,14 @@ options = [
|
||||
Option "s" ["shell"]
|
||||
(ReqArg (Flag "shell") "SHELLNAME")
|
||||
"Specify dialect (sh, bash, dash, ksh)",
|
||||
Option "S" ["severity"]
|
||||
(ReqArg (Flag "severity") "SEVERITY")
|
||||
"Minimum severity of errors to consider (error, warning, info, style)",
|
||||
Option "V" ["version"]
|
||||
(NoArg $ Flag "version" "true") "Print version information",
|
||||
Option "W" ["wiki-link-count"]
|
||||
(ReqArg (Flag "wiki-link-count") "NUM")
|
||||
"The number of wiki links to show, when applicable.",
|
||||
Option "x" ["external-sources"]
|
||||
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES"
|
||||
]
|
||||
@@ -117,9 +129,9 @@ formatList = intercalate ", " names
|
||||
where
|
||||
names = Map.keys $ formats (formatterOptions defaultOptions)
|
||||
|
||||
getOption [] _ = Nothing
|
||||
getOption [] _ = Nothing
|
||||
getOption (Flag var val:_) name | name == var = return val
|
||||
getOption (_:rest) flag = getOption rest flag
|
||||
getOption (_:rest) flag = getOption rest flag
|
||||
|
||||
getOptions options name =
|
||||
map (\(Flag _ val) -> val) . filter (\(Flag var _) -> var == name) $ options
|
||||
@@ -133,12 +145,6 @@ split char str =
|
||||
else split' rest (a:element)
|
||||
split' [] element = [reverse element]
|
||||
|
||||
getExclusions options =
|
||||
let elements = concatMap (split ',') $ getOptions options "exclude"
|
||||
clean = dropWhile (not . isDigit)
|
||||
in
|
||||
map (Prelude.read . clean) elements :: [Int]
|
||||
|
||||
toStatus = fmap (either id id) . runExceptT
|
||||
|
||||
getEnvArgs = do
|
||||
@@ -159,10 +165,10 @@ main = do
|
||||
|
||||
statusToCode status =
|
||||
case status of
|
||||
NoProblems -> ExitSuccess
|
||||
SomeProblems -> ExitFailure 1
|
||||
SyntaxFailure -> ExitFailure 3
|
||||
SupportFailure -> ExitFailure 4
|
||||
NoProblems -> ExitSuccess
|
||||
SomeProblems -> ExitFailure 1
|
||||
SyntaxFailure -> ExitFailure 3
|
||||
SupportFailure -> ExitFailure 4
|
||||
RuntimeException -> ExitFailure 2
|
||||
|
||||
process :: [Flag] -> [FilePath] -> ExceptT Status IO Status
|
||||
@@ -203,7 +209,7 @@ runFormatter sys format options files = do
|
||||
|
||||
process :: FilePath -> IO Status
|
||||
process filename = do
|
||||
input <- (siReadFile sys) filename
|
||||
input <- siReadFile sys filename
|
||||
either (reportFailure filename) check input
|
||||
where
|
||||
check contents = do
|
||||
@@ -218,12 +224,28 @@ runFormatter sys format options files = do
|
||||
then NoProblems
|
||||
else SomeProblems
|
||||
|
||||
parseColorOption colorOption =
|
||||
case colorOption of
|
||||
"auto" -> ColorAuto
|
||||
"always" -> ColorAlways
|
||||
"never" -> ColorNever
|
||||
_ -> error $ "Bad value for --color `" ++ colorOption ++ "'"
|
||||
parseEnum name value list =
|
||||
case filter ((== value) . fst) list of
|
||||
[(name, value)] -> return value
|
||||
[] -> do
|
||||
printErr $ "Unknown value for --" ++ name ++ ". " ++
|
||||
"Valid options are: " ++ (intercalate ", " $ map fst list)
|
||||
throwError SupportFailure
|
||||
|
||||
parseColorOption value =
|
||||
parseEnum "color" value [
|
||||
("auto", ColorAuto),
|
||||
("always", ColorAlways),
|
||||
("never", ColorNever)
|
||||
]
|
||||
|
||||
parseSeverityOption value =
|
||||
parseEnum "severity" value [
|
||||
("error", ErrorC),
|
||||
("warning", WarningC),
|
||||
("info", InfoC),
|
||||
("style", StyleC)
|
||||
]
|
||||
|
||||
parseOption flag options =
|
||||
case flag of
|
||||
@@ -237,7 +259,7 @@ parseOption flag options =
|
||||
}
|
||||
|
||||
Flag "exclude" str -> do
|
||||
new <- mapM parseNum $ split ',' str
|
||||
new <- mapM parseNum $ filter (not . null) $ split ',' str
|
||||
let old = csExcludedWarnings . checkSpec $ options
|
||||
return options {
|
||||
checkSpec = (checkSpec options) {
|
||||
@@ -254,10 +276,11 @@ parseOption flag options =
|
||||
externalSources = True
|
||||
}
|
||||
|
||||
Flag "color" color ->
|
||||
Flag "color" color -> do
|
||||
option <- parseColorOption color
|
||||
return options {
|
||||
formatterOptions = (formatterOptions options) {
|
||||
foColorOption = parseColorOption color
|
||||
foColorOption = option
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,6 +291,22 @@ parseOption flag options =
|
||||
}
|
||||
}
|
||||
|
||||
Flag "severity" severity -> do
|
||||
option <- parseSeverityOption severity
|
||||
return options {
|
||||
checkSpec = (checkSpec options) {
|
||||
csMinSeverity = option
|
||||
}
|
||||
}
|
||||
|
||||
Flag "wiki-link-count" countString -> do
|
||||
count <- parseNum countString
|
||||
return options {
|
||||
formatterOptions = (formatterOptions options) {
|
||||
foWikiLinkCount = count
|
||||
}
|
||||
}
|
||||
|
||||
_ -> return options
|
||||
where
|
||||
die s = do
|
||||
@@ -276,7 +315,7 @@ parseOption flag options =
|
||||
parseNum ('S':'C':str) = parseNum str
|
||||
parseNum num = do
|
||||
unless (all isDigit num) $ do
|
||||
printErr $ "Bad exclusion: " ++ num
|
||||
printErr $ "Invalid number: " ++ num
|
||||
throwError SyntaxFailure
|
||||
return (Prelude.read num :: Integer)
|
||||
|
||||
@@ -292,7 +331,7 @@ ioInterface options files = do
|
||||
get cache inputs file = do
|
||||
map <- readIORef cache
|
||||
case Map.lookup file map of
|
||||
Just x -> return $ Right x
|
||||
Just x -> return $ Right x
|
||||
Nothing -> fetch cache inputs file
|
||||
|
||||
fetch cache inputs file = do
|
||||
@@ -355,7 +394,7 @@ decodeString = decode
|
||||
in
|
||||
case next of
|
||||
Just (n, remainder) -> chr n : decode remainder
|
||||
Nothing -> c : decode rest
|
||||
Nothing -> c : decode rest
|
||||
|
||||
construct x 0 rest = do
|
||||
guard $ x <= 0x10FFFF
|
||||
@@ -378,4 +417,4 @@ printVersion = do
|
||||
putStrLn "ShellCheck - shell script analysis tool"
|
||||
putStrLn $ "version: " ++ shellcheckVersion
|
||||
putStrLn "license: GNU General Public License, version 3"
|
||||
putStrLn "website: http://www.shellcheck.net"
|
||||
putStrLn "website: https://www.shellcheck.net"
|
||||
|
53
snap/snapcraft.yaml
Normal file
53
snap/snapcraft.yaml
Normal file
@@ -0,0 +1,53 @@
|
||||
name: shellcheck
|
||||
summary: A shell script static analysis tool
|
||||
description: |
|
||||
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh
|
||||
shell scripts.
|
||||
|
||||
The goals of ShellCheck are
|
||||
|
||||
- To point out and clarify typical beginner's syntax issues that cause a
|
||||
shell to give cryptic error messages.
|
||||
|
||||
- To point out and clarify typical intermediate level semantic problems that
|
||||
cause a shell to behave strangely and counter-intuitively.
|
||||
|
||||
- To point out subtle caveats, corner cases and pitfalls that may cause an
|
||||
advanced user's otherwise working script to fail under future
|
||||
circumstances.
|
||||
|
||||
By default ShellCheck can only check non-hidden files under /home, to make
|
||||
ShellCheck be able to check files under /media and /run/media you must
|
||||
connect it to the `removable-media` interface manually:
|
||||
|
||||
# snap connect shellcheck:removable-media
|
||||
|
||||
version: git
|
||||
grade: devel
|
||||
confinement: strict
|
||||
|
||||
apps:
|
||||
shellcheck:
|
||||
command: usr/bin/shellcheck
|
||||
plugs: [home, removable-media]
|
||||
|
||||
parts:
|
||||
shellcheck:
|
||||
plugin: dump
|
||||
source: ./
|
||||
build-packages:
|
||||
- cabal-install
|
||||
- squid3
|
||||
build: |
|
||||
# See comments in .snapsquid.conf
|
||||
[ "$http_proxy" ] && {
|
||||
squid3 -f .snapsquid.conf
|
||||
export http_proxy="http://localhost:8888"
|
||||
sleep 3
|
||||
}
|
||||
cabal sandbox init
|
||||
cabal update || cat /var/log/squid/*
|
||||
cabal install -j
|
||||
install: |
|
||||
install -d $SNAPCRAFT_PART_INSTALL/usr/bin
|
||||
install .cabal-sandbox/bin/shellcheck $SNAPCRAFT_PART_INSTALL/usr/bin
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.AST where
|
||||
|
||||
@@ -37,8 +37,8 @@ newtype Root = Root Token
|
||||
data Token =
|
||||
TA_Binary Id String Token Token
|
||||
| TA_Assignment Id String Token Token
|
||||
| TA_Variable Id String [Token]
|
||||
| TA_Expansion Id [Token]
|
||||
| TA_Index Id Token
|
||||
| TA_Sequence Id [Token]
|
||||
| TA_Trinary Id Token Token Token
|
||||
| TA_Unary Id String Token
|
||||
@@ -134,7 +134,8 @@ data Token =
|
||||
| T_Pipe Id String
|
||||
| T_CoProc Id (Maybe String) Token
|
||||
| T_CoProcBody Id Token
|
||||
| T_Include Id Token Token -- . & source: SimpleCommand T_Script
|
||||
| T_Include Id Token
|
||||
| T_SourceCommand Id Token Token
|
||||
deriving (Show)
|
||||
|
||||
data Annotation =
|
||||
@@ -266,11 +267,12 @@ analyze f g i =
|
||||
c <- round t3
|
||||
return $ TA_Trinary id a b c
|
||||
delve (TA_Expansion id t) = dl t $ TA_Expansion id
|
||||
delve (TA_Index id t) = d1 t $ TA_Index id
|
||||
delve (TA_Variable id str t) = dl t $ TA_Variable id str
|
||||
delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns
|
||||
delve (T_CoProc id var body) = d1 body $ T_CoProc id var
|
||||
delve (T_CoProcBody id t) = d1 t $ T_CoProcBody id
|
||||
delve (T_Include id includer script) = d2 includer script $ T_Include id
|
||||
delve (T_Include id script) = d1 script $ T_Include id
|
||||
delve (T_SourceCommand id includer t_include) = d2 includer t_include $ T_SourceCommand id
|
||||
delve t = return t
|
||||
|
||||
getId :: Token -> Id
|
||||
@@ -360,7 +362,6 @@ getId t = case t of
|
||||
TA_Sequence id _ -> id
|
||||
TA_Trinary id _ _ _ -> id
|
||||
TA_Expansion id _ -> id
|
||||
TA_Index id _ -> id
|
||||
T_ProcSub id _ _ -> id
|
||||
T_Glob id _ -> id
|
||||
T_ForArithmetic id _ _ _ _ -> id
|
||||
@@ -371,9 +372,11 @@ getId t = case t of
|
||||
T_Pipe id _ -> id
|
||||
T_CoProc id _ _ -> id
|
||||
T_CoProcBody id _ -> id
|
||||
T_Include id _ _ -> id
|
||||
T_Include id _ -> id
|
||||
T_SourceCommand id _ _ -> id
|
||||
T_UnparsedIndex id _ _ -> id
|
||||
TC_Empty id _ -> id
|
||||
TA_Variable id _ _ -> id
|
||||
|
||||
blank :: Monad m => Token -> m ()
|
||||
blank = const $ return ()
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.ASTLib where
|
||||
|
||||
@@ -23,6 +23,7 @@ import ShellCheck.AST
|
||||
|
||||
import Control.Monad.Writer
|
||||
import Control.Monad
|
||||
import Data.Char
|
||||
import Data.Functor
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
@@ -112,6 +113,7 @@ getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
|
||||
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
|
||||
|
||||
-- Get all flags in a GNU way, up until --
|
||||
getAllFlags :: Token -> [(Token, String)]
|
||||
getAllFlags = getFlagsUntil (== "--")
|
||||
-- Get all flags in a BSD way, up until first non-flag argument or --
|
||||
getLeadingFlags = getFlagsUntil (\x -> x == "--" || (not $ "-" `isPrefixOf` x))
|
||||
@@ -225,8 +227,43 @@ getLiteralStringExt more = g
|
||||
g (T_SingleQuoted _ s) = return s
|
||||
g (T_Literal _ s) = return s
|
||||
g (T_ParamSubSpecialChar _ s) = return s
|
||||
g (T_DollarSingleQuoted _ s) = return $ decodeEscapes s
|
||||
g x = more x
|
||||
|
||||
-- Bash style $'..' decoding
|
||||
decodeEscapes ('\\':c:cs) =
|
||||
case c of
|
||||
'a' -> '\a' : rest
|
||||
'b' -> '\b' : rest
|
||||
'e' -> '\x1B' : rest
|
||||
'f' -> '\f' : rest
|
||||
'n' -> '\n' : rest
|
||||
'r' -> '\r' : rest
|
||||
't' -> '\t' : rest
|
||||
'v' -> '\v' : rest
|
||||
'\'' -> '\'' : rest
|
||||
'"' -> '"' : rest
|
||||
'\\' -> '\\' : rest
|
||||
'x' ->
|
||||
case cs of
|
||||
(x:y:more) ->
|
||||
if isHexDigit x && isHexDigit y
|
||||
then chr (16*(digitToInt x) + (digitToInt y)) : rest
|
||||
else '\\':c:rest
|
||||
_ | isOctDigit c ->
|
||||
let digits = take 3 $ takeWhile isOctDigit (c:cs)
|
||||
num = parseOct digits
|
||||
in (if num < 256 then chr num else '?') : rest
|
||||
_ -> '\\' : c : rest
|
||||
where
|
||||
rest = decodeEscapes cs
|
||||
parseOct = f 0
|
||||
where
|
||||
f n "" = n
|
||||
f n (c:rest) = f (n * 8 + digitToInt c) rest
|
||||
decodeEscapes (c:cs) = c : decodeEscapes cs
|
||||
decodeEscapes [] = []
|
||||
|
||||
-- Is this token a string literal?
|
||||
isLiteral t = isJust $ getLiteralString t
|
||||
|
||||
@@ -256,17 +293,27 @@ getCommand t =
|
||||
T_Annotation _ _ t -> getCommand t
|
||||
_ -> Nothing
|
||||
|
||||
-- Maybe get the command name of a token representing a command
|
||||
getCommandName t = do
|
||||
-- Maybe get the command name string of a token representing a command
|
||||
getCommandName :: Token -> Maybe String
|
||||
getCommandName = fst . getCommandNameAndToken
|
||||
|
||||
-- Get the command name token from a command, i.e.
|
||||
-- the token representing 'ls' in 'ls -la 2> foo'.
|
||||
-- If it can't be determined, return the original token.
|
||||
getCommandTokenOrThis = snd . getCommandNameAndToken
|
||||
|
||||
getCommandNameAndToken :: Token -> (Maybe String, Token)
|
||||
getCommandNameAndToken t = fromMaybe (Nothing, t) $ do
|
||||
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
|
||||
s <- getLiteralString w
|
||||
if "busybox" `isSuffixOf` s
|
||||
if "busybox" `isSuffixOf` s || "builtin" == s
|
||||
then
|
||||
case rest of
|
||||
(applet:_) -> getLiteralString applet
|
||||
_ -> return s
|
||||
(applet:_) -> return (getLiteralString applet, applet)
|
||||
_ -> return (Just s, w)
|
||||
else
|
||||
return s
|
||||
return (Just s, w)
|
||||
|
||||
|
||||
-- If a command substitution is a single command, get its name.
|
||||
-- $(date +%s) = Just "date"
|
||||
@@ -434,3 +481,12 @@ pseudoGlobIsSuperSetof = matchable
|
||||
|
||||
wordsCanBeEqual x y = fromMaybe True $
|
||||
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
|
||||
|
||||
-- Is this an expansion that can be quoted,
|
||||
-- e.g. $(foo) `foo` $foo (but not {foo,})?
|
||||
isQuoteableExpansion t = case t of
|
||||
T_DollarExpansion {} -> True
|
||||
T_DollarBraceCommandExpansion {} -> True
|
||||
T_Backticked {} -> True
|
||||
T_DollarBraced {} -> True
|
||||
_ -> False
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,9 +15,10 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE TemplateHaskell, FlexibleContexts #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
module ShellCheck.Analytics (runAnalytics, ShellCheck.Analytics.runTests) where
|
||||
|
||||
import ShellCheck.AST
|
||||
@@ -119,7 +120,6 @@ nodeChecks = [
|
||||
,checkGlobbedRegex
|
||||
,checkTestRedirects
|
||||
,checkIndirectExpansion
|
||||
,checkSudoRedirect
|
||||
,checkPS1Assignments
|
||||
,checkBackticks
|
||||
,checkInexplicablyUnquoted
|
||||
@@ -166,6 +166,10 @@ nodeChecks = [
|
||||
,checkFlagAsCommand
|
||||
,checkEmptyCondition
|
||||
,checkPipeToNowhere
|
||||
,checkForLoopGlobVariables
|
||||
,checkSubshelledTests
|
||||
,checkInvertedStringTest
|
||||
,checkRedirectionToCommand
|
||||
]
|
||||
|
||||
|
||||
@@ -394,7 +398,7 @@ checkPipePitfalls _ (T_Pipeline id _ commands) = do
|
||||
mapM_ (f . (\ n -> take (length l) $ drop n commands)) indices
|
||||
return . not . null $ indices
|
||||
for' l f = for l (first f)
|
||||
first func (x:_) = func (getId x)
|
||||
first func (x:_) = func (getId $ getCommandTokenOrThis x)
|
||||
first _ _ = return ()
|
||||
hasShortParameter char = any (\x -> "-" `isPrefixOf` x && char `elem` x)
|
||||
hasParameter string =
|
||||
@@ -428,17 +432,22 @@ prop_checkShebang4 = verifyNotTree checkShebang "#shellcheck shell=sh\nfoo"
|
||||
prop_checkShebang5 = verifyTree checkShebang "#!/usr/bin/env ash"
|
||||
prop_checkShebang6 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=dash\n"
|
||||
prop_checkShebang7 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=sh\n"
|
||||
prop_checkShebang8 = verifyTree checkShebang "#!bin/sh\ntrue"
|
||||
prop_checkShebang9 = verifyNotTree checkShebang "# shellcheck shell=sh\ntrue"
|
||||
prop_checkShebang10= verifyNotTree checkShebang "#!foo\n# shellcheck shell=sh ignore=SC2239\ntrue"
|
||||
checkShebang params (T_Annotation _ list t) =
|
||||
if any isOverride list then [] else checkShebang params t
|
||||
where
|
||||
isOverride (ShellOverride _) = True
|
||||
isOverride _ = False
|
||||
checkShebang params (T_Script id sb _) = execWriter $
|
||||
checkShebang params (T_Script id sb _) = execWriter $ do
|
||||
unless (shellTypeSpecified params) $ do
|
||||
when (sb == "") $
|
||||
err id 2148 "Tips depend on target shell and yours is unknown. Add a shebang."
|
||||
when (executableFromShebang sb == "ash") $
|
||||
warn id 2187 "Ash scripts will be checked as Dash. Add '# shellcheck shell=dash' to silence."
|
||||
unless (null sb || "/" `isPrefixOf` sb) $
|
||||
err id 2239 "Ensure the shebang uses an absolute path to the interpreter."
|
||||
|
||||
|
||||
prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done"
|
||||
@@ -572,6 +581,7 @@ prop_checkRedirectToSame4 = verifyNot checkRedirectToSame "foo /dev/null > /dev/
|
||||
prop_checkRedirectToSame5 = verifyNot checkRedirectToSame "foo > bar 2> bar"
|
||||
prop_checkRedirectToSame6 = verifyNot checkRedirectToSame "echo foo > foo"
|
||||
prop_checkRedirectToSame7 = verifyNot checkRedirectToSame "sed 's/foo/bar/g' file | sponge file"
|
||||
prop_checkRedirectToSame8 = verifyNot checkRedirectToSame "while read -r line; do _=\"$fname\"; done <\"$fname\""
|
||||
checkRedirectToSame params s@(T_Pipeline _ _ list) =
|
||||
mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurrences x) l) (getAllRedirs list))) list
|
||||
where
|
||||
@@ -582,7 +592,8 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) =
|
||||
&& x == y
|
||||
&& not (isOutput t && isOutput u)
|
||||
&& not (special t)
|
||||
&& not (any isHarmlessCommand [t,u])) $ do
|
||||
&& not (any isHarmlessCommand [t,u])
|
||||
&& not (any containsAssignment [u])) $ do
|
||||
addComment $ note newId
|
||||
addComment $ note exceptId
|
||||
checkOccurrences _ _ = return ()
|
||||
@@ -609,6 +620,9 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) =
|
||||
cmd <- getClosestCommand (parentMap params) arg
|
||||
name <- getCommandBasename cmd
|
||||
return $ name `elem` ["echo", "printf", "sponge"]
|
||||
containsAssignment arg = fromMaybe False $ do
|
||||
cmd <- getClosestCommand (parentMap params) arg
|
||||
return $ isAssignment cmd
|
||||
|
||||
checkRedirectToSame _ _ = return ()
|
||||
|
||||
@@ -702,6 +716,7 @@ prop_checkArrayWithoutIndex5 = verifyTree checkArrayWithoutIndex "a[0]=foo; echo
|
||||
prop_checkArrayWithoutIndex6 = verifyTree checkArrayWithoutIndex "echo $PIPESTATUS"
|
||||
prop_checkArrayWithoutIndex7 = verifyTree checkArrayWithoutIndex "a=(a b); a+=c"
|
||||
prop_checkArrayWithoutIndex8 = verifyTree checkArrayWithoutIndex "declare -a foo; foo=bar;"
|
||||
prop_checkArrayWithoutIndex9 = verifyTree checkArrayWithoutIndex "read -r -a arr <<< 'foo bar'; echo \"$arr\""
|
||||
checkArrayWithoutIndex params _ =
|
||||
doVariableFlowAnalysis readF writeF defaultMap (variableFlow params)
|
||||
where
|
||||
@@ -742,6 +757,7 @@ prop_checkStderrRedirect3 = verifyNot checkStderrRedirect "test 2>&1 > file | gr
|
||||
prop_checkStderrRedirect4 = verifyNot checkStderrRedirect "errors=$(test 2>&1 > file)"
|
||||
prop_checkStderrRedirect5 = verifyNot checkStderrRedirect "read < <(test 2>&1 > file)"
|
||||
prop_checkStderrRedirect6 = verify checkStderrRedirect "foo | bar 2>&1 > /dev/null"
|
||||
prop_checkStderrRedirect7 = verifyNot checkStderrRedirect "{ cmd > file; } 2>&1"
|
||||
checkStderrRedirect params redir@(T_Redirecting _ [
|
||||
T_FdRedirect id "2" (T_IoDuplicate _ (T_GREATAND _) "1"),
|
||||
T_FdRedirect _ _ (T_IoFile _ op _)
|
||||
@@ -760,7 +776,7 @@ checkStderrRedirect params redir@(T_Redirecting _ [
|
||||
isCaptured = any usesOutput $ getPath (parentMap params) redir
|
||||
|
||||
error = unless isCaptured $
|
||||
err id 2069 "The order of the 2>&1 and the redirect matters. The 2>&1 has to be last."
|
||||
warn id 2069 "To redirect stdout+stderr, 2>&1 must be last (or use '{ cmd > file; } 2>&1' to clarify)."
|
||||
|
||||
checkStderrRedirect _ _ = return ()
|
||||
|
||||
@@ -786,6 +802,10 @@ prop_checkSingleQuotedVariables11= verifyNot checkSingleQuotedVariables "sed '${
|
||||
prop_checkSingleQuotedVariables12= verifyNot checkSingleQuotedVariables "eval 'echo $1'"
|
||||
prop_checkSingleQuotedVariables13= verifyNot checkSingleQuotedVariables "busybox awk '{print $1}'"
|
||||
prop_checkSingleQuotedVariables14= verifyNot checkSingleQuotedVariables "[ -v 'bar[$foo]' ]"
|
||||
prop_checkSingleQuotedVariables15= verifyNot checkSingleQuotedVariables "git filter-branch 'test $GIT_COMMIT'"
|
||||
prop_checkSingleQuotedVariables16= verify checkSingleQuotedVariables "git '$a'"
|
||||
prop_checkSingleQuotedVariables17= verifyNot checkSingleQuotedVariables "rename 's/(.)a/$1/g' *"
|
||||
|
||||
checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
||||
when (s `matches` re) $
|
||||
if "sed" == commandName
|
||||
@@ -798,7 +818,7 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
||||
commandName = fromMaybe "" $ do
|
||||
cmd <- getClosestCommand parents t
|
||||
name <- getCommandBasename cmd
|
||||
return $ if name == "find" then getFindCommand cmd else name
|
||||
return $ if name == "find" then getFindCommand cmd else if name == "git" then getGitCommand cmd else name
|
||||
|
||||
isProbablyOk =
|
||||
any isOkAssignment (take 3 $ getPath parents t)
|
||||
@@ -813,8 +833,12 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
||||
,"xprop"
|
||||
,"alias"
|
||||
,"sudo" -- covering "sudo sh" and such
|
||||
,"docker" -- like above
|
||||
,"dpkg-query"
|
||||
,"jq" -- could also check that user provides --arg
|
||||
,"rename"
|
||||
,"unset"
|
||||
,"git filter-branch"
|
||||
]
|
||||
|| "awk" `isSuffixOf` commandName
|
||||
|| "perl" `isPrefixOf` commandName
|
||||
@@ -838,6 +862,12 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
||||
_ -> "find"
|
||||
getFindCommand (T_Redirecting _ _ cmd) = getFindCommand cmd
|
||||
getFindCommand _ = "find"
|
||||
getGitCommand (T_SimpleCommand _ _ words) =
|
||||
case map getLiteralString words of
|
||||
Just "git":Just "filter-branch":_ -> "git filter-branch"
|
||||
_ -> "git"
|
||||
getGitCommand (T_Redirecting _ _ cmd) = getGitCommand cmd
|
||||
getGitCommand _ = "git"
|
||||
checkSingleQuotedVariables _ _ = return ()
|
||||
|
||||
|
||||
@@ -845,6 +875,7 @@ prop_checkUnquotedN = verify checkUnquotedN "if [ -n $foo ]; then echo cow; fi"
|
||||
prop_checkUnquotedN2 = verify checkUnquotedN "[ -n $cow ]"
|
||||
prop_checkUnquotedN3 = verifyNot checkUnquotedN "[[ -n $foo ]] && echo cow"
|
||||
prop_checkUnquotedN4 = verify checkUnquotedN "[ -n $cow -o -t 1 ]"
|
||||
prop_checkUnquotedN5 = verifyNot checkUnquotedN "[ -n \"$@\" ]"
|
||||
checkUnquotedN _ (TC_Unary _ SingleBracket "-n" (T_NormalWord id [t])) | willSplit t =
|
||||
err id 2070 "-n doesn't work with unquoted arguments. Quote or use [[ ]]."
|
||||
checkUnquotedN _ _ = return ()
|
||||
@@ -996,7 +1027,7 @@ checkQuotedCondRegex _ (TC_Binary _ _ "=~" _ rhs) =
|
||||
error t =
|
||||
unless (isConstantNonRe t) $
|
||||
err (getId t) 2076
|
||||
"Don't quote rhs of =~, it'll match literally rather than as a regex."
|
||||
"Don't quote right-hand side of =~, it'll match literally rather than as a regex."
|
||||
re = mkRegex "[][*.+()|]"
|
||||
hasMetachars s = s `matches` re
|
||||
isConstantNonRe t = fromMaybe False $ do
|
||||
@@ -1006,13 +1037,16 @@ checkQuotedCondRegex _ _ = return ()
|
||||
|
||||
prop_checkGlobbedRegex1 = verify checkGlobbedRegex "[[ $foo =~ *foo* ]]"
|
||||
prop_checkGlobbedRegex2 = verify checkGlobbedRegex "[[ $foo =~ f* ]]"
|
||||
prop_checkGlobbedRegex2a = verify checkGlobbedRegex "[[ $foo =~ \\#* ]]"
|
||||
prop_checkGlobbedRegex3 = verifyNot checkGlobbedRegex "[[ $foo =~ $foo ]]"
|
||||
prop_checkGlobbedRegex4 = verifyNot checkGlobbedRegex "[[ $foo =~ ^c.* ]]"
|
||||
prop_checkGlobbedRegex5 = verifyNot checkGlobbedRegex "[[ $foo =~ \\* ]]"
|
||||
prop_checkGlobbedRegex6 = verifyNot checkGlobbedRegex "[[ $foo =~ (o*) ]]"
|
||||
prop_checkGlobbedRegex7 = verifyNot checkGlobbedRegex "[[ $foo =~ \\*foo ]]"
|
||||
prop_checkGlobbedRegex8 = verifyNot checkGlobbedRegex "[[ $foo =~ x\\* ]]"
|
||||
checkGlobbedRegex _ (TC_Binary _ DoubleBracket "=~" _ rhs) =
|
||||
let s = concat $ oversimplify rhs in
|
||||
when (isConfusedGlobRegex s) $
|
||||
warn (getId rhs) 2049 "=~ is for regex. Use == for globs."
|
||||
warn (getId rhs) 2049 "=~ is for regex, but this looks like a glob. Use = instead."
|
||||
checkGlobbedRegex _ _ = return ()
|
||||
|
||||
|
||||
@@ -1140,12 +1174,10 @@ checkArithmeticDeref params t@(TA_Expansion _ [b@(T_DollarBraced id _)]) =
|
||||
T_Arithmetic {} -> return normalWarning
|
||||
T_DollarArithmetic {} -> return normalWarning
|
||||
T_ForArithmetic {} -> return normalWarning
|
||||
TA_Index {} -> return indexWarning
|
||||
T_SimpleCommand {} -> return noWarning
|
||||
_ -> Nothing
|
||||
|
||||
normalWarning = style id 2004 "$/${} is unnecessary on arithmetic variables."
|
||||
indexWarning = style id 2149 "Remove $/${} for numeric index, or escape it for string."
|
||||
noWarning = return ()
|
||||
checkArithmeticDeref _ _ = return ()
|
||||
|
||||
@@ -1165,11 +1197,12 @@ prop_checkComparisonAgainstGlob2 = verifyNot checkComparisonAgainstGlob "[[ $cow
|
||||
prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = *foo* ]"
|
||||
prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]"
|
||||
prop_checkComparisonAgainstGlob5 = verify checkComparisonAgainstGlob "[[ $cow != $bar ]]"
|
||||
prop_checkComparisonAgainstGlob6 = verify checkComparisonAgainstGlob "[ $f != /* ]"
|
||||
checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _]))
|
||||
| op `elem` ["=", "==", "!="] =
|
||||
warn id 2053 $ "Quote the rhs of " ++ op ++ " in [[ ]] to prevent glob matching."
|
||||
warn id 2053 $ "Quote the right-hand side of " ++ op ++ " in [[ ]] to prevent glob matching."
|
||||
checkComparisonAgainstGlob _ (TC_Binary _ SingleBracket op _ word)
|
||||
| (op == "=" || op == "==") && isGlob word =
|
||||
| op `elem` ["=", "==", "!="] && isGlob word =
|
||||
err (getId word) 2081 "[ .. ] can't match globs. Use [[ .. ]] or case statement."
|
||||
checkComparisonAgainstGlob _ _ = return ()
|
||||
|
||||
@@ -1276,33 +1309,6 @@ checkTestRedirects _ (T_Redirecting id redirs cmd) | cmd `isCommand` "test" =
|
||||
_ -> False
|
||||
checkTestRedirects _ _ = return ()
|
||||
|
||||
prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
|
||||
prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
|
||||
prop_checkSudoRedirect3 = verify checkSudoRedirect "sudo cmd >> file"
|
||||
prop_checkSudoRedirect4 = verify checkSudoRedirect "sudo cmd &> file"
|
||||
prop_checkSudoRedirect5 = verifyNot checkSudoRedirect "sudo cmd 2>&1"
|
||||
prop_checkSudoRedirect6 = verifyNot checkSudoRedirect "sudo cmd 2> log"
|
||||
prop_checkSudoRedirect7 = verifyNot checkSudoRedirect "sudo cmd > /dev/null 2>&1"
|
||||
checkSudoRedirect _ (T_Redirecting _ redirs cmd) | cmd `isCommand` "sudo" =
|
||||
mapM_ warnAbout redirs
|
||||
where
|
||||
warnAbout (T_FdRedirect _ s (T_IoFile id op file))
|
||||
| (s == "" || s == "&") && not (special file) =
|
||||
case op of
|
||||
T_Less _ ->
|
||||
info (getId op) 2024
|
||||
"sudo doesn't affect redirects. Use sudo cat file | .."
|
||||
T_Greater _ ->
|
||||
warn (getId op) 2024
|
||||
"sudo doesn't affect redirects. Use ..| sudo tee file"
|
||||
T_DGREAT _ ->
|
||||
warn (getId op) 2024
|
||||
"sudo doesn't affect redirects. Use .. | sudo tee -a file"
|
||||
_ -> return ()
|
||||
warnAbout _ = return ()
|
||||
special file = concat (oversimplify file) == "/dev/null"
|
||||
checkSudoRedirect _ _ = return ()
|
||||
|
||||
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
|
||||
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
|
||||
prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
|
||||
@@ -1330,7 +1336,7 @@ prop_checkBackticks1 = verify checkBackticks "echo `foo`"
|
||||
prop_checkBackticks2 = verifyNot checkBackticks "echo $(foo)"
|
||||
prop_checkBackticks3 = verifyNot checkBackticks "echo `#inlined comment` foo"
|
||||
checkBackticks _ (T_Backticked id list) | not (null list) =
|
||||
style id 2006 "Use $(..) instead of legacy `..`."
|
||||
style id 2006 "Use $(...) notation instead of legacy backticked `...`."
|
||||
checkBackticks _ _ = return ()
|
||||
|
||||
prop_checkIndirectExpansion1 = verify checkIndirectExpansion "${foo$n}"
|
||||
@@ -1426,6 +1432,7 @@ prop_checkSpuriousExec4 = verifyNot checkSpuriousExec "if a; then exec b; fi"
|
||||
prop_checkSpuriousExec5 = verifyNot checkSpuriousExec "exec > file; cmd"
|
||||
prop_checkSpuriousExec6 = verify checkSpuriousExec "exec foo > file; cmd"
|
||||
prop_checkSpuriousExec7 = verifyNot checkSpuriousExec "exec file; echo failed; exit 3"
|
||||
prop_checkSpuriousExec8 = verifyNot checkSpuriousExec "exec {origout}>&1- >tmp.log 2>&1; bar"
|
||||
checkSpuriousExec _ = doLists
|
||||
where
|
||||
doLists (T_Script _ _ cmds) = doList cmds
|
||||
@@ -1603,6 +1610,7 @@ prop_checkSpacefulness32= verifyNotTree checkSpacefulness "var=$1; [ -v var ]"
|
||||
prop_checkSpacefulness33= verifyTree checkSpacefulness "for file; do echo $file; done"
|
||||
prop_checkSpacefulness34= verifyTree checkSpacefulness "declare foo$n=$1"
|
||||
prop_checkSpacefulness35= verifyNotTree checkSpacefulness "echo ${1+\"$1\"}"
|
||||
prop_checkSpacefulness36= verifyNotTree checkSpacefulness "arg=$#; echo $arg"
|
||||
|
||||
checkSpacefulness params t =
|
||||
doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
|
||||
@@ -1617,16 +1625,23 @@ checkSpacefulness params t =
|
||||
modify $ Map.insert name bool
|
||||
|
||||
readF _ token name = do
|
||||
spaced <- hasSpaces name
|
||||
return [makeComment InfoC (getId token) 2086 warning |
|
||||
isExpansion token && spaced
|
||||
spaces <- hasSpaces name
|
||||
return [warning |
|
||||
isExpansion token && spaces
|
||||
&& not (isArrayExpansion token) -- There's another warning for this
|
||||
&& not (isCountingReference token)
|
||||
&& not (isQuoteFree parents token)
|
||||
&& not (isQuotedAlternativeReference token)
|
||||
&& not (usedAsCommandName parents token)]
|
||||
where
|
||||
warning = "Double quote to prevent globbing and word splitting."
|
||||
warning =
|
||||
if isDefaultAssignment (parentMap params) token
|
||||
then
|
||||
makeComment InfoC (getId token) 2223
|
||||
"This default assignment may cause DoS due to globbing. Quote it."
|
||||
else
|
||||
makeComment InfoC (getId token) 2086
|
||||
"Double quote to prevent globbing and word splitting."
|
||||
|
||||
writeF _ _ name (DataString SourceExternal) = setSpaces name True >> return []
|
||||
writeF _ _ name (DataString SourceInteger) = setSpaces name False >> return []
|
||||
@@ -1665,6 +1680,12 @@ checkSpacefulness params t =
|
||||
globspace = "*?[] \t\n"
|
||||
containsAny s = any (`elem` s)
|
||||
|
||||
isDefaultAssignment parents token =
|
||||
let modifier = getBracedModifier $ bracedString token in
|
||||
isExpansion token
|
||||
&& any (`isPrefixOf` modifier) ["=", ":="]
|
||||
&& isParamTo parents ":" token
|
||||
|
||||
prop_checkQuotesInLiterals1 = verifyTree checkQuotesInLiterals "param='--foo=\"bar\"'; app $param"
|
||||
prop_checkQuotesInLiterals1a= verifyTree checkQuotesInLiterals "param=\"--foo='lolbar'\"; app $param"
|
||||
prop_checkQuotesInLiterals2 = verifyNotTree checkQuotesInLiterals "param='--foo=\"bar\"'; app \"$param\""
|
||||
@@ -1811,6 +1832,9 @@ prop_checkUnused34= verifyNotTree checkUnusedAssignments "foo=1; (( t = foo ));
|
||||
prop_checkUnused35= verifyNotTree checkUnusedAssignments "a=foo; b=2; echo ${a:b}"
|
||||
prop_checkUnused36= verifyNotTree checkUnusedAssignments "if [[ -v foo ]]; then true; fi"
|
||||
prop_checkUnused37= verifyNotTree checkUnusedAssignments "fd=2; exec {fd}>&-"
|
||||
prop_checkUnused38= verifyTree checkUnusedAssignments "(( a=42 ))"
|
||||
prop_checkUnused39= verifyNotTree checkUnusedAssignments "declare -x -f foo"
|
||||
prop_checkUnused40= verifyNotTree checkUnusedAssignments "arr=(1 2); num=2; echo \"${arr[@]:num}\""
|
||||
checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
|
||||
where
|
||||
flow = variableFlow params
|
||||
@@ -1828,7 +1852,7 @@ checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
|
||||
|
||||
warnFor (name, token) =
|
||||
warn (getId token) 2034 $
|
||||
name ++ " appears unused. Verify it or export it."
|
||||
name ++ " appears unused. Verify use (or export if used externally)."
|
||||
|
||||
stripSuffix = takeWhile isVariableChar
|
||||
defaultMap = Map.fromList $ zip internalVariables $ repeat ()
|
||||
@@ -1865,6 +1889,10 @@ prop_checkUnassignedReferences29= verifyNotTree checkUnassignedReferences "if [[
|
||||
prop_checkUnassignedReferences30= verifyNotTree checkUnassignedReferences "if [[ -v foo[3] ]]; then echo ${foo[3]}; fi"
|
||||
prop_checkUnassignedReferences31= verifyNotTree checkUnassignedReferences "X=1; if [[ -v foo[$X+42] ]]; then echo ${foo[$X+42]}; fi"
|
||||
prop_checkUnassignedReferences32= verifyNotTree checkUnassignedReferences "if [[ -v \"foo[1]\" ]]; then echo ${foo[@]}; fi"
|
||||
prop_checkUnassignedReferences33= verifyNotTree checkUnassignedReferences "f() { local -A foo; echo \"${foo[@]}\"; }"
|
||||
prop_checkUnassignedReferences34= verifyNotTree checkUnassignedReferences "declare -A foo; (( foo[bar] ))"
|
||||
prop_checkUnassignedReferences35= verifyNotTree checkUnassignedReferences "echo ${arr[foo-bar]:?fail}"
|
||||
prop_checkUnassignedReferences36= verifyNotTree checkUnassignedReferences "read -a foo -r <<<\"foo bar\"; echo \"$foo\""
|
||||
checkUnassignedReferences params t = warnings
|
||||
where
|
||||
(readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)
|
||||
@@ -1924,11 +1952,13 @@ checkUnassignedReferences params t = warnings
|
||||
isArray _ = False
|
||||
|
||||
isGuarded (T_DollarBraced _ v) =
|
||||
any (`isPrefixOf` rest) ["-", ":-", "?", ":?"]
|
||||
rest `matches` guardRegex
|
||||
where
|
||||
name = concat $ oversimplify v
|
||||
rest = dropWhile isVariableChar $ dropWhile (`elem` "#!") name
|
||||
isGuarded _ = False
|
||||
-- :? or :- with optional array index and colon
|
||||
guardRegex = mkRegex "^(\\[.*\\])?:?[-?]"
|
||||
|
||||
match var candidate =
|
||||
if var /= candidate && map toLower var == map toLower candidate
|
||||
@@ -2053,6 +2083,7 @@ prop_checkCdAndBack1 = verify checkCdAndBack "for f in *; do cd $f; git pull; cd
|
||||
prop_checkCdAndBack2 = verifyNot checkCdAndBack "for f in *; do cd $f || continue; git pull; cd ..; done"
|
||||
prop_checkCdAndBack3 = verifyNot checkCdAndBack "while [[ $PWD != / ]]; do cd ..; done"
|
||||
prop_checkCdAndBack4 = verify checkCdAndBack "cd $tmp; foo; cd -"
|
||||
prop_checkCdAndBack5 = verifyNot checkCdAndBack "cd ..; foo; cd .."
|
||||
checkCdAndBack params = doLists
|
||||
where
|
||||
shell = shellType params
|
||||
@@ -2075,10 +2106,20 @@ checkCdAndBack params = doLists
|
||||
getCmd (T_Pipeline id _ [x]) = getCommandName x
|
||||
getCmd _ = Nothing
|
||||
|
||||
findCdPair list =
|
||||
case list of
|
||||
(a:b:rest) ->
|
||||
if isCdRevert b && not (isCdRevert a)
|
||||
then return $ getId b
|
||||
else findCdPair (b:rest)
|
||||
_ -> Nothing
|
||||
|
||||
|
||||
doList list =
|
||||
let cds = filter ((== Just "cd") . getCmd) list in
|
||||
when (length cds >= 2 && isCdRevert (last cds)) $
|
||||
info (getId $ last cds) 2103 message
|
||||
potentially $ do
|
||||
cd <- findCdPair cds
|
||||
return $ info cd 2103 message
|
||||
|
||||
message = "Use a ( subshell ) to avoid having to cd back."
|
||||
|
||||
@@ -2456,7 +2497,7 @@ prop_checkReadWithoutR1 = verify checkReadWithoutR "read -a foo"
|
||||
prop_checkReadWithoutR2 = verifyNot checkReadWithoutR "read -ar foo"
|
||||
checkReadWithoutR _ t@T_SimpleCommand {} | t `isUnqualifiedCommand` "read" =
|
||||
unless ("r" `elem` map snd (getAllFlags t)) $
|
||||
info (getId t) 2162 "read without -r will mangle backslashes."
|
||||
info (getId $ getCommandTokenOrThis t) 2162 "read without -r will mangle backslashes."
|
||||
checkReadWithoutR _ _ = return ()
|
||||
|
||||
prop_checkUncheckedCd1 = verifyTree checkUncheckedCdPushdPopd "cd ~/src; rm -r foo"
|
||||
@@ -2467,6 +2508,7 @@ prop_checkUncheckedCd5 = verifyTree checkUncheckedCdPushdPopd "if true; then cd
|
||||
prop_checkUncheckedCd6 = verifyNotTree checkUncheckedCdPushdPopd "cd .."
|
||||
prop_checkUncheckedCd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\ncd foo\nrm bar"
|
||||
prop_checkUncheckedCd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; cd foo; rm bar"
|
||||
prop_checkUncheckedCd9 = verifyTree checkUncheckedCdPushdPopd "builtin cd ~/src; rm -r foo"
|
||||
prop_checkUncheckedPushd1 = verifyTree checkUncheckedCdPushdPopd "pushd ~/src; rm -r foo"
|
||||
prop_checkUncheckedPushd2 = verifyNotTree checkUncheckedCdPushdPopd "pushd ~/src || exit; rm -r foo"
|
||||
prop_checkUncheckedPushd3 = verifyNotTree checkUncheckedCdPushdPopd "set -e; pushd ~/src; rm -r foo"
|
||||
@@ -2484,6 +2526,7 @@ prop_checkUncheckedPopd5 = verifyTree checkUncheckedCdPushdPopd "if true; then p
|
||||
prop_checkUncheckedPopd6 = verifyTree checkUncheckedCdPushdPopd "popd"
|
||||
prop_checkUncheckedPopd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\npopd\nrm bar"
|
||||
prop_checkUncheckedPopd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; popd; rm bar"
|
||||
prop_checkUncheckedPopd9 = verifyNotTree checkUncheckedCdPushdPopd "popd -n foo"
|
||||
|
||||
checkUncheckedCdPushdPopd params root =
|
||||
if hasSetE params then
|
||||
@@ -2493,7 +2536,7 @@ checkUncheckedCdPushdPopd params root =
|
||||
checkElement t@T_SimpleCommand {} =
|
||||
when(name t `elem` ["cd", "pushd", "popd"]
|
||||
&& not (isSafeDir t)
|
||||
&& not (name t == "pushd" && ("n" `elem` map snd (getAllFlags t)))
|
||||
&& not (name t `elem` ["pushd", "popd"] && ("n" `elem` map snd (getAllFlags t)))
|
||||
&& not (isCondition $ getPath (parentMap params) t)) $
|
||||
warn (getId t) 2164 "Use 'cd ... || exit' or 'cd ... || return' in case cd fails."
|
||||
checkElement _ = return ()
|
||||
@@ -2525,7 +2568,7 @@ checkLoopVariableReassignment params token =
|
||||
T_ForArithmetic _
|
||||
(TA_Sequence _
|
||||
[TA_Assignment _ "="
|
||||
(TA_Expansion _ [T_Literal _ var]) _])
|
||||
(TA_Variable _ var _ ) _])
|
||||
_ _ _ -> return var
|
||||
_ -> fail "not loop"
|
||||
|
||||
@@ -2668,26 +2711,31 @@ prop_checkUnmatchableCases5 = verify checkUnmatchableCases "case $f in *.txt) tr
|
||||
prop_checkUnmatchableCases6 = verifyNot checkUnmatchableCases "case $f in ?*) true;; *) false;; esac"
|
||||
prop_checkUnmatchableCases7 = verifyNot checkUnmatchableCases "case $f in $(x)) true;; asdf) false;; esac"
|
||||
prop_checkUnmatchableCases8 = verify checkUnmatchableCases "case $f in cow) true;; bar|cow) false;; esac"
|
||||
prop_checkUnmatchableCases9 = verifyNot checkUnmatchableCases "case $f in x) true;;& x) false;; esac"
|
||||
checkUnmatchableCases _ t =
|
||||
case t of
|
||||
T_CaseExpression _ word list -> do
|
||||
let patterns = concatMap snd3 list
|
||||
-- Check all patterns for whether they can ever match
|
||||
let allpatterns = concatMap snd3 list
|
||||
-- Check only the non-fallthrough branches for shadowing
|
||||
let breakpatterns = concatMap snd3 $ filter (\x -> fst3 x == CaseBreak) list
|
||||
|
||||
if isConstant word
|
||||
then warn (getId word) 2194
|
||||
"This word is constant. Did you forget the $ on a variable?"
|
||||
else potentially $ do
|
||||
pg <- wordToPseudoGlob word
|
||||
return $ mapM_ (check pg) patterns
|
||||
return $ mapM_ (check pg) allpatterns
|
||||
|
||||
let exactGlobs = tupMap wordToExactPseudoGlob patterns
|
||||
let fuzzyGlobs = tupMap wordToPseudoGlob patterns
|
||||
let exactGlobs = tupMap wordToExactPseudoGlob breakpatterns
|
||||
let fuzzyGlobs = tupMap wordToPseudoGlob breakpatterns
|
||||
let dominators = zip exactGlobs (tails $ drop 1 fuzzyGlobs)
|
||||
|
||||
mapM_ checkDoms dominators
|
||||
|
||||
_ -> return ()
|
||||
where
|
||||
fst3 (x,_,_) = x
|
||||
snd3 (_,x,_) = x
|
||||
check target candidate = potentially $ do
|
||||
candidateGlob <- wordToPseudoGlob candidate
|
||||
@@ -2819,6 +2867,8 @@ prop_checkPipeToNowhere4 = verify checkPipeToNowhere "printf 'Lol' << eof\nlol\n
|
||||
prop_checkPipeToNowhere5 = verifyNot checkPipeToNowhere "echo foo | xargs du"
|
||||
prop_checkPipeToNowhere6 = verifyNot checkPipeToNowhere "ls | echo $(cat)"
|
||||
prop_checkPipeToNowhere7 = verifyNot checkPipeToNowhere "echo foo | var=$(cat) ls"
|
||||
prop_checkPipeToNowhere8 = verify checkPipeToNowhere "foo | true"
|
||||
prop_checkPipeToNowhere9 = verifyNot checkPipeToNowhere "mv -i f . < /dev/stdin"
|
||||
checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity ()
|
||||
checkPipeToNowhere _ t =
|
||||
case t of
|
||||
@@ -2831,15 +2881,25 @@ checkPipeToNowhere _ t =
|
||||
name <- getCommandBasename cmd
|
||||
guard $ name `elem` nonReadingCommands
|
||||
guard . not $ hasAdditionalConsumers cmd
|
||||
-- Confusing echo for cat is so common that it's worth a special case
|
||||
let suggestion =
|
||||
if name == "echo"
|
||||
then "Did you want 'cat' instead?"
|
||||
else "Wrong command or missing xargs?"
|
||||
return $ warn (getId cmd) 2216 $
|
||||
"Piping to '" ++ name ++ "', a command that doesn't read stdin. Wrong command or missing xargs?"
|
||||
"Piping to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion
|
||||
|
||||
checkRedir cmd = potentially $ do
|
||||
name <- getCommandBasename cmd
|
||||
guard $ name `elem` nonReadingCommands
|
||||
guard . not $ hasAdditionalConsumers cmd
|
||||
guard . not $ name `elem` ["cp", "mv", "rm"] && cmd `hasFlag` "i"
|
||||
let suggestion =
|
||||
if name == "echo"
|
||||
then "Did you want 'cat' instead?"
|
||||
else "Bad quoting, wrong command or missing xargs?"
|
||||
return $ warn (getId cmd) 2217 $
|
||||
"Redirecting to '" ++ name ++ "', a command that doesn't read stdin. Bad quoting or missing xargs?"
|
||||
"Redirecting to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion
|
||||
|
||||
-- Could any words in a SimpleCommand consume stdin (e.g. echo "$(cat)")?
|
||||
hasAdditionalConsumers t = fromMaybe True $ do
|
||||
@@ -2891,5 +2951,113 @@ checkUseBeforeDefinition _ t =
|
||||
then [x]
|
||||
else concatMap recursiveSequences list
|
||||
|
||||
prop_checkForLoopGlobVariables1 = verify checkForLoopGlobVariables "for i in $var/*.txt; do true; done"
|
||||
prop_checkForLoopGlobVariables2 = verifyNot checkForLoopGlobVariables "for i in \"$var\"/*.txt; do true; done"
|
||||
prop_checkForLoopGlobVariables3 = verifyNot checkForLoopGlobVariables "for i in $var; do true; done"
|
||||
checkForLoopGlobVariables _ t =
|
||||
case t of
|
||||
T_ForIn _ _ words _ -> mapM_ check words
|
||||
_ -> return ()
|
||||
where
|
||||
check (T_NormalWord _ parts) =
|
||||
when (any isGlob parts) $
|
||||
mapM_ suggest $ filter isQuoteableExpansion parts
|
||||
suggest t = info (getId t) 2231
|
||||
"Quote expansions in this for loop glob to prevent wordsplitting, e.g. \"$dir\"/*.txt ."
|
||||
|
||||
prop_checkSubshelledTests1 = verify checkSubshelledTests "a && ( [ b ] || ! [ c ] )"
|
||||
prop_checkSubshelledTests2 = verify checkSubshelledTests "( [ a ] )"
|
||||
prop_checkSubshelledTests3 = verify checkSubshelledTests "( [ a ] && [ b ] || test c )"
|
||||
prop_checkSubshelledTests4 = verify checkSubshelledTests "( [ a ] && { [ b ] && [ c ]; } )"
|
||||
checkSubshelledTests params t =
|
||||
case t of
|
||||
T_Subshell id list | all isTestStructure list ->
|
||||
case () of
|
||||
-- Special case for if (test) and while (test)
|
||||
_ | isCompoundCondition (getPath (parentMap params) t) ->
|
||||
style id 2233 "Remove superfluous (..) around condition."
|
||||
|
||||
-- Special case for ([ x ])
|
||||
_ | isSingleTest list ->
|
||||
style id 2234 "Remove superfluous (..) around test command."
|
||||
|
||||
-- General case for ([ x ] || [ y ] && etc)
|
||||
_ -> style id 2235 "Use { ..; } instead of (..) to avoid subshell overhead."
|
||||
_ -> return ()
|
||||
where
|
||||
|
||||
isSingleTest cmds =
|
||||
case cmds of
|
||||
[c] | isTestCommand c -> True
|
||||
_ -> False
|
||||
|
||||
isTestStructure t =
|
||||
case t of
|
||||
T_Banged _ t -> isTestStructure t
|
||||
T_AndIf _ a b -> isTestStructure a && isTestStructure b
|
||||
T_OrIf _ a b -> isTestStructure a && isTestStructure b
|
||||
T_Pipeline _ [] [T_Redirecting _ _ cmd] ->
|
||||
case cmd of
|
||||
T_BraceGroup _ ts -> all isTestStructure ts
|
||||
T_Subshell _ ts -> all isTestStructure ts
|
||||
_ -> isTestCommand t
|
||||
_ -> isTestCommand t
|
||||
|
||||
isTestCommand t =
|
||||
case t of
|
||||
T_Pipeline _ [] [T_Redirecting _ _ cmd] ->
|
||||
case cmd of
|
||||
T_Condition {} -> True
|
||||
_ -> cmd `isCommand` "test"
|
||||
_ -> False
|
||||
|
||||
-- Check if a T_Subshell is used as a condition, e.g. if ( test )
|
||||
-- This technically also triggers for `if true; then ( test ); fi`
|
||||
-- but it's still a valid suggestion.
|
||||
isCompoundCondition chain =
|
||||
case dropWhile skippable (drop 1 chain) of
|
||||
T_IfExpression {} : _ -> True
|
||||
T_WhileExpression {} : _ -> True
|
||||
T_UntilExpression {} : _ -> True
|
||||
_ -> False
|
||||
|
||||
-- Skip any parent of a T_Subshell until we reach something interesting
|
||||
skippable t =
|
||||
case t of
|
||||
T_Redirecting _ [] _ -> True
|
||||
T_Pipeline _ [] _ -> True
|
||||
T_Annotation {} -> True
|
||||
_ -> False
|
||||
|
||||
prop_checkInvertedStringTest1 = verify checkInvertedStringTest "[ ! -z $var ]"
|
||||
prop_checkInvertedStringTest2 = verify checkInvertedStringTest "! [[ -n $var ]]"
|
||||
prop_checkInvertedStringTest3 = verifyNot checkInvertedStringTest "! [ -x $var ]"
|
||||
prop_checkInvertedStringTest4 = verifyNot checkInvertedStringTest "[[ ! -w $var ]]"
|
||||
prop_checkInvertedStringTest5 = verifyNot checkInvertedStringTest "[ -z $var ]"
|
||||
checkInvertedStringTest _ t =
|
||||
case t of
|
||||
TC_Unary _ _ "!" (TC_Unary _ _ op _) ->
|
||||
case op of
|
||||
"-n" -> style (getId t) 2236 "Use -z instead of ! -n."
|
||||
"-z" -> style (getId t) 2236 "Use -n instead of ! -z."
|
||||
_ -> return ()
|
||||
T_Banged _ (T_Pipeline _ _
|
||||
[T_Redirecting _ _ (T_Condition _ _ (TC_Unary _ _ op _))]) ->
|
||||
case op of
|
||||
"-n" -> style (getId t) 2237 "Use [ -z .. ] instead of ! [ -n .. ]."
|
||||
"-z" -> style (getId t) 2237 "Use [ -n .. ] instead of ! [ -z .. ]."
|
||||
_ -> return ()
|
||||
_ -> return ()
|
||||
|
||||
prop_checkRedirectionToCommand1 = verify checkRedirectionToCommand "ls > rm"
|
||||
prop_checkRedirectionToCommand2 = verifyNot checkRedirectionToCommand "ls > 'rm'"
|
||||
prop_checkRedirectionToCommand3 = verifyNot checkRedirectionToCommand "ls > myfile"
|
||||
checkRedirectionToCommand _ t =
|
||||
case t of
|
||||
T_IoFile _ _ (T_NormalWord id [T_Literal _ str]) | str `elem` commonCommands ->
|
||||
unless (str == "file") $ -- This would be confusing
|
||||
warn id 2238 "Redirecting to/from command name instead of file. Did you want pipes/xargs (or quote to ignore)?"
|
||||
_ -> return ()
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Analyzer (analyzeScript) where
|
||||
|
||||
@@ -30,7 +30,7 @@ import qualified ShellCheck.Checks.ShellSupport
|
||||
|
||||
-- TODO: Clean up the cruft this is layered on
|
||||
analyzeScript :: AnalysisSpec -> AnalysisResult
|
||||
analyzeScript spec = AnalysisResult {
|
||||
analyzeScript spec = newAnalysisResult {
|
||||
arComments =
|
||||
filterByAnnotation spec params . nub $
|
||||
runAnalytics spec
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,30 +15,31 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
module ShellCheck.AnalyzerLib where
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Regex
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Arrow (first)
|
||||
import Control.Monad.Identity
|
||||
import Control.Monad.RWS
|
||||
import Control.Monad.State
|
||||
import Control.Monad.Writer
|
||||
import Data.Char
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import qualified Data.Map as Map
|
||||
import Control.Arrow (first)
|
||||
import Control.Monad.Identity
|
||||
import Control.Monad.RWS
|
||||
import Control.Monad.State
|
||||
import Control.Monad.Writer
|
||||
import Data.Char
|
||||
import Data.List
|
||||
import qualified Data.Map as Map
|
||||
import Data.Maybe
|
||||
import Data.Semigroup
|
||||
|
||||
import Test.QuickCheck.All (forAllProperties)
|
||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||
import Test.QuickCheck.All (forAllProperties)
|
||||
import Test.QuickCheck.Test (maxSuccess, quickCheckWithResult, stdArgs)
|
||||
|
||||
type Analysis = AnalyzerM ()
|
||||
type AnalyzerM a = RWS Parameters [TokenComment] Cache a
|
||||
@@ -47,7 +48,7 @@ nullCheck = const $ return ()
|
||||
|
||||
data Checker = Checker {
|
||||
perScript :: Root -> Analysis,
|
||||
perToken :: Token -> Analysis
|
||||
perToken :: Token -> Analysis
|
||||
}
|
||||
|
||||
runChecker :: Parameters -> Checker -> [TokenComment]
|
||||
@@ -57,28 +58,30 @@ runChecker params checker = notes
|
||||
check = perScript checker `composeAnalyzers` (\(Root x) -> void $ doAnalysis (perToken checker) x)
|
||||
notes = snd $ evalRWS (check $ Root root) params Cache
|
||||
|
||||
instance Semigroup Checker where
|
||||
(<>) x y = Checker {
|
||||
perScript = perScript x `composeAnalyzers` perScript y,
|
||||
perToken = perToken x `composeAnalyzers` perToken y
|
||||
}
|
||||
|
||||
instance Monoid Checker where
|
||||
mempty = Checker {
|
||||
perScript = nullCheck,
|
||||
perToken = nullCheck
|
||||
}
|
||||
mappend x y = Checker {
|
||||
perScript = perScript x `composeAnalyzers` perScript y,
|
||||
perToken = perToken x `composeAnalyzers` perToken y
|
||||
}
|
||||
|
||||
mappend = (Data.Semigroup.<>)
|
||||
|
||||
composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis
|
||||
composeAnalyzers f g x = f x >> g x
|
||||
|
||||
data Parameters = Parameters {
|
||||
hasLastpipe :: Bool, -- Whether this script has the 'lastpipe' option set/default.
|
||||
hasSetE :: Bool, -- Whether this script has 'set -e' anywhere.
|
||||
variableFlow :: [StackData], -- A linear (bad) analysis of data flow
|
||||
parentMap :: Map.Map Id Token, -- A map from Id to parent Token
|
||||
shellType :: Shell, -- The shell type, such as Bash or Ksh
|
||||
hasLastpipe :: Bool, -- Whether this script has the 'lastpipe' option set/default.
|
||||
hasSetE :: Bool, -- Whether this script has 'set -e' anywhere.
|
||||
variableFlow :: [StackData], -- A linear (bad) analysis of data flow
|
||||
parentMap :: Map.Map Id Token, -- A map from Id to parent Token
|
||||
shellType :: Shell, -- The shell type, such as Bash or Ksh
|
||||
shellTypeSpecified :: Bool, -- True if shell type was forced via flags
|
||||
rootNode :: Token -- The root node of the AST
|
||||
rootNode :: Token -- The root node of the AST
|
||||
}
|
||||
|
||||
-- TODO: Cache results of common AST ops here
|
||||
@@ -106,19 +109,17 @@ data DataSource =
|
||||
|
||||
data VariableState = Dead Token String | Alive deriving (Show)
|
||||
|
||||
defaultSpec root = AnalysisSpec {
|
||||
asScript = root,
|
||||
defaultSpec root = spec {
|
||||
asShellType = Nothing,
|
||||
asCheckSourced = False,
|
||||
asExecutionMode = Executed
|
||||
}
|
||||
} where spec = newAnalysisSpec root
|
||||
|
||||
pScript s =
|
||||
let
|
||||
pSpec = ParseSpec {
|
||||
pSpec = newParseSpec {
|
||||
psFilename = "script",
|
||||
psScript = s,
|
||||
psCheckSourced = False
|
||||
psScript = s
|
||||
}
|
||||
in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec
|
||||
|
||||
@@ -132,7 +133,14 @@ producesComments c s = do
|
||||
|
||||
makeComment :: Severity -> Id -> Code -> String -> TokenComment
|
||||
makeComment severity id code note =
|
||||
TokenComment id $ Comment severity code note
|
||||
newTokenComment {
|
||||
tcId = id,
|
||||
tcComment = newComment {
|
||||
cSeverity = severity,
|
||||
cCode = code,
|
||||
cMessage = note
|
||||
}
|
||||
}
|
||||
|
||||
addComment note = tell [note]
|
||||
|
||||
@@ -151,8 +159,8 @@ makeParameters spec =
|
||||
case shellType params of
|
||||
Bash -> containsLastpipe root
|
||||
Dash -> False
|
||||
Sh -> False
|
||||
Ksh -> True,
|
||||
Sh -> False
|
||||
Ksh -> True,
|
||||
|
||||
shellTypeSpecified = isJust $ asShellType spec,
|
||||
parentMap = getParentTree root,
|
||||
@@ -205,7 +213,7 @@ determineShell t = fromMaybe Bash $ do
|
||||
forAnnotation t =
|
||||
case t of
|
||||
(ShellOverride s) -> return s
|
||||
_ -> fail ""
|
||||
_ -> fail ""
|
||||
getCandidates :: Token -> [Maybe String]
|
||||
getCandidates t@T_Script {} = [Just $ fromShebang t]
|
||||
getCandidates (T_Annotation _ annotations s) =
|
||||
@@ -232,9 +240,10 @@ getParentTree t =
|
||||
where
|
||||
pre t = modify (first ((:) t))
|
||||
post t = do
|
||||
(_:rest, map) <- get
|
||||
case rest of [] -> put (rest, map)
|
||||
(x:_) -> put (rest, Map.insert (getId t) x map)
|
||||
(x, map) <- get
|
||||
case x of
|
||||
_:rest -> case rest of [] -> put (rest, map)
|
||||
(x:_) -> put (rest, Map.insert (getId t) x map)
|
||||
|
||||
-- Given a root node, make a map from Id to Token
|
||||
getTokenMap :: Token -> Map.Map Id Token
|
||||
@@ -264,27 +273,27 @@ isQuoteFreeNode strict tree t =
|
||||
case t of
|
||||
T_Assignment {} -> return True
|
||||
T_FdRedirect {} -> return True
|
||||
_ -> Nothing
|
||||
_ -> Nothing
|
||||
|
||||
-- Are any subnodes inherently self-quoting?
|
||||
isQuoteFreeContext t =
|
||||
case t of
|
||||
TC_Nullary _ DoubleBracket _ -> return True
|
||||
TC_Unary _ DoubleBracket _ _ -> return True
|
||||
TC_Nullary _ DoubleBracket _ -> return True
|
||||
TC_Unary _ DoubleBracket _ _ -> return True
|
||||
TC_Binary _ DoubleBracket _ _ _ -> return True
|
||||
TA_Sequence {} -> return True
|
||||
T_Arithmetic {} -> return True
|
||||
T_Assignment {} -> return True
|
||||
T_Redirecting {} -> return False
|
||||
T_DoubleQuoted _ _ -> return True
|
||||
T_DollarDoubleQuoted _ _ -> return True
|
||||
T_CaseExpression {} -> return True
|
||||
T_HereDoc {} -> return True
|
||||
T_DollarBraced {} -> return True
|
||||
TA_Sequence {} -> return True
|
||||
T_Arithmetic {} -> return True
|
||||
T_Assignment {} -> return True
|
||||
T_Redirecting {} -> return False
|
||||
T_DoubleQuoted _ _ -> return True
|
||||
T_DollarDoubleQuoted _ _ -> return True
|
||||
T_CaseExpression {} -> return True
|
||||
T_HereDoc {} -> return True
|
||||
T_DollarBraced {} -> return True
|
||||
-- When non-strict, pragmatically assume it's desirable to split here
|
||||
T_ForIn {} -> return (not strict)
|
||||
T_SelectIn {} -> return (not strict)
|
||||
_ -> Nothing
|
||||
T_ForIn {} -> return (not strict)
|
||||
T_SelectIn {} -> return (not strict)
|
||||
_ -> Nothing
|
||||
|
||||
-- Check if a token is a parameter to a certain command by name:
|
||||
-- Example: isParamTo (parentMap params) "sed" t
|
||||
@@ -293,16 +302,16 @@ isParamTo tree cmd =
|
||||
go
|
||||
where
|
||||
go x = case Map.lookup (getId x) tree of
|
||||
Nothing -> False
|
||||
Nothing -> False
|
||||
Just parent -> check parent
|
||||
check t =
|
||||
case t of
|
||||
T_SingleQuoted _ _ -> go t
|
||||
T_DoubleQuoted _ _ -> go t
|
||||
T_NormalWord _ _ -> go t
|
||||
T_NormalWord _ _ -> go t
|
||||
T_SimpleCommand {} -> isCommand t cmd
|
||||
T_Redirecting {} -> isCommand t cmd
|
||||
_ -> False
|
||||
T_Redirecting {} -> isCommand t cmd
|
||||
_ -> False
|
||||
|
||||
-- Get the parent command (T_Redirecting) of a Token, if any.
|
||||
getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token
|
||||
@@ -312,8 +321,8 @@ getClosestCommand tree t =
|
||||
findCommand t =
|
||||
case t of
|
||||
T_Redirecting {} -> return True
|
||||
T_Script {} -> return False
|
||||
_ -> Nothing
|
||||
T_Script {} -> return False
|
||||
_ -> Nothing
|
||||
|
||||
-- Like above, if koala_man knew Haskell when starting this project.
|
||||
getClosestCommandM t = do
|
||||
@@ -334,7 +343,7 @@ usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
||||
-- A list of the element and all its parents up to the root node.
|
||||
getPath tree t = t :
|
||||
case Map.lookup (getId t) tree of
|
||||
Nothing -> []
|
||||
Nothing -> []
|
||||
Just parent -> getPath tree parent
|
||||
|
||||
-- Version of the above taking the map from the current context
|
||||
@@ -360,9 +369,9 @@ findFirst p l =
|
||||
[] -> Nothing
|
||||
(x:xs) ->
|
||||
case p x of
|
||||
Just True -> return x
|
||||
Just True -> return x
|
||||
Just False -> Nothing
|
||||
Nothing -> findFirst p xs
|
||||
Nothing -> findFirst p xs
|
||||
|
||||
-- Check whether a word is entirely output from a single command
|
||||
tokenIsJustCommandOutput t = case t of
|
||||
@@ -373,7 +382,7 @@ tokenIsJustCommandOutput t = case t of
|
||||
_ -> False
|
||||
where
|
||||
check [x] = not $ isOnlyRedirection x
|
||||
check _ = False
|
||||
check _ = False
|
||||
|
||||
-- TODO: Replace this with a proper Control Flow Graph
|
||||
getVariableFlow params t =
|
||||
@@ -393,9 +402,9 @@ getVariableFlow params t =
|
||||
unless (assignFirst t) $ setWritten t
|
||||
when (scopeType /= NoneScope) $ modify (StackScopeEnd:)
|
||||
|
||||
assignFirst T_ForIn {} = True
|
||||
assignFirst T_ForIn {} = True
|
||||
assignFirst T_SelectIn {} = True
|
||||
assignFirst _ = False
|
||||
assignFirst _ = False
|
||||
|
||||
setRead t =
|
||||
let read = getReferencedVariables (parentMap params) t
|
||||
@@ -423,7 +432,7 @@ leadType params t =
|
||||
parent <- Map.lookup (getId t) (parentMap params)
|
||||
case parent of
|
||||
T_Pipeline {} -> return parent
|
||||
_ -> Nothing
|
||||
_ -> Nothing
|
||||
|
||||
causesSubshell = do
|
||||
(T_Pipeline _ _ list) <- parentPipeline
|
||||
@@ -444,15 +453,12 @@ getModifiedVariables t =
|
||||
c@T_SimpleCommand {} ->
|
||||
getModifiedVariableCommand c
|
||||
|
||||
TA_Unary _ "++|" var -> maybeToList $ do
|
||||
name <- getLiteralString var
|
||||
return (t, t, name, DataString $ SourceFrom [t])
|
||||
TA_Unary _ "|++" var -> maybeToList $ do
|
||||
name <- getLiteralString var
|
||||
return (t, t, name, DataString $ SourceFrom [t])
|
||||
TA_Assignment _ op lhs rhs -> maybeToList $ do
|
||||
TA_Unary _ "++|" v@(TA_Variable _ name _) ->
|
||||
[(t, v, name, DataString $ SourceFrom [v])]
|
||||
TA_Unary _ "|++" v@(TA_Variable _ name _) ->
|
||||
[(t, v, name, DataString $ SourceFrom [v])]
|
||||
TA_Assignment _ op (TA_Variable _ name _) rhs -> maybeToList $ do
|
||||
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
||||
name <- getLiteralString lhs
|
||||
return (t, t, name, DataString $ SourceFrom [rhs])
|
||||
|
||||
-- Count [[ -v foo ]] as an "assignment".
|
||||
@@ -462,10 +468,10 @@ getModifiedVariables t =
|
||||
flip getLiteralStringExt token $ \x ->
|
||||
case x of
|
||||
T_Glob _ s -> return s -- Unquoted index
|
||||
_ -> Nothing
|
||||
_ -> Nothing
|
||||
|
||||
guard . not . null $ str
|
||||
return (t, token, str, DataString $ SourceChecked)
|
||||
return (t, token, str, DataString SourceChecked)
|
||||
|
||||
T_DollarBraced _ l -> maybeToList $ do
|
||||
let string = bracedString t
|
||||
@@ -489,7 +495,7 @@ isClosingFileOp op =
|
||||
case op of
|
||||
T_IoDuplicate _ (T_GREATAND _) "-" -> True
|
||||
T_IoDuplicate _ (T_LESSAND _) "-" -> True
|
||||
_ -> False
|
||||
_ -> False
|
||||
|
||||
|
||||
-- Consider 'export/declare -x' a reference, since it makes the var available
|
||||
@@ -498,7 +504,9 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
|
||||
"export" -> if "f" `elem` flags
|
||||
then []
|
||||
else concatMap getReference rest
|
||||
"declare" -> if any (`elem` flags) ["x", "p"]
|
||||
"declare" -> if
|
||||
any (`elem` flags) ["x", "p"] &&
|
||||
(not $ any (`elem` flags) ["f", "F"])
|
||||
then concatMap getReference rest
|
||||
else []
|
||||
"readonly" ->
|
||||
@@ -518,16 +526,26 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
|
||||
|
||||
getReferencedVariableCommand _ = []
|
||||
|
||||
-- The function returns a tuple consisting of four items describing an assignment.
|
||||
-- Given e.g. declare foo=bar
|
||||
-- (
|
||||
-- BaseCommand :: Token, -- The command/structure assigning the variable, i.e. declare foo=bar
|
||||
-- AssignmentToken :: Token, -- The specific part that assigns this variable, i.e. foo=bar
|
||||
-- VariableName :: String, -- The variable name, i.e. foo
|
||||
-- VariableValue :: DataType -- A description of the value being assigned, i.e. "Literal string with value foo"
|
||||
-- )
|
||||
getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
||||
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
|
||||
case x of
|
||||
"read" ->
|
||||
let params = map getLiteral rest in
|
||||
catMaybes . takeWhile isJust . reverse $ params
|
||||
let params = map getLiteral rest
|
||||
readArrayVars = getReadArrayVariables rest
|
||||
in
|
||||
catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params
|
||||
"getopts" ->
|
||||
case rest of
|
||||
opts:var:_ -> maybeToList $ getLiteral var
|
||||
_ -> []
|
||||
_ -> []
|
||||
|
||||
"let" -> concatMap letParamToLiteral rest
|
||||
|
||||
@@ -566,10 +584,14 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
|
||||
where
|
||||
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
|
||||
|
||||
getLiteral t = do
|
||||
getLiteralOfDataType t d = do
|
||||
s <- getLiteralString t
|
||||
when ("-" `isPrefixOf` s) $ fail "argument"
|
||||
return (base, t, s, DataString SourceExternal)
|
||||
return (base, t, s, d)
|
||||
|
||||
getLiteral t = getLiteralOfDataType t (DataString SourceExternal)
|
||||
|
||||
getLiteralArray t = getLiteralOfDataType t (DataArray SourceExternal)
|
||||
|
||||
getModifierParamString = getModifierParam DataString
|
||||
|
||||
@@ -591,9 +613,9 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
|
||||
getSetParams (t:rest) =
|
||||
let s = getLiteralString t in
|
||||
case s of
|
||||
Just "--" -> return rest
|
||||
Just "--" -> return rest
|
||||
Just ('-':_) -> getSetParams rest
|
||||
_ -> return (t:fromMaybe [] (getSetParams rest))
|
||||
_ -> return (t:fromMaybe [] (getSetParams rest))
|
||||
getSetParams [] = Nothing
|
||||
|
||||
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
|
||||
@@ -611,6 +633,11 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
|
||||
guard $ isVariableName name
|
||||
return (base, lastArg, name, DataArray SourceExternal)
|
||||
|
||||
-- get all the array variables used in read, e.g. read -a arr
|
||||
getReadArrayVariables args = do
|
||||
map (getLiteralArray . snd)
|
||||
(filter (\(x,_) -> getLiteralString x == Just "-a") (zip (args) (tail args)))
|
||||
|
||||
getModifiedVariableCommand _ = []
|
||||
|
||||
getIndexReferences s = fromMaybe [] $ do
|
||||
@@ -620,12 +647,17 @@ getIndexReferences s = fromMaybe [] $ do
|
||||
where
|
||||
re = mkRegex "(\\[.*\\])"
|
||||
|
||||
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
|
||||
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
|
||||
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
|
||||
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
|
||||
getOffsetReferences mods = fromMaybe [] $ do
|
||||
-- if mods start with [, then drop until ]
|
||||
match <- matchRegex re mods
|
||||
offsets <- match !!! 0
|
||||
offsets <- match !!! 1
|
||||
return $ matchAllStrings variableNameRegex offsets
|
||||
where
|
||||
re = mkRegex "^ *:([^-=?+].*)"
|
||||
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
|
||||
|
||||
getReferencedVariables parents t =
|
||||
case t of
|
||||
@@ -634,10 +666,10 @@ getReferencedVariables parents t =
|
||||
map (\x -> (l, l, x)) (
|
||||
getIndexReferences str
|
||||
++ getOffsetReferences (getBracedModifier str))
|
||||
TA_Expansion id _ ->
|
||||
TA_Variable id name _ ->
|
||||
if isArithmeticAssignment t
|
||||
then []
|
||||
else getIfReference t t
|
||||
else [(t, t, name)]
|
||||
T_Assignment id mode str _ word ->
|
||||
[(t, t, str) | mode == Append] ++ specialReferences str t word
|
||||
|
||||
@@ -664,9 +696,8 @@ getReferencedVariables parents t =
|
||||
else []
|
||||
|
||||
literalizer t = case t of
|
||||
TA_Index {} -> return "" -- x[0] becomes a reference of x
|
||||
T_Glob _ s -> return s -- Also when parsed as globs
|
||||
_ -> Nothing
|
||||
_ -> Nothing
|
||||
|
||||
getIfReference context token = maybeToList $ do
|
||||
str <- getLiteralStringExt literalizer token
|
||||
@@ -678,7 +709,7 @@ getReferencedVariables parents t =
|
||||
|
||||
isArithmeticAssignment t = case getPath parents t of
|
||||
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
|
||||
_ -> False
|
||||
_ -> False
|
||||
|
||||
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
|
||||
|
||||
@@ -691,9 +722,8 @@ isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `
|
||||
-- Compare a command to a literal. Like above, but checks full path.
|
||||
isUnqualifiedCommand token str = isCommandMatch token (== str)
|
||||
|
||||
isCommandMatch token matcher = fromMaybe False $ do
|
||||
cmd <- getCommandName token
|
||||
return $ matcher cmd
|
||||
isCommandMatch token matcher = fromMaybe False $
|
||||
fmap matcher (getCommandName token)
|
||||
|
||||
-- Does this regex look like it was intended as a glob?
|
||||
-- True: *foo*
|
||||
@@ -701,7 +731,7 @@ isCommandMatch token matcher = fromMaybe False $ do
|
||||
isConfusedGlobRegex :: String -> Bool
|
||||
isConfusedGlobRegex ('*':_) = True
|
||||
isConfusedGlobRegex [x,'*'] | x /= '\\' = True
|
||||
isConfusedGlobRegex _ = False
|
||||
isConfusedGlobRegex _ = False
|
||||
|
||||
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
|
||||
isVariableChar x = isVariableStartChar x || isDigit x
|
||||
@@ -711,7 +741,7 @@ prop_isVariableName1 = isVariableName "_fo123"
|
||||
prop_isVariableName2 = not $ isVariableName "4"
|
||||
prop_isVariableName3 = not $ isVariableName "test: "
|
||||
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
|
||||
isVariableName _ = False
|
||||
isVariableName _ = False
|
||||
|
||||
getVariablesFromLiteralToken token =
|
||||
getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token)
|
||||
@@ -744,7 +774,7 @@ getBracedReference s = fromMaybe s $
|
||||
where
|
||||
noPrefix = dropPrefix s
|
||||
dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest
|
||||
dropPrefix "" = ""
|
||||
dropPrefix "" = ""
|
||||
takeName s = do
|
||||
let name = takeWhile isVariableChar s
|
||||
guard . not $ null name
|
||||
@@ -769,12 +799,12 @@ getBracedModifier s = fromMaybe "" . listToMaybe $ do
|
||||
a <- dropModifier s
|
||||
dropPrefix var a
|
||||
where
|
||||
dropPrefix [] t = return t
|
||||
dropPrefix [] t = return t
|
||||
dropPrefix (a:b) (c:d) | a == c = dropPrefix b d
|
||||
dropPrefix _ _ = []
|
||||
dropPrefix _ _ = []
|
||||
|
||||
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
|
||||
dropModifier x = [x]
|
||||
dropModifier x = [x]
|
||||
|
||||
-- Useful generic functions.
|
||||
|
||||
@@ -789,12 +819,12 @@ potentially = fromMaybe (return ())
|
||||
|
||||
-- Get element 0 or a default. Like `head` but safe.
|
||||
headOrDefault _ (a:_) = a
|
||||
headOrDefault def _ = def
|
||||
headOrDefault def _ = def
|
||||
|
||||
--- Get element n of a list, or Nothing. Like `!!` but safe.
|
||||
(!!!) list i =
|
||||
case drop i list of
|
||||
[] -> Nothing
|
||||
[] -> Nothing
|
||||
(r:_) -> Just r
|
||||
|
||||
-- Run a command if the shell is in the given list
|
||||
@@ -807,25 +837,24 @@ filterByAnnotation asSpec params =
|
||||
filter (not . shouldIgnore)
|
||||
where
|
||||
token = asScript asSpec
|
||||
idFor (TokenComment id _) = id
|
||||
shouldIgnore note =
|
||||
any (shouldIgnoreFor (getCode note)) $
|
||||
getPath parents (T_Bang $ idFor note)
|
||||
getPath parents (T_Bang $ tcId note)
|
||||
shouldIgnoreFor num (T_Annotation _ anns _) =
|
||||
any hasNum anns
|
||||
where
|
||||
hasNum (DisableComment ts) = num == ts
|
||||
hasNum _ = False
|
||||
hasNum _ = False
|
||||
shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec
|
||||
shouldIgnoreFor _ _ = False
|
||||
parents = parentMap params
|
||||
getCode (TokenComment _ (Comment _ c _)) = c
|
||||
getCode = cCode . tcComment
|
||||
|
||||
-- Is this a ${#anything}, to get string length or array count?
|
||||
isCountingReference (T_DollarBraced id token) =
|
||||
case concat $ oversimplify token of
|
||||
'#':_ -> True
|
||||
_ -> False
|
||||
_ -> False
|
||||
isCountingReference _ = False
|
||||
|
||||
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
|
||||
@@ -837,7 +866,38 @@ isQuotedAlternativeReference t =
|
||||
where
|
||||
re = mkRegex "(^|\\]):?\\+"
|
||||
|
||||
-- getGnuOpts "erd:u:" will parse a SimpleCommand like
|
||||
-- read -re -d : -u 3 bar
|
||||
-- into
|
||||
-- Just [("r", -re), ("e", -re), ("d", :), ("u", 3), ("", bar)]
|
||||
-- where flags with arguments map to arguments, while others map to themselves.
|
||||
-- Any unrecognized flag will result in Nothing.
|
||||
getGnuOpts = getOpts getAllFlags
|
||||
getBsdOpts = getOpts getLeadingFlags
|
||||
getOpts :: (Token -> [(Token, String)]) -> String -> Token -> Maybe [(String, Token)]
|
||||
getOpts flagTokenizer string cmd = process flags
|
||||
where
|
||||
flags = flagTokenizer cmd
|
||||
flagList (c:':':rest) = ([c], True) : flagList rest
|
||||
flagList (c:rest) = ([c], False) : flagList rest
|
||||
flagList [] = []
|
||||
flagMap = Map.fromList $ ("", False) : flagList string
|
||||
|
||||
process [] = return []
|
||||
process [(token, flag)] = do
|
||||
takesArg <- Map.lookup flag flagMap
|
||||
guard $ not takesArg
|
||||
return [(flag, token)]
|
||||
process ((token1, flag1):rest2@((token2, flag2):rest)) = do
|
||||
takesArg <- Map.lookup flag1 flagMap
|
||||
if takesArg
|
||||
then do
|
||||
guard $ flag2 == ""
|
||||
more <- process rest
|
||||
return $ (flag1, token2) : more
|
||||
else do
|
||||
more <- process rest2
|
||||
return $ (flag1, token1) : more
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
|
||||
@@ -37,25 +37,30 @@ import Control.Monad
|
||||
|
||||
import Test.QuickCheck.All
|
||||
|
||||
tokenToPosition map (TokenComment id c) = fromMaybe fail $ do
|
||||
position <- Map.lookup id map
|
||||
return $ PositionedComment position position c
|
||||
tokenToPosition startMap t = fromMaybe fail $ do
|
||||
span <- Map.lookup (tcId t) startMap
|
||||
return $ newPositionedComment {
|
||||
pcStartPos = fst span,
|
||||
pcEndPos = snd span,
|
||||
pcComment = tcComment t
|
||||
}
|
||||
where
|
||||
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
|
||||
|
||||
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
|
||||
checkScript sys spec = do
|
||||
results <- checkScript (csScript spec)
|
||||
return CheckResult {
|
||||
return emptyCheckResult {
|
||||
crFilename = csFilename spec,
|
||||
crComments = results
|
||||
}
|
||||
where
|
||||
checkScript contents = do
|
||||
result <- parseScript sys ParseSpec {
|
||||
result <- parseScript sys newParseSpec {
|
||||
psFilename = csFilename spec,
|
||||
psScript = contents,
|
||||
psCheckSourced = csCheckSourced spec
|
||||
psCheckSourced = csCheckSourced spec,
|
||||
psShellTypeOverride = csShellTypeOverride spec
|
||||
}
|
||||
let parseMessages = prComments result
|
||||
let analysisMessages =
|
||||
@@ -66,27 +71,38 @@ checkScript sys spec = do
|
||||
return . nub . sortMessages . filter shouldInclude $
|
||||
(parseMessages ++ map translator analysisMessages)
|
||||
|
||||
shouldInclude (PositionedComment _ _ (Comment _ code _)) =
|
||||
code `notElem` csExcludedWarnings spec
|
||||
shouldInclude pc =
|
||||
let code = cCode (pcComment pc)
|
||||
severity = cSeverity (pcComment pc)
|
||||
in
|
||||
code `notElem` csExcludedWarnings spec &&
|
||||
severity <= csMinSeverity spec
|
||||
|
||||
sortMessages = sortBy (comparing order)
|
||||
order (PositionedComment pos _ (Comment severity code message)) =
|
||||
(posFile pos, posLine pos, posColumn pos, severity, code, message)
|
||||
getPosition (PositionedComment pos _ _) = pos
|
||||
order pc =
|
||||
let pos = pcStartPos pc
|
||||
comment = pcComment pc in
|
||||
(posFile pos,
|
||||
posLine pos,
|
||||
posColumn pos,
|
||||
cSeverity comment,
|
||||
cCode comment,
|
||||
cMessage comment)
|
||||
getPosition = pcStartPos
|
||||
|
||||
analysisSpec root =
|
||||
AnalysisSpec {
|
||||
as {
|
||||
asScript = root,
|
||||
asShellType = csShellTypeOverride spec,
|
||||
asCheckSourced = csCheckSourced spec,
|
||||
asExecutionMode = Executed
|
||||
}
|
||||
} where as = newAnalysisSpec root
|
||||
|
||||
getErrors sys spec =
|
||||
sort . map getCode . crComments $
|
||||
runIdentity (checkScript sys spec)
|
||||
where
|
||||
getCode (PositionedComment _ _ (Comment _ code _)) = code
|
||||
getCode = cCode . pcComment
|
||||
|
||||
check = checkWithIncludes []
|
||||
|
||||
@@ -136,6 +152,21 @@ prop_optionDisablesIssue2 =
|
||||
csExcludedWarnings = [2148, 1037]
|
||||
}
|
||||
|
||||
prop_wontParseBadShell =
|
||||
[1071] == check "#!/usr/bin/python\ntrue $1\n"
|
||||
|
||||
prop_optionDisablesBadShebang =
|
||||
null $ getErrors
|
||||
(mockedSystemInterface [])
|
||||
emptyCheckSpec {
|
||||
csScript = "#!/usr/bin/python\ntrue\n",
|
||||
csShellTypeOverride = Just Sh
|
||||
}
|
||||
|
||||
prop_annotationDisablesBadShebang =
|
||||
[] == check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
|
||||
|
||||
|
||||
prop_canParseDevNull =
|
||||
[] == check "source /dev/null"
|
||||
|
||||
@@ -180,7 +211,7 @@ prop_filewideAnnotation1 = null $
|
||||
prop_filewideAnnotation2 = null $
|
||||
check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
|
||||
prop_filewideAnnotation3 = null $
|
||||
check "#!/bin/sh\n#unerlated\n# shellcheck disable=2086\ntrue\necho $1"
|
||||
check "#!/bin/sh\n#unrelated\n# shellcheck disable=2086\ntrue\necho $1"
|
||||
prop_filewideAnnotation4 = null $
|
||||
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
|
||||
prop_filewideAnnotation5 = null $
|
||||
@@ -194,5 +225,8 @@ prop_filewideAnnotationBase2 = [2086, 2181] == check "true\n[ $? == 0 ] && echo
|
||||
prop_filewideAnnotation8 = null $
|
||||
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
|
||||
|
||||
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
|
||||
2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,15 +15,13 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
|
||||
-- This module contains checks that examine specific commands by name.
|
||||
module ShellCheck.Checks.Commands (checker
|
||||
, ShellCheck.Checks.Commands.runTests
|
||||
) where
|
||||
module ShellCheck.Checks.Commands (checker , ShellCheck.Checks.Commands.runTests) where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
@@ -63,6 +61,7 @@ commandChecks = [
|
||||
,checkGrepRe
|
||||
,checkTrapQuotes
|
||||
,checkReturn
|
||||
,checkExit
|
||||
,checkFindExecWithSingleArgument
|
||||
,checkUnusedEchoEscapes
|
||||
,checkInjectableFindSh
|
||||
@@ -88,6 +87,13 @@ commandChecks = [
|
||||
,checkWhileGetoptsCase
|
||||
,checkCatastrophicRm
|
||||
,checkLetUsage
|
||||
,checkMvArguments, checkCpArguments, checkLnArguments
|
||||
,checkFindRedirections
|
||||
,checkReadExpansions
|
||||
,checkWhich
|
||||
,checkSudoRedirect
|
||||
,checkSudoArgs
|
||||
,checkSourceArgs
|
||||
]
|
||||
|
||||
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
||||
@@ -137,6 +143,7 @@ prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
|
||||
prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
|
||||
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
|
||||
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
|
||||
prop_checkTr12= verifyNot checkTr "tr '[=e=]' 'e'"
|
||||
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
|
||||
where
|
||||
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
|
||||
@@ -148,7 +155,7 @@ checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
|
||||
Just s -> do -- Eliminate false positives by only looking for dupes in SET2?
|
||||
when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $
|
||||
info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)."
|
||||
unless ("[:" `isPrefixOf` s) $
|
||||
unless ("[:" `isPrefixOf` s || "[=" `isPrefixOf` s) $
|
||||
when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $
|
||||
info (getId word) 2021 "Don't use [] around classes in tr, it replaces literal square brackets."
|
||||
Nothing -> return ()
|
||||
@@ -179,7 +186,7 @@ prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)
|
||||
checkNeedlessExpr = CommandCheck (Basename "expr") f where
|
||||
f t =
|
||||
when (all (`notElem` exceptions) (words $ arguments t)) $
|
||||
style (getId t) 2003
|
||||
style (getId $ getCommandTokenOrThis t) 2003
|
||||
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
|
||||
-- These operators are hard to replicate in POSIX
|
||||
exceptions = [ ":", "<", ">", "<=", ">=" ]
|
||||
@@ -275,15 +282,28 @@ prop_checkReturn4 = verifyNot checkReturn "return $((a|b))"
|
||||
prop_checkReturn5 = verify checkReturn "return -1"
|
||||
prop_checkReturn6 = verify checkReturn "return 1000"
|
||||
prop_checkReturn7 = verify checkReturn "return 'hello world'"
|
||||
checkReturn = CommandCheck (Exactly "return") (f . arguments)
|
||||
checkReturn = CommandCheck (Exactly "return") (returnOrExit
|
||||
(\c -> err c 2151 "Only one integer 0-255 can be returned. Use stdout for other data.")
|
||||
(\c -> err c 2152 "Can only return 0-255. Other data should be written to stdout."))
|
||||
|
||||
prop_checkExit1 = verifyNot checkExit "exit"
|
||||
prop_checkExit2 = verifyNot checkExit "exit 1"
|
||||
prop_checkExit3 = verifyNot checkExit "exit $var"
|
||||
prop_checkExit4 = verifyNot checkExit "exit $((a|b))"
|
||||
prop_checkExit5 = verify checkExit "exit -1"
|
||||
prop_checkExit6 = verify checkExit "exit 1000"
|
||||
prop_checkExit7 = verify checkExit "exit 'hello world'"
|
||||
checkExit = CommandCheck (Exactly "exit") (returnOrExit
|
||||
(\c -> err c 2241 "The exit status can only be one integer 0-255. Use stdout for other data.")
|
||||
(\c -> err c 2242 "Can only exit with status 0-255. Other data should be written to stdout/stderr."))
|
||||
|
||||
returnOrExit multi invalid = (f . arguments)
|
||||
where
|
||||
f (first:second:_) =
|
||||
err (getId second) 2151
|
||||
"Only one integer 0-255 can be returned. Use stdout for other data."
|
||||
multi (getId first)
|
||||
f [value] =
|
||||
when (isInvalid $ literal value) $
|
||||
err (getId value) 2152
|
||||
"Can only return 0-255. Other data should be written to stdout."
|
||||
invalid (getId value)
|
||||
f _ = return ()
|
||||
|
||||
isInvalid s = s == "" || any (not . isDigit) s || length s > 5
|
||||
@@ -320,26 +340,18 @@ prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\n
|
||||
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
|
||||
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
|
||||
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
|
||||
checkUnusedEchoEscapes = CommandCheck (Basename "echo") (f . arguments)
|
||||
checkUnusedEchoEscapes = CommandCheck (Basename "echo") f
|
||||
where
|
||||
isDashE = mkRegex "^-.*e"
|
||||
hasEscapes = mkRegex "\\\\[rnt]"
|
||||
f args | concat (concatMap oversimplify allButLast) `matches` isDashE =
|
||||
return ()
|
||||
where allButLast = reverse . drop 1 . reverse $ args
|
||||
f args = mapM_ checkEscapes args
|
||||
f cmd =
|
||||
whenShell [Sh, Bash, Ksh] $
|
||||
unless (cmd `hasFlag` "e") $
|
||||
mapM_ examine $ arguments cmd
|
||||
|
||||
checkEscapes (T_NormalWord _ args) =
|
||||
mapM_ checkEscapes args
|
||||
checkEscapes (T_DoubleQuoted id args) =
|
||||
mapM_ checkEscapes args
|
||||
checkEscapes (T_Literal id str) = examine id str
|
||||
checkEscapes (T_SingleQuoted id str) = examine id str
|
||||
checkEscapes _ = return ()
|
||||
|
||||
examine id str =
|
||||
examine token = do
|
||||
let str = onlyLiteralString token
|
||||
when (str `matches` hasEscapes) $
|
||||
info id 2028 "echo won't expand escape sequences. Consider printf."
|
||||
info (getId token) 2028 "echo may not expand escape sequences. Use printf."
|
||||
|
||||
|
||||
prop_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
|
||||
@@ -480,13 +492,13 @@ checkInteractiveSu = CommandCheck (Basename "su") f
|
||||
prop_checkSshCmdStr1 = verify checkSshCommandString "ssh host \"echo $PS1\""
|
||||
prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\""
|
||||
prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""
|
||||
prop_checkSshCmdStr4 = verifyNot checkSshCommandString "ssh -i key \"$host\""
|
||||
checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
|
||||
where
|
||||
nonOptions =
|
||||
filter (\x -> not $ "-" `isPrefixOf` concat (oversimplify x))
|
||||
isOption x = "-" `isPrefixOf` (concat $ oversimplify x)
|
||||
f args =
|
||||
case nonOptions args of
|
||||
(hostport:r@(_:_)) -> checkArg $ last r
|
||||
case partition isOption args of
|
||||
([], hostport:r@(_:_)) -> checkArg $ last r
|
||||
_ -> return ()
|
||||
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
|
||||
case filter (not . isConstant) parts of
|
||||
@@ -507,6 +519,13 @@ prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\"
|
||||
prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
|
||||
prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
|
||||
prop_checkPrintfVar11= verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
|
||||
prop_checkPrintfVar12= verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
|
||||
prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
|
||||
prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1"
|
||||
prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
|
||||
prop_checkPrintfVar16= verifyNot checkPrintfVar "printf $'string'"
|
||||
prop_checkPrintfVar17= verify checkPrintfVar "printf '%-*s\\n' 1"
|
||||
prop_checkPrintfVar18= verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
|
||||
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
|
||||
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
|
||||
@@ -517,10 +536,20 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||
case string of
|
||||
'%':'%':rest -> countFormats rest
|
||||
'%':'(':rest -> 1 + countFormats (dropWhile (/= ')') rest)
|
||||
'%':rest -> 1 + countFormats rest
|
||||
'%':rest -> regexBasedCountFormats rest + countFormats (dropWhile (/= '%') rest)
|
||||
_:rest -> countFormats rest
|
||||
[] -> 0
|
||||
|
||||
regexBasedCountFormats rest =
|
||||
maybe 1 (foldl (\acc group -> acc + (if group == "*" then 1 else 0)) 1) (matchRegex re rest)
|
||||
where
|
||||
-- constructed based on specifications in "man printf"
|
||||
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*).?(\\d*|\\*)[diouxXfFeEgGaAcsb]"
|
||||
-- \____ _____/\___ ____/ \____ ____/\________ ________/
|
||||
-- V V V V
|
||||
-- flags field width precision format character
|
||||
-- field width and precision can be specified with a '*' instead of a digit,
|
||||
-- in which case printf will accept one more argument for each '*' used
|
||||
check format more = do
|
||||
fromMaybe (return ()) $ do
|
||||
string <- getLiteralString format
|
||||
@@ -532,7 +561,7 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||
"This printf format string has no variables. Other arguments are ignored."
|
||||
|
||||
when (vars > 0
|
||||
&& length more < vars
|
||||
&& ((length more) `mod` vars /= 0 || null more)
|
||||
&& all (not . mayBecomeMultipleArgs) more) $
|
||||
warn (getId format) 2183 $
|
||||
"This format string has " ++ show vars ++ " variables, but is passed " ++ show (length more) ++ " arguments."
|
||||
@@ -585,15 +614,48 @@ checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
|
||||
prop_checkExportedExpansions1 = verify checkExportedExpansions "export $foo"
|
||||
prop_checkExportedExpansions2 = verify checkExportedExpansions "export \"$foo\""
|
||||
prop_checkExportedExpansions3 = verifyNot checkExportedExpansions "export foo"
|
||||
checkExportedExpansions = CommandCheck (Exactly "export") (check . arguments)
|
||||
prop_checkExportedExpansions4 = verifyNot checkExportedExpansions "export ${foo?}"
|
||||
checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
|
||||
where
|
||||
check = mapM_ checkForVariables
|
||||
checkForVariables f =
|
||||
case getWordParts f of
|
||||
[t@(T_DollarBraced {})] ->
|
||||
warn (getId t) 2163 "Exporting an expansion rather than a variable."
|
||||
_ -> return ()
|
||||
check t = potentially $ do
|
||||
var <- getSingleUnmodifiedVariable t
|
||||
let name = bracedString var
|
||||
return . warn (getId t) 2163 $
|
||||
"This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
||||
|
||||
prop_checkReadExpansions1 = verify checkReadExpansions "read $var"
|
||||
prop_checkReadExpansions2 = verify checkReadExpansions "read -r $var"
|
||||
prop_checkReadExpansions3 = verifyNot checkReadExpansions "read -p $var"
|
||||
prop_checkReadExpansions4 = verifyNot checkReadExpansions "read -rd $delim name"
|
||||
prop_checkReadExpansions5 = verify checkReadExpansions "read \"$var\""
|
||||
prop_checkReadExpansions6 = verify checkReadExpansions "read -a $var"
|
||||
prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1"
|
||||
prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}"
|
||||
checkReadExpansions = CommandCheck (Exactly "read") check
|
||||
where
|
||||
options = getGnuOpts "sreu:n:N:i:p:a:"
|
||||
getVars cmd = fromMaybe [] $ do
|
||||
opts <- options cmd
|
||||
return . map snd $ filter (\(x,_) -> x == "" || x == "a") opts
|
||||
|
||||
check cmd = mapM_ warning $ getVars cmd
|
||||
warning t = potentially $ do
|
||||
var <- getSingleUnmodifiedVariable t
|
||||
let name = bracedString var
|
||||
guard $ isVariableName name -- e.g. not $1
|
||||
return . warn (getId t) 2229 $
|
||||
"This does not read '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
||||
|
||||
-- Return the single variable expansion that makes up this word, if any.
|
||||
-- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing
|
||||
getSingleUnmodifiedVariable :: Token -> Maybe Token
|
||||
getSingleUnmodifiedVariable word =
|
||||
case getWordParts word of
|
||||
[t@(T_DollarBraced {})] ->
|
||||
let contents = bracedString t
|
||||
name = getBracedReference contents
|
||||
in guard (contents == name) >> return t
|
||||
_ -> Nothing
|
||||
|
||||
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
|
||||
prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'"
|
||||
@@ -636,20 +698,24 @@ prop_checkFindWithoutPath3 = verifyNot checkFindWithoutPath "find . -type f"
|
||||
prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"
|
||||
prop_checkFindWithoutPath5 = verifyNot checkFindWithoutPath "find -O3 ."
|
||||
prop_checkFindWithoutPath6 = verifyNot checkFindWithoutPath "find -D exec ."
|
||||
prop_checkFindWithoutPath7 = verifyNot checkFindWithoutPath "find --help"
|
||||
prop_checkFindWithoutPath8 = verifyNot checkFindWithoutPath "find -Hx . -print"
|
||||
checkFindWithoutPath = CommandCheck (Basename "find") f
|
||||
where
|
||||
f (T_SimpleCommand _ _ (cmd:args)) =
|
||||
unless (hasPath args) $
|
||||
f t@(T_SimpleCommand _ _ (cmd:args)) =
|
||||
unless (t `hasFlag` "help" || hasPath args) $
|
||||
info (getId cmd) 2185 "Some finds don't have a default path. Specify '.' explicitly."
|
||||
|
||||
-- This is a bit of a kludge. find supports flag arguments both before and after the path,
|
||||
-- as well as multiple non-flag arguments that are not the path. We assume that all the
|
||||
-- pre-path flags are single characters, which is generally the case except for -O3.
|
||||
-- This is a bit of a kludge. find supports flag arguments both before and
|
||||
-- after the path, as well as multiple non-flag arguments that are not the
|
||||
-- path. We assume that all the pre-path flags are single characters from a
|
||||
-- list of GNU and macOS flags.
|
||||
hasPath (first:rest) =
|
||||
let flag = fromJust $ getLiteralStringExt (const $ return "___") first in
|
||||
not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest
|
||||
hasPath [] = False
|
||||
isLeadingFlag flag = length flag <= 2 || "-O" `isPrefixOf` flag
|
||||
isLeadingFlag flag = length flag <= 2 || all (`elem` leadingFlagChars) flag
|
||||
leadingFlagChars="-EHLPXdfsxO0123456789"
|
||||
|
||||
|
||||
prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10"
|
||||
@@ -698,20 +764,20 @@ checkLocalScope = CommandCheck (Exactly "local") $ \t ->
|
||||
whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local
|
||||
path <- getPathM t
|
||||
unless (any isFunction path) $
|
||||
err (getId t) 2168 "'local' is only valid in functions."
|
||||
err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
|
||||
|
||||
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
|
||||
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
|
||||
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
|
||||
\t -> warn (getId t) 2186 "tempfile is deprecated. Use mktemp instead."
|
||||
\t -> warn (getId $ getCommandTokenOrThis t) 2186 "tempfile is deprecated. Use mktemp instead."
|
||||
|
||||
prop_checkDeprecatedEgrep = verify checkDeprecatedEgrep "egrep '.+'"
|
||||
checkDeprecatedEgrep = CommandCheck (Basename "egrep") $
|
||||
\t -> info (getId t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."
|
||||
\t -> info (getId $ getCommandTokenOrThis t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."
|
||||
|
||||
prop_checkDeprecatedFgrep = verify checkDeprecatedFgrep "fgrep '*' files"
|
||||
checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $
|
||||
\t -> info (getId t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
|
||||
\t -> info (getId $ getCommandTokenOrThis t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
|
||||
|
||||
prop_checkWhileGetoptsCase1 = verify checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; esac; done"
|
||||
prop_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; b) bar;; esac; done"
|
||||
@@ -850,5 +916,123 @@ checkLetUsage = CommandCheck (Exactly "let") f
|
||||
f t = whenShell [Bash,Ksh] $ do
|
||||
style (getId t) 2219 $ "Instead of 'let expr', prefer (( expr )) ."
|
||||
|
||||
|
||||
missingDestination handler token = do
|
||||
case params of
|
||||
[single] -> do
|
||||
unless (hasTarget || mayBecomeMultipleArgs single) $
|
||||
handler token
|
||||
_ -> return ()
|
||||
where
|
||||
args = getAllFlags token
|
||||
params = map fst $ filter (\(_,x) -> x == "") args
|
||||
hasTarget =
|
||||
any (\x -> x /= "" && x `isPrefixOf` "target-directory") $
|
||||
map snd args
|
||||
|
||||
prop_checkMvArguments1 = verify checkMvArguments "mv 'foo bar'"
|
||||
prop_checkMvArguments2 = verifyNot checkMvArguments "mv foo bar"
|
||||
prop_checkMvArguments3 = verifyNot checkMvArguments "mv 'foo bar'{,bak}"
|
||||
prop_checkMvArguments4 = verifyNot checkMvArguments "mv \"$@\""
|
||||
prop_checkMvArguments5 = verifyNot checkMvArguments "mv -t foo bar"
|
||||
prop_checkMvArguments6 = verifyNot checkMvArguments "mv --target-directory=foo bar"
|
||||
prop_checkMvArguments7 = verifyNot checkMvArguments "mv --target-direc=foo bar"
|
||||
prop_checkMvArguments8 = verifyNot checkMvArguments "mv --version"
|
||||
prop_checkMvArguments9 = verifyNot checkMvArguments "mv \"${!var}\""
|
||||
checkMvArguments = CommandCheck (Basename "mv") $ missingDestination f
|
||||
where
|
||||
f t = err (getId t) 2224 "This mv has no destination. Check the arguments."
|
||||
|
||||
checkCpArguments = CommandCheck (Basename "cp") $ missingDestination f
|
||||
where
|
||||
f t = err (getId t) 2225 "This cp has no destination. Check the arguments."
|
||||
|
||||
checkLnArguments = CommandCheck (Basename "ln") $ missingDestination f
|
||||
where
|
||||
f t = warn (getId t) 2226 "This ln has no destination. Check the arguments, or specify '.' explicitly."
|
||||
|
||||
|
||||
prop_checkFindRedirections1 = verify checkFindRedirections "find . -exec echo {} > file \\;"
|
||||
prop_checkFindRedirections2 = verifyNot checkFindRedirections "find . -exec echo {} \\; > file"
|
||||
prop_checkFindRedirections3 = verifyNot checkFindRedirections "find . -execdir sh -c 'foo > file' \\;"
|
||||
checkFindRedirections = CommandCheck (Basename "find") f
|
||||
where
|
||||
f t = do
|
||||
redirecting <- getClosestCommandM t
|
||||
case redirecting of
|
||||
Just (T_Redirecting _ redirs@(_:_) (T_SimpleCommand _ _ args@(_:_:_))) -> do
|
||||
-- This assumes IDs are sequential, which is mostly but not always true.
|
||||
let minRedir = minimum $ map getId redirs
|
||||
let maxArg = maximum $ map getId args
|
||||
when (minRedir < maxArg) $
|
||||
warn minRedir 2227
|
||||
"Redirection applies to the find command itself. Rewrite to work per action (or move to end)."
|
||||
_ -> return ()
|
||||
|
||||
prop_checkWhich = verify checkWhich "which '.+'"
|
||||
checkWhich = CommandCheck (Basename "which") $
|
||||
\t -> info (getId $ getCommandTokenOrThis t) 2230 "which is non-standard. Use builtin 'command -v' instead."
|
||||
|
||||
prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
|
||||
prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
|
||||
prop_checkSudoRedirect3 = verify checkSudoRedirect "sudo cmd >> file"
|
||||
prop_checkSudoRedirect4 = verify checkSudoRedirect "sudo cmd &> file"
|
||||
prop_checkSudoRedirect5 = verifyNot checkSudoRedirect "sudo cmd 2>&1"
|
||||
prop_checkSudoRedirect6 = verifyNot checkSudoRedirect "sudo cmd 2> log"
|
||||
prop_checkSudoRedirect7 = verifyNot checkSudoRedirect "sudo cmd > /dev/null 2>&1"
|
||||
checkSudoRedirect = CommandCheck (Basename "sudo") f
|
||||
where
|
||||
f t = do
|
||||
t_redir <- getClosestCommandM t
|
||||
case t_redir of
|
||||
Just (T_Redirecting _ redirs _) ->
|
||||
mapM_ warnAbout redirs
|
||||
warnAbout (T_FdRedirect _ s (T_IoFile id op file))
|
||||
| (s == "" || s == "&") && not (special file) =
|
||||
case op of
|
||||
T_Less _ ->
|
||||
info (getId op) 2024
|
||||
"sudo doesn't affect redirects. Use sudo cat file | .."
|
||||
T_Greater _ ->
|
||||
warn (getId op) 2024
|
||||
"sudo doesn't affect redirects. Use ..| sudo tee file"
|
||||
T_DGREAT _ ->
|
||||
warn (getId op) 2024
|
||||
"sudo doesn't affect redirects. Use .. | sudo tee -a file"
|
||||
_ -> return ()
|
||||
warnAbout _ = return ()
|
||||
special file = concat (oversimplify file) == "/dev/null"
|
||||
|
||||
prop_checkSudoArgs1 = verify checkSudoArgs "sudo cd /root"
|
||||
prop_checkSudoArgs2 = verify checkSudoArgs "sudo export x=3"
|
||||
prop_checkSudoArgs3 = verifyNot checkSudoArgs "sudo ls /usr/local/protected"
|
||||
prop_checkSudoArgs4 = verifyNot checkSudoArgs "sudo ls && export x=3"
|
||||
prop_checkSudoArgs5 = verifyNot checkSudoArgs "sudo echo ls"
|
||||
prop_checkSudoArgs6 = verifyNot checkSudoArgs "sudo -n -u export ls"
|
||||
prop_checkSudoArgs7 = verifyNot checkSudoArgs "sudo docker export foo"
|
||||
checkSudoArgs = CommandCheck (Basename "sudo") f
|
||||
where
|
||||
f t = potentially $ do
|
||||
opts <- parseOpts t
|
||||
let nonFlags = map snd $ filter (\(flag, _) -> flag == "") opts
|
||||
commandArg <- nonFlags !!! 0
|
||||
command <- getLiteralString commandArg
|
||||
guard $ command `elem` builtins
|
||||
return $ warn (getId t) 2232 $ "Can't use sudo with builtins like " ++ command ++ ". Did you want sudo sh -c .. instead?"
|
||||
builtins = [ "cd", "eval", "export", "history", "read", "source", "wait" ]
|
||||
-- This mess is why ShellCheck prefers not to know.
|
||||
parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:"
|
||||
|
||||
prop_checkSourceArgs1 = verify checkSourceArgs "#!/bin/sh\n. script arg"
|
||||
prop_checkSourceArgs2 = verifyNot checkSourceArgs "#!/bin/sh\n. script"
|
||||
prop_checkSourceArgs3 = verifyNot checkSourceArgs "#!/bin/bash\n. script arg"
|
||||
checkSourceArgs = CommandCheck (Exactly ".") f
|
||||
where
|
||||
f t = whenShell [Sh, Dash] $
|
||||
case arguments t of
|
||||
(file:arg1:_) -> warn (getId arg1) 2240 $
|
||||
"The dot command does not support arguments in sh/dash. Set them as variables."
|
||||
_ -> return ()
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2016 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,13 +15,11 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
module ShellCheck.Checks.ShellSupport (checker
|
||||
, ShellCheck.Checks.ShellSupport.runTests
|
||||
) where
|
||||
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
@@ -129,13 +127,15 @@ prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
|
||||
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
|
||||
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
|
||||
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
|
||||
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/dash\necho $LINENO"
|
||||
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
|
||||
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
|
||||
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
|
||||
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
||||
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
||||
prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
||||
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"
|
||||
prop_checkBashisms55= verify checkBashisms "#!/bin/sh\necho ${@%foo}"
|
||||
prop_checkBashisms56= verifyNot checkBashisms "#!/bin/sh\necho ${##}"
|
||||
checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
params <- ask
|
||||
kludge params t
|
||||
@@ -191,11 +191,9 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
bashism (T_Glob id str) | "[^" `isInfixOf` str =
|
||||
warnMsg id "^ in place of ! in glob bracket expressions is"
|
||||
|
||||
bashism t@(TA_Expansion id _) | isBashism =
|
||||
warnMsg id $ fromJust str ++ " is"
|
||||
where
|
||||
str = getLiteralString t
|
||||
isBashism = isJust str && isBashVariable (fromJust str)
|
||||
bashism t@(TA_Variable id str _) | isBashVariable str =
|
||||
warnMsg id $ str ++ " is"
|
||||
|
||||
bashism t@(T_DollarBraced id token) = do
|
||||
mapM_ check expansion
|
||||
when (isBashVariable var) $
|
||||
@@ -279,14 +277,18 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
||||
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
|
||||
"typeset"
|
||||
] ++ if not isDash then ["local", "type"] else []
|
||||
] ++ if not isDash then ["local"] else []
|
||||
allowedFlags = Map.fromList [
|
||||
("read", if isDash then ["r", "p"] else ["r"]),
|
||||
("ulimit", ["f"]),
|
||||
("exec", []),
|
||||
("export", ["-p"]),
|
||||
("printf", []),
|
||||
("exec", [])
|
||||
("read", if isDash then ["r", "p"] else ["r"]),
|
||||
("ulimit", ["f"])
|
||||
]
|
||||
|
||||
bashism t@(T_SourceCommand id src _) =
|
||||
let name = fromMaybe "" $ getCommandName src
|
||||
in do
|
||||
when (name == "source") $ warnMsg id "'source' in place of '.' is"
|
||||
bashism _ = return ()
|
||||
|
||||
varChars="_0-9a-zA-Z"
|
||||
@@ -295,15 +297,16 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
|
||||
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
|
||||
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
|
||||
(re $ "^[" ++ varChars ++ "]+:[^-=?+]", "string indexing is"),
|
||||
(re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is")
|
||||
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", "string indexing is"),
|
||||
(re $ "^([*@][%#]|#[@*])", "string operations on $@/$* are"),
|
||||
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")
|
||||
]
|
||||
bashVars = [
|
||||
"LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
||||
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
||||
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
|
||||
]
|
||||
bashDynamicVars = [ "RANDOM", "SECONDS" ]
|
||||
dashVars = [ "LINENO" ]
|
||||
dashVars = [ ]
|
||||
isBashVariable var =
|
||||
(var `elem` bashDynamicVars
|
||||
|| var `elem` bashVars && not (isAssigned var))
|
@@ -39,7 +39,7 @@ internalVariables = [
|
||||
]
|
||||
|
||||
variablesWithoutSpaces = [
|
||||
"$", "-", "?", "!",
|
||||
"$", "-", "?", "!", "#",
|
||||
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
|
||||
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
|
||||
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
|
||||
@@ -79,10 +79,10 @@ commonCommands = [
|
||||
|
||||
nonReadingCommands = [
|
||||
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
|
||||
"cp", "du", "echo", "export", "fg", "fuser", "getconf", "getopt",
|
||||
"getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls", "locale", "mv",
|
||||
"nice", "printf", "ps", "pwd", "renice", "rm", "rmdir", "set", "sleep",
|
||||
"touch", "trap", "ulimit", "unalias", "uname"
|
||||
"cp", "du", "echo", "export", "false", "fg", "fuser", "getconf",
|
||||
"getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
|
||||
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
|
||||
"set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname"
|
||||
]
|
||||
|
||||
sampleWords = [
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.CheckStyle (format) where
|
||||
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.Format where
|
||||
|
||||
@@ -30,17 +30,17 @@ data Formatter = Formatter {
|
||||
footer :: IO ()
|
||||
}
|
||||
|
||||
sourceFile (PositionedComment pos _ _) = posFile pos
|
||||
lineNo (PositionedComment pos _ _) = posLine pos
|
||||
endLineNo (PositionedComment _ end _) = posLine end
|
||||
colNo (PositionedComment pos _ _) = posColumn pos
|
||||
endColNo (PositionedComment _ end _) = posColumn end
|
||||
codeNo (PositionedComment _ _ (Comment _ code _)) = code
|
||||
messageText (PositionedComment _ _ (Comment _ _ t)) = t
|
||||
sourceFile = posFile . pcStartPos
|
||||
lineNo = posLine . pcStartPos
|
||||
endLineNo = posLine . pcEndPos
|
||||
colNo = posColumn . pcStartPos
|
||||
endColNo = posColumn . pcEndPos
|
||||
codeNo = cCode . pcComment
|
||||
messageText = cMessage . pcComment
|
||||
|
||||
severityText :: PositionedComment -> String
|
||||
severityText (PositionedComment _ _ (Comment c _ _)) =
|
||||
case c of
|
||||
severityText pc =
|
||||
case cSeverity (pcComment pc) of
|
||||
ErrorC -> "error"
|
||||
WarningC -> "warning"
|
||||
InfoC -> "info"
|
||||
@@ -51,11 +51,14 @@ makeNonVirtual comments contents =
|
||||
map fix comments
|
||||
where
|
||||
ls = lines contents
|
||||
fix c@(PositionedComment start end comment) = PositionedComment start {
|
||||
posColumn = realignColumn lineNo colNo c
|
||||
} end {
|
||||
posColumn = realignColumn endLineNo endColNo c
|
||||
} comment
|
||||
fix c = c {
|
||||
pcStartPos = (pcStartPos c) {
|
||||
posColumn = realignColumn lineNo colNo c
|
||||
}
|
||||
, pcEndPos = (pcEndPos c) {
|
||||
posColumn = realignColumn endLineNo endColNo c
|
||||
}
|
||||
}
|
||||
realignColumn lineNo colNo c =
|
||||
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
|
||||
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.GCC (format) where
|
||||
|
@@ -1,8 +1,9 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,17 +16,19 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.JSON (format) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Data.Aeson
|
||||
import Data.IORef
|
||||
import Data.Monoid
|
||||
import GHC.Exts
|
||||
import System.IO
|
||||
import Text.JSON
|
||||
import qualified Data.ByteString.Lazy.Char8 as BL
|
||||
|
||||
format = do
|
||||
ref <- newIORef []
|
||||
@@ -36,19 +39,36 @@ format = do
|
||||
footer = finish ref
|
||||
}
|
||||
|
||||
instance JSON (PositionedComment) where
|
||||
showJSON comment@(PositionedComment start end (Comment level code string)) = makeObj [
|
||||
("file", showJSON $ posFile start),
|
||||
("line", showJSON $ posLine start),
|
||||
("endLine", showJSON $ posLine end),
|
||||
("column", showJSON $ posColumn start),
|
||||
("endColumn", showJSON $ posColumn end),
|
||||
("level", showJSON $ severityText comment),
|
||||
("code", showJSON code),
|
||||
("message", showJSON string)
|
||||
]
|
||||
instance ToJSON (PositionedComment) where
|
||||
toJSON comment =
|
||||
let start = pcStartPos comment
|
||||
end = pcEndPos comment
|
||||
c = pcComment comment in
|
||||
object [
|
||||
"file" .= posFile start,
|
||||
"line" .= posLine start,
|
||||
"endLine" .= posLine end,
|
||||
"column" .= posColumn start,
|
||||
"endColumn" .= posColumn end,
|
||||
"level" .= severityText comment,
|
||||
"code" .= cCode c,
|
||||
"message" .= cMessage c
|
||||
]
|
||||
|
||||
readJSON = undefined
|
||||
toEncoding comment =
|
||||
let start = pcStartPos comment
|
||||
end = pcEndPos comment
|
||||
c = pcComment comment in
|
||||
pairs (
|
||||
"file" .= posFile start
|
||||
<> "line" .= posLine start
|
||||
<> "endLine" .= posLine end
|
||||
<> "column" .= posColumn start
|
||||
<> "endColumn" .= posColumn end
|
||||
<> "level" .= severityText comment
|
||||
<> "code" .= cCode c
|
||||
<> "message" .= cMessage c
|
||||
)
|
||||
|
||||
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
|
||||
collectResult ref result _ =
|
||||
@@ -56,5 +76,5 @@ collectResult ref result _ =
|
||||
|
||||
finish ref = do
|
||||
list <- readIORef ref
|
||||
putStrLn $ encodeStrict list
|
||||
BL.putStrLn $ encode list
|
||||
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,25 +15,34 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.TTY (format) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Control.Monad
|
||||
import Data.IORef
|
||||
import Data.List
|
||||
import GHC.Exts
|
||||
import System.Info
|
||||
import System.IO
|
||||
import System.Info
|
||||
|
||||
wikiLink = "https://www.shellcheck.net/wiki/"
|
||||
|
||||
-- An arbitrary Ord thing to order warnings
|
||||
type Ranking = (Char, Severity, Integer)
|
||||
|
||||
format :: FormatterOptions -> IO Formatter
|
||||
format options = return Formatter {
|
||||
header = return (),
|
||||
footer = return (),
|
||||
onFailure = outputError options,
|
||||
onResult = outputResult options
|
||||
}
|
||||
format options = do
|
||||
topErrorRef <- newIORef []
|
||||
return Formatter {
|
||||
header = return (),
|
||||
footer = outputWiki topErrorRef,
|
||||
onFailure = outputError options,
|
||||
onResult = outputResult options topErrorRef
|
||||
}
|
||||
|
||||
colorForLevel level =
|
||||
case level of
|
||||
@@ -45,13 +54,60 @@ colorForLevel level =
|
||||
"source" -> 0 -- none
|
||||
_ -> 0 -- none
|
||||
|
||||
rankError :: PositionedComment -> Ranking
|
||||
rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err)
|
||||
where
|
||||
ranking =
|
||||
if cCode (pcComment err) `elem` uninteresting
|
||||
then 'Z'
|
||||
else 'A'
|
||||
|
||||
-- A list of the most generic, least directly helpful
|
||||
-- error codes to downrank.
|
||||
uninteresting = [
|
||||
1009, -- Mentioned parser error was..
|
||||
1019, -- Expected this to be an argument
|
||||
1036, -- ( is invalid here
|
||||
1047, -- Expected 'fi'
|
||||
1062, -- Expected 'done'
|
||||
1070, -- Parsing stopped here (generic)
|
||||
1072, -- Missing/unexpected ..
|
||||
1073, -- Couldn't parse this ..
|
||||
1088, -- Parsing stopped here (paren)
|
||||
1089 -- Parsing stopped here (keyword)
|
||||
]
|
||||
|
||||
appendComments errRef comments max = do
|
||||
previous <- readIORef errRef
|
||||
let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments
|
||||
writeIORef errRef . take max . nubBy equal . sort $ previous ++ current
|
||||
where
|
||||
fst3 (x,_,_) = x
|
||||
equal x y = fst3 x == fst3 y
|
||||
|
||||
outputWiki :: IORef [(Ranking, Integer, String)] -> IO ()
|
||||
outputWiki errRef = do
|
||||
issues <- readIORef errRef
|
||||
unless (null issues) $ do
|
||||
putStrLn "For more information:"
|
||||
mapM_ showErr issues
|
||||
where
|
||||
showErr (_, code, msg) =
|
||||
putStrLn $ " " ++ wikiLink ++ "SC" ++ show code ++ " -- " ++ shorten msg
|
||||
limit = 36
|
||||
shorten msg =
|
||||
if length msg < limit
|
||||
then msg
|
||||
else (take (limit-3) msg) ++ "..."
|
||||
|
||||
outputError options file error = do
|
||||
color <- getColorFunc $ foColorOption options
|
||||
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
|
||||
|
||||
outputResult options result sys = do
|
||||
outputResult options ref result sys = do
|
||||
color <- getColorFunc $ foColorOption options
|
||||
let comments = crComments result
|
||||
appendComments ref comments (fromIntegral $ foWikiLinkCount options)
|
||||
let fileGroups = groupWith sourceFile comments
|
||||
mapM_ (outputForFile color sys) fileGroups
|
||||
|
||||
@@ -78,9 +134,16 @@ outputForFile color sys comments = do
|
||||
cuteIndent :: PositionedComment -> String
|
||||
cuteIndent comment =
|
||||
replicate (fromIntegral $ colNo comment - 1) ' ' ++
|
||||
"^-- " ++ code (codeNo comment) ++ ": " ++ messageText comment
|
||||
makeArrow ++ " " ++ code (codeNo comment) ++ ": " ++ messageText comment
|
||||
where
|
||||
arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^"
|
||||
makeArrow =
|
||||
let sameLine = lineNo comment == endLineNo comment
|
||||
delta = endColNo comment - colNo comment
|
||||
in
|
||||
if sameLine && delta > 2 && delta < 32 then arrow delta else "^--"
|
||||
|
||||
code code = "SC" ++ show code
|
||||
code num = "SC" ++ show num
|
||||
|
||||
getColorFunc colorOption = do
|
||||
term <- hIsTerminalDevice stdout
|
232
src/ShellCheck/Interface.hs
Normal file
232
src/ShellCheck/Interface.hs
Normal file
@@ -0,0 +1,232 @@
|
||||
{-
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Interface
|
||||
(
|
||||
SystemInterface(..)
|
||||
, CheckSpec(csFilename, csScript, csCheckSourced, csExcludedWarnings, csShellTypeOverride, csMinSeverity)
|
||||
, CheckResult(crFilename, crComments)
|
||||
, ParseSpec(psFilename, psScript, psCheckSourced, psShellTypeOverride)
|
||||
, ParseResult(prComments, prTokenPositions, prRoot)
|
||||
, AnalysisSpec(asScript, asShellType, asExecutionMode, asCheckSourced)
|
||||
, AnalysisResult(arComments)
|
||||
, FormatterOptions(foColorOption, foWikiLinkCount)
|
||||
, Shell(Ksh, Sh, Bash, Dash)
|
||||
, ExecutionMode(Executed, Sourced)
|
||||
, ErrorMessage
|
||||
, Code
|
||||
, Severity(ErrorC, WarningC, InfoC, StyleC)
|
||||
, Position(posFile, posLine, posColumn)
|
||||
, Comment(cSeverity, cCode, cMessage)
|
||||
, PositionedComment(pcStartPos , pcEndPos , pcComment)
|
||||
, ColorOption(ColorAuto, ColorAlways, ColorNever)
|
||||
, TokenComment(tcId, tcComment)
|
||||
, emptyCheckResult
|
||||
, newParseResult
|
||||
, newAnalysisSpec
|
||||
, newAnalysisResult
|
||||
, newFormatterOptions
|
||||
, newPosition
|
||||
, newTokenComment
|
||||
, mockedSystemInterface
|
||||
, newParseSpec
|
||||
, emptyCheckSpec
|
||||
, newPositionedComment
|
||||
, newComment
|
||||
) where
|
||||
|
||||
import ShellCheck.AST
|
||||
import Control.Monad.Identity
|
||||
import qualified Data.Map as Map
|
||||
|
||||
|
||||
newtype SystemInterface m = SystemInterface {
|
||||
-- Read a file by filename, or return an error
|
||||
siReadFile :: String -> m (Either ErrorMessage String)
|
||||
}
|
||||
|
||||
-- ShellCheck input and output
|
||||
data CheckSpec = CheckSpec {
|
||||
csFilename :: String,
|
||||
csScript :: String,
|
||||
csCheckSourced :: Bool,
|
||||
csExcludedWarnings :: [Integer],
|
||||
csShellTypeOverride :: Maybe Shell,
|
||||
csMinSeverity :: Severity
|
||||
} deriving (Show, Eq)
|
||||
|
||||
data CheckResult = CheckResult {
|
||||
crFilename :: String,
|
||||
crComments :: [PositionedComment]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
emptyCheckResult :: CheckResult
|
||||
emptyCheckResult = CheckResult {
|
||||
crFilename = "",
|
||||
crComments = []
|
||||
}
|
||||
|
||||
emptyCheckSpec :: CheckSpec
|
||||
emptyCheckSpec = CheckSpec {
|
||||
csFilename = "",
|
||||
csScript = "",
|
||||
csCheckSourced = False,
|
||||
csExcludedWarnings = [],
|
||||
csShellTypeOverride = Nothing,
|
||||
csMinSeverity = StyleC
|
||||
}
|
||||
|
||||
newParseSpec :: ParseSpec
|
||||
newParseSpec = ParseSpec {
|
||||
psFilename = "",
|
||||
psScript = "",
|
||||
psCheckSourced = False,
|
||||
psShellTypeOverride = Nothing
|
||||
}
|
||||
|
||||
-- Parser input and output
|
||||
data ParseSpec = ParseSpec {
|
||||
psFilename :: String,
|
||||
psScript :: String,
|
||||
psCheckSourced :: Bool,
|
||||
psShellTypeOverride :: Maybe Shell
|
||||
} deriving (Show, Eq)
|
||||
|
||||
data ParseResult = ParseResult {
|
||||
prComments :: [PositionedComment],
|
||||
prTokenPositions :: Map.Map Id (Position, Position),
|
||||
prRoot :: Maybe Token
|
||||
} deriving (Show, Eq)
|
||||
|
||||
newParseResult :: ParseResult
|
||||
newParseResult = ParseResult {
|
||||
prComments = [],
|
||||
prTokenPositions = Map.empty,
|
||||
prRoot = Nothing
|
||||
}
|
||||
|
||||
-- Analyzer input and output
|
||||
data AnalysisSpec = AnalysisSpec {
|
||||
asScript :: Token,
|
||||
asShellType :: Maybe Shell,
|
||||
asExecutionMode :: ExecutionMode,
|
||||
asCheckSourced :: Bool
|
||||
}
|
||||
|
||||
newAnalysisSpec token = AnalysisSpec {
|
||||
asScript = token,
|
||||
asShellType = Nothing,
|
||||
asExecutionMode = Executed,
|
||||
asCheckSourced = False
|
||||
}
|
||||
|
||||
newtype AnalysisResult = AnalysisResult {
|
||||
arComments :: [TokenComment]
|
||||
}
|
||||
|
||||
newAnalysisResult = AnalysisResult {
|
||||
arComments = []
|
||||
}
|
||||
|
||||
-- Formatter options
|
||||
data FormatterOptions = FormatterOptions {
|
||||
foColorOption :: ColorOption,
|
||||
foWikiLinkCount :: Integer
|
||||
}
|
||||
|
||||
newFormatterOptions = FormatterOptions {
|
||||
foColorOption = ColorAuto,
|
||||
foWikiLinkCount = 3
|
||||
}
|
||||
|
||||
|
||||
-- Supporting data types
|
||||
data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
|
||||
data ExecutionMode = Executed | Sourced deriving (Show, Eq)
|
||||
|
||||
type ErrorMessage = String
|
||||
type Code = Integer
|
||||
|
||||
data Severity = ErrorC | WarningC | InfoC | StyleC deriving (Show, Eq, Ord)
|
||||
data Position = Position {
|
||||
posFile :: String, -- Filename
|
||||
posLine :: Integer, -- 1 based source line
|
||||
posColumn :: Integer -- 1 based source column, where tabs are 8
|
||||
} deriving (Show, Eq)
|
||||
|
||||
newPosition :: Position
|
||||
newPosition = Position {
|
||||
posFile = "",
|
||||
posLine = 1,
|
||||
posColumn = 1
|
||||
}
|
||||
|
||||
data Comment = Comment {
|
||||
cSeverity :: Severity,
|
||||
cCode :: Code,
|
||||
cMessage :: String
|
||||
} deriving (Show, Eq)
|
||||
|
||||
newComment :: Comment
|
||||
newComment = Comment {
|
||||
cSeverity = StyleC,
|
||||
cCode = 0,
|
||||
cMessage = ""
|
||||
}
|
||||
|
||||
data PositionedComment = PositionedComment {
|
||||
pcStartPos :: Position,
|
||||
pcEndPos :: Position,
|
||||
pcComment :: Comment
|
||||
} deriving (Show, Eq)
|
||||
|
||||
newPositionedComment :: PositionedComment
|
||||
newPositionedComment = PositionedComment {
|
||||
pcStartPos = newPosition,
|
||||
pcEndPos = newPosition,
|
||||
pcComment = newComment
|
||||
}
|
||||
|
||||
data TokenComment = TokenComment {
|
||||
tcId :: Id,
|
||||
tcComment :: Comment
|
||||
} deriving (Show, Eq)
|
||||
|
||||
newTokenComment = TokenComment {
|
||||
tcId = Id 0,
|
||||
tcComment = newComment
|
||||
}
|
||||
|
||||
data ColorOption =
|
||||
ColorAuto
|
||||
| ColorAlways
|
||||
| ColorNever
|
||||
deriving (Ord, Eq, Show)
|
||||
|
||||
-- For testing
|
||||
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
|
||||
mockedSystemInterface files = SystemInterface {
|
||||
siReadFile = rf
|
||||
}
|
||||
where
|
||||
rf file =
|
||||
case filter ((== file) . fst) files of
|
||||
[] -> return $ Left "File not included in mock."
|
||||
[(_, contents)] -> return $ Right contents
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# This file was automatically generated by stack init
|
||||
# For more information, see: http://docs.haskellstack.org/en/stable/yaml_configuration/
|
||||
# For more information, see: https://docs.haskellstack.org/en/stable/yaml_configuration/
|
||||
|
||||
# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
|
||||
resolver: lts-8.5
|
||||
|
78
striptests
Executable file
78
striptests
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
# This file strips all unit tests from ShellCheck, removing
|
||||
# the dependency on QuickCheck and Template Haskell and
|
||||
# reduces the binary size considerably.
|
||||
set -o pipefail
|
||||
|
||||
sponge() {
|
||||
local data
|
||||
data="$(cat)"
|
||||
printf '%s\n' "$data" > "$1"
|
||||
}
|
||||
|
||||
modify() {
|
||||
if ! "${@:2}" < "$1" | sponge "$1"
|
||||
then
|
||||
{
|
||||
printf 'Failed to modify %s: ' "$1"
|
||||
printf '%q ' "${@:2}"
|
||||
printf '\n'
|
||||
} >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
detestify() {
|
||||
printf '%s\n' '-- AUTOGENERATED from ShellCheck by striptests. Do not modify.'
|
||||
awk '
|
||||
BEGIN {
|
||||
state = 0;
|
||||
}
|
||||
|
||||
/LANGUAGE TemplateHaskell/ { next; }
|
||||
/^import.*Test\./ { next; }
|
||||
|
||||
/^module/ {
|
||||
sub(/,[^,)]*runTests/, "");
|
||||
}
|
||||
|
||||
# Delete tests
|
||||
/^prop_/ { state = 1; next; }
|
||||
|
||||
# ..and any blank lines following them.
|
||||
state == 1 && /^ / { next; }
|
||||
|
||||
# Template Haskell marker
|
||||
/^return / {
|
||||
exit;
|
||||
}
|
||||
|
||||
{ state = 0; print; }
|
||||
'
|
||||
}
|
||||
|
||||
|
||||
|
||||
if [[ ! -e 'ShellCheck.cabal' ]]
|
||||
then
|
||||
echo "Run me from the ShellCheck directory." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -d '.git' ]] && ! git diff --exit-code > /dev/null 2>&1
|
||||
then
|
||||
echo "You have local changes! These may be overwritten." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
modify 'ShellCheck.cabal' sed -e '
|
||||
/QuickCheck/d
|
||||
/^test-suite/{ s/.*//; q; }
|
||||
'
|
||||
|
||||
find . -name '.git' -prune -o -type f -name '*.hs' -print |
|
||||
while IFS= read -r file
|
||||
do
|
||||
modify "$file" detestify
|
||||
done
|
||||
|
35
test/buildtest
Executable file
35
test/buildtest
Executable 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
80
test/distrotest
Executable 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
27
test/stacktest
Executable 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"
|
Reference in New Issue
Block a user