mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 08:49:20 +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
|
#### For bugs
|
||||||
- Rule Id (if any, e.g. SC1000):
|
- Rule Id (if any, e.g. SC1000):
|
||||||
- My shellcheck version (`shellcheck --version` or "online"):
|
- My shellcheck version (`shellcheck --version` or "online"):
|
||||||
|
- [ ] 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
|
- [ ] I tried on shellcheck.net and verified that this is still a problem on the latest commit
|
||||||
- [ ] It's not reproducible on shellcheck.net, but I think that's because it's an OS, configuration or encoding issue
|
|
||||||
|
|
||||||
#### For new checks and feature suggestions
|
#### For new checks and feature suggestions
|
||||||
- [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this
|
- [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this
|
||||||
|
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,4 +1,4 @@
|
|||||||
# Created by http://www.gitignore.io
|
# Created by https://www.gitignore.io
|
||||||
|
|
||||||
### Haskell ###
|
### Haskell ###
|
||||||
dist
|
dist
|
||||||
@@ -13,3 +13,10 @@ cabal-dev
|
|||||||
cabal.sandbox.config
|
cabal.sandbox.config
|
||||||
cabal.config
|
cabal.config
|
||||||
.stack-work
|
.stack-work
|
||||||
|
|
||||||
|
### Snap ###
|
||||||
|
/snap/.snapcraft/
|
||||||
|
/stage/
|
||||||
|
/parts/
|
||||||
|
/prime/
|
||||||
|
*.snap
|
||||||
|
@@ -7,7 +7,7 @@ cd deploy
|
|||||||
cp ../LICENSE LICENSE.txt
|
cp ../LICENSE LICENSE.txt
|
||||||
sed -e $'s/$/\r/' > README.txt << END
|
sed -e $'s/$/\r/' > README.txt << END
|
||||||
This is a precompiled ShellCheck binary.
|
This is a precompiled ShellCheck binary.
|
||||||
http://www.shellcheck.net/
|
https://www.shellcheck.net/
|
||||||
|
|
||||||
ShellCheck is a static analysis tool for shell scripts.
|
ShellCheck is a static analysis tool for shell scripts.
|
||||||
It's licensed under the GNU General Public License v3.0.
|
It's licensed under the GNU General Public License v3.0.
|
||||||
@@ -27,7 +27,7 @@ do
|
|||||||
zip "${file%.*}.zip" README.txt LICENSE.txt "$file"
|
zip "${file%.*}.zip" README.txt LICENSE.txt "$file"
|
||||||
done
|
done
|
||||||
|
|
||||||
for file in *.linux
|
for file in *.linux-x86_64
|
||||||
do
|
do
|
||||||
base="${file%.*}"
|
base="${file%.*}"
|
||||||
cp "$file" "shellcheck"
|
cp "$file" "shellcheck"
|
||||||
@@ -35,6 +35,14 @@ do
|
|||||||
rm "shellcheck"
|
rm "shellcheck"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
for file in *.linux-armv6hf
|
||||||
|
do
|
||||||
|
base="${file%.*}"
|
||||||
|
cp "$file" "shellcheck"
|
||||||
|
tar -cJf "$base.linux.armv6hf.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
|
||||||
|
rm "shellcheck"
|
||||||
|
done
|
||||||
|
|
||||||
for file in ./*
|
for file in ./*
|
||||||
do
|
do
|
||||||
sha512sum "$file" > "$file.sha512sum"
|
sha512sum "$file" > "$file.sha512sum"
|
||||||
|
14
.snapsquid.conf
Normal file
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=""
|
- DOCKER_BUILDS=""
|
||||||
- TAGS=""
|
- TAGS=""
|
||||||
- test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
|
- test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
|
||||||
- test -n "$TRAVIS_TAG" && TAGS="$TAGS $TRAVIS_TAG" || true
|
- test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
|
||||||
- test "$TRAVIS_BRANCH" = master && test -n "$TRAVIS_TAG" && TAGS="$TAGS stable" || true
|
- echo "Tags are $TAGS"
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- mkdir deploy
|
- mkdir deploy
|
||||||
# Windows .exe
|
# Remove all tests to reduce binary size
|
||||||
- docker pull koalaman/winghc
|
- ./striptests
|
||||||
- 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
|
|
||||||
# Linux Docker image
|
# Linux Docker image
|
||||||
- name="$DOCKER_BASE"
|
- name="$DOCKER_BASE"
|
||||||
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
|
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
|
||||||
- docker build -t "$name:current" .
|
- docker build -t "$name:current" .
|
||||||
- docker run "$name:current" --version
|
- 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
|
# Linux Alpine based Docker image
|
||||||
- name="$DOCKER_BASE-alpine"
|
- name="$DOCKER_BASE-alpine"
|
||||||
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
|
- 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 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
|
# Misc packaging
|
||||||
- ./.prepare_deploy
|
- ./.prepare_deploy
|
||||||
|
|
||||||
after_success:
|
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;
|
- for repo in $DOCKER_BUILDS;
|
||||||
do
|
do
|
||||||
for tag in $TAGS;
|
for tag in $TAGS;
|
||||||
do
|
do
|
||||||
echo "Deploying $repo:current as $repo:$tag...";
|
echo "Deploying $repo:current as $repo:$tag...";
|
||||||
docker tag "$repo:current" "$repo:$tag";
|
docker tag "$repo:current" "$repo:$tag" || exit 1;
|
||||||
docker push "$repo:$tag";
|
docker push "$repo:$tag" || exit 1;
|
||||||
done;
|
done;
|
||||||
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
|
## v0.4.7 - 2017-12-08
|
||||||
### Added
|
### Added
|
||||||
- Statically linked binaries for Linux and Windows (see README.md)!
|
- 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>"
|
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
|
||||||
|
COPY --from=build /out /
|
||||||
|
|
||||||
# This file assumes ShellCheck has already been built.
|
# DELETE-MARKER (Remove everything below to keep the alpine image)
|
||||||
# See https://github.com/koalaman/scbuilder
|
|
||||||
COPY shellcheck /bin/shellcheck
|
|
||||||
|
|
||||||
|
# Resulting ShellCheck image
|
||||||
|
FROM scratch
|
||||||
|
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
|
||||||
WORKDIR /mnt
|
WORKDIR /mnt
|
||||||
|
COPY --from=build /out /
|
||||||
ENTRYPOINT ["/bin/shellcheck"]
|
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
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
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
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
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.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <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.
|
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,
|
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.
|
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
|
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
|
The GNU General Public License does not permit incorporating your program
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
may consider it more useful to permit linking proprietary applications with
|
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
|
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
|
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>.
|
||||||
|
100
README.md
100
README.md
@@ -1,8 +1,10 @@
|
|||||||
|
[](https://travis-ci.org/koalaman/shellcheck)
|
||||||
|
|
||||||
# ShellCheck - A shell script static analysis tool
|
# ShellCheck - A shell script static analysis tool
|
||||||
|
|
||||||
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts:
|
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts:
|
||||||
|
|
||||||
.
|

|
||||||
|
|
||||||
The goals of ShellCheck are
|
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 editor](#in-your-editor)
|
||||||
- [In your build or test suites](#in-your-build-or-test-suites)
|
- [In your build or test suites](#in-your-build-or-test-suites)
|
||||||
- [Installing](#installing)
|
- [Installing](#installing)
|
||||||
- [Travis CI Setup](#travis-ci-setup)
|
- [Travis CI](#travis-ci)
|
||||||
- [Compiling from source](#compiling-from-source)
|
- [Compiling from source](#compiling-from-source)
|
||||||
- [Installing Cabal](#installing-cabal)
|
- [Installing Cabal](#installing-cabal)
|
||||||
- [Compiling ShellCheck](#compiling-shellcheck)
|
- [Compiling ShellCheck](#compiling-shellcheck)
|
||||||
@@ -52,9 +54,9 @@ There are a number of ways to use ShellCheck!
|
|||||||
|
|
||||||
### On the web
|
### 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
|
### 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):
|
||||||
|
|
||||||
.
|
.
|
||||||
|
|
||||||
@@ -108,6 +110,8 @@ On Arch Linux based distros:
|
|||||||
|
|
||||||
pacman -S shellcheck
|
pacman -S shellcheck
|
||||||
|
|
||||||
|
or get the dependency free [shellcheck-static](https://aur.archlinux.org/packages/shellcheck-static/) from the AUR.
|
||||||
|
|
||||||
On Gentoo based distros:
|
On Gentoo based distros:
|
||||||
|
|
||||||
emerge --ask shellcheck
|
emerge --ask shellcheck
|
||||||
@@ -121,62 +125,78 @@ On Fedora based distros:
|
|||||||
|
|
||||||
dnf install ShellCheck
|
dnf install ShellCheck
|
||||||
|
|
||||||
|
On FreeBSD:
|
||||||
|
|
||||||
|
pkg install hs-ShellCheck
|
||||||
|
|
||||||
On OS X with homebrew:
|
On OS X with homebrew:
|
||||||
|
|
||||||
brew install shellcheck
|
brew install shellcheck
|
||||||
|
|
||||||
On OS X with MacPorts:
|
On OpenBSD:
|
||||||
|
|
||||||
port install shellcheck
|
pkg_add shellcheck
|
||||||
|
|
||||||
On openSUSE:Tumbleweed:
|
On openSUSE
|
||||||
|
|
||||||
zypper in ShellCheck
|
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
|
Or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
|
||||||
|
|
||||||
On Solus:
|
On Solus:
|
||||||
|
|
||||||
eopkg install shellcheck
|
eopkg install shellcheck
|
||||||
|
|
||||||
|
On Windows (via [scoop](http://scoop.sh)):
|
||||||
|
|
||||||
|
scoop install shellcheck
|
||||||
|
|
||||||
|
From Snap Store:
|
||||||
|
|
||||||
|
snap install --channel=edge shellcheck
|
||||||
|
|
||||||
From Docker Hub:
|
From Docker Hub:
|
||||||
|
|
||||||
```sh
|
```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
|
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)
|
* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
|
||||||
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-latest.zip)
|
* [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
|
## Travis CI
|
||||||
language: bash
|
|
||||||
addons:
|
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
|
||||||
apt:
|
|
||||||
sources:
|
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.
|
||||||
- debian-sid # Grab ShellCheck from the Debian repo
|
|
||||||
packages:
|
## Installing the shellcheck binary
|
||||||
- shellcheck
|
|
||||||
|
*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
|
## 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.
|
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
|
tr -cd '[a-zA-Z0-9]' # [] around ranges in tr
|
||||||
exec foo; echo "Done!" # Misused 'exec'
|
exec foo; echo "Done!" # Misused 'exec'
|
||||||
find -name \*.bak -o -name \*~ -delete # Implicit precedence in find
|
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
|
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
|
echo ${var$n} # Wrong indirect reference
|
||||||
var=(1, 2, 3) # Comma separated arrays
|
var=(1, 2, 3) # Comma separated arrays
|
||||||
array=( [index] = value ) # Incorrect index initialization
|
array=( [index] = value ) # Incorrect index initialization
|
||||||
|
echo $var[14] # Missing {} in array references
|
||||||
echo "Argument 10 is $10" # Positional parameter misreference
|
echo "Argument 10 is $10" # Positional parameter misreference
|
||||||
if $(myfunction); then ..; fi # Wrapping commands in $()
|
if $(myfunction); then ..; fi # Wrapping commands in $()
|
||||||
else if othercondition; then .. # Using 'else if'
|
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
|
### Style
|
||||||
@@ -341,6 +366,8 @@ printf "%s\n" "Arguments: $@." # Concatenating strings and arrays
|
|||||||
var=World; echo "Hello " var # Unused lowercase variables
|
var=World; echo "Hello " var # Unused lowercase variables
|
||||||
echo "Hello $name" # Unassigned lowercase variables
|
echo "Hello $name" # Unassigned lowercase variables
|
||||||
cmd | read bar; echo $bar # Assignments in subshells
|
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
|
### Robustness
|
||||||
@@ -354,6 +381,7 @@ find . -exec sh -c 'a && b {}' \; # Find -exec shell injection
|
|||||||
printf "Hello $name" # Variables in printf format
|
printf "Hello $name" # Variables in printf format
|
||||||
for f in $(ls *.txt); do # Iterating over ls output
|
for f in $(ls *.txt); do # Iterating over ls output
|
||||||
export MYVAR=$(cmd) # Masked exit codes
|
export MYVAR=$(cmd) # Masked exit codes
|
||||||
|
case $version in 2.*) :;; 2.6.*) # Shadowed case branches
|
||||||
```
|
```
|
||||||
|
|
||||||
### Portability
|
### Portability
|
||||||
@@ -388,6 +416,7 @@ var=42 echo $var # Expansion of inlined environment
|
|||||||
echo $((n/180*100)) # Unnecessary loss of precision
|
echo $((n/180*100)) # Unnecessary loss of precision
|
||||||
ls *[:digit:].txt # Bad character class globs
|
ls *[:digit:].txt # Bad character class globs
|
||||||
sed 's/foo/bar/' file > file # Redirecting to input
|
sed 's/foo/bar/' file > file # Redirecting to input
|
||||||
|
while getopts "a" f; do case $f in "b") # Unhandled getopts flags
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testimonials
|
## 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).
|
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!
|
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
|
putStrLn $ "pandoc exited with " ++ show result
|
||||||
return emptyHookedBuildInfo
|
return emptyHookedBuildInfo
|
||||||
where
|
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
|
Name: ShellCheck
|
||||||
Version: 0.4.7
|
Version: 0.6.0
|
||||||
Synopsis: Shell script analysis tool
|
Synopsis: Shell script analysis tool
|
||||||
License: GPL-3
|
License: GPL-3
|
||||||
License-file: LICENSE
|
License-file: LICENSE
|
||||||
Category: Static Analysis
|
Category: Static Analysis
|
||||||
Author: Vidar Holen
|
Author: Vidar Holen
|
||||||
Maintainer: vidar@vidarholen.net
|
Maintainer: vidar@vidarholen.net
|
||||||
Homepage: http://www.shellcheck.net/
|
Homepage: https://www.shellcheck.net/
|
||||||
Build-Type: Custom
|
Build-Type: Custom
|
||||||
Cabal-Version: >= 1.8
|
Cabal-Version: >= 1.8
|
||||||
Bug-reports: https://github.com/koalaman/shellcheck/issues
|
Bug-reports: https://github.com/koalaman/shellcheck/issues
|
||||||
@@ -28,19 +28,34 @@ Extra-Source-Files:
|
|||||||
shellcheck.1.md
|
shellcheck.1.md
|
||||||
-- built with a cabal sdist hook
|
-- built with a cabal sdist hook
|
||||||
shellcheck.1
|
shellcheck.1
|
||||||
|
-- convenience script for stripping tests
|
||||||
|
striptests
|
||||||
-- tests
|
-- tests
|
||||||
test/shellcheck.hs
|
test/shellcheck.hs
|
||||||
|
|
||||||
|
custom-setup
|
||||||
|
setup-depends:
|
||||||
|
base >= 4 && <5,
|
||||||
|
process >= 1.0 && <1.7,
|
||||||
|
Cabal >= 1.10 && <2.5
|
||||||
|
|
||||||
source-repository head
|
source-repository head
|
||||||
type: git
|
type: git
|
||||||
location: git://github.com/koalaman/shellcheck.git
|
location: git://github.com/koalaman/shellcheck.git
|
||||||
|
|
||||||
library
|
library
|
||||||
|
hs-source-dirs: src
|
||||||
|
if impl(ghc < 8.0)
|
||||||
build-depends:
|
build-depends:
|
||||||
base >= 4 && < 5,
|
semigroups
|
||||||
containers,
|
build-depends:
|
||||||
|
-- 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,
|
directory,
|
||||||
json,
|
|
||||||
mtl >= 2.2.1,
|
mtl >= 2.2.1,
|
||||||
parsec,
|
parsec,
|
||||||
regex-tdfa,
|
regex-tdfa,
|
||||||
@@ -69,27 +84,34 @@ library
|
|||||||
Paths_ShellCheck
|
Paths_ShellCheck
|
||||||
|
|
||||||
executable shellcheck
|
executable shellcheck
|
||||||
|
if impl(ghc < 8.0)
|
||||||
build-depends:
|
build-depends:
|
||||||
|
semigroups
|
||||||
|
build-depends:
|
||||||
|
aeson,
|
||||||
base >= 4 && < 5,
|
base >= 4 && < 5,
|
||||||
|
bytestring,
|
||||||
|
ShellCheck,
|
||||||
containers,
|
containers,
|
||||||
directory,
|
directory,
|
||||||
json,
|
|
||||||
mtl >= 2.2.1,
|
mtl >= 2.2.1,
|
||||||
parsec,
|
parsec >= 3.0,
|
||||||
regex-tdfa,
|
QuickCheck >= 2.7.4,
|
||||||
QuickCheck >= 2.7.4
|
regex-tdfa
|
||||||
main-is: shellcheck.hs
|
main-is: shellcheck.hs
|
||||||
|
|
||||||
test-suite test-shellcheck
|
test-suite test-shellcheck
|
||||||
type: exitcode-stdio-1.0
|
type: exitcode-stdio-1.0
|
||||||
build-depends:
|
build-depends:
|
||||||
|
aeson,
|
||||||
base >= 4 && < 5,
|
base >= 4 && < 5,
|
||||||
|
bytestring,
|
||||||
|
ShellCheck,
|
||||||
containers,
|
containers,
|
||||||
directory,
|
directory,
|
||||||
json,
|
|
||||||
mtl >= 2.2.1,
|
mtl >= 2.2.1,
|
||||||
parsec,
|
parsec,
|
||||||
regex-tdfa,
|
QuickCheck >= 2.7.4,
|
||||||
QuickCheck >= 2.7.4
|
regex-tdfa
|
||||||
main-is: test/shellcheck.hs
|
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
|
# TODO: Find a less trashy way to get the next available error code
|
||||||
|
if ! shopt -s globstar
|
||||||
shopt -s globstar
|
then
|
||||||
|
echo "Error: This script depends on Bash 4." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
for i in 1 2
|
for i in 1 2
|
||||||
do
|
do
|
||||||
|
4
quickrun
4
quickrun
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
# quickrun runs ShellCheck in an interpreted mode.
|
# quickrun runs ShellCheck in an interpreted mode.
|
||||||
# This allows testing changes without recompiling.
|
# This allows testing changes without recompiling.
|
||||||
|
|
||||||
runghc -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.
|
# quicktest runs the ShellCheck unit tests in an interpreted mode.
|
||||||
# This allows running tests without compiling, which can be faster.
|
# This allows running tests without compiling, which can be faster.
|
||||||
# 'cabal test' remains the source of truth.
|
# '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**
|
standard output. Subsequent **-f** options are ignored, see **FORMATS**
|
||||||
below for more information.
|
below for more information.
|
||||||
|
|
||||||
|
**-S**\ *SEVERITY*,\ **--severity=***severity*
|
||||||
|
|
||||||
|
: Specify minimum severity of errors to consider. Valid values are *error*,
|
||||||
|
*warning*, *info* and *style*. The default is *style*.
|
||||||
|
|
||||||
**-s**\ *shell*,\ **--shell=***shell*
|
**-s**\ *shell*,\ **--shell=***shell*
|
||||||
|
|
||||||
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
|
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
|
||||||
@@ -66,6 +71,11 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
|||||||
|
|
||||||
: Print version information and exit.
|
: Print version information and exit.
|
||||||
|
|
||||||
|
**-W** *NUM*,\ **--wiki-link-count=NUM**
|
||||||
|
|
||||||
|
: For TTY output, show *NUM* wiki links to more information about mentioned
|
||||||
|
warnings. Set to 0 to disable them entirely.
|
||||||
|
|
||||||
**-x**,\ **--external-sources**
|
**-x**,\ **--external-sources**
|
||||||
|
|
||||||
: Follow 'source' statements even when the file is not specified as input.
|
: Follow 'source' statements even when the file is not specified as input.
|
||||||
@@ -207,7 +217,7 @@ https://github.com/koalaman/shellcheck/issues
|
|||||||
# COPYRIGHT
|
# COPYRIGHT
|
||||||
Copyright 2012-2015, Vidar Holen.
|
Copyright 2012-2015, Vidar Holen.
|
||||||
Licensed under the GNU General Public License version 3 or later,
|
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
|
# SEE ALSO
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
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
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,15 +15,15 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <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.Checker
|
||||||
|
import ShellCheck.Data
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Regex
|
import ShellCheck.Regex
|
||||||
|
|
||||||
import ShellCheck.Formatter.Format
|
|
||||||
import qualified ShellCheck.Formatter.CheckStyle
|
import qualified ShellCheck.Formatter.CheckStyle
|
||||||
|
import ShellCheck.Formatter.Format
|
||||||
import qualified ShellCheck.Formatter.GCC
|
import qualified ShellCheck.Formatter.GCC
|
||||||
import qualified ShellCheck.Formatter.JSON
|
import qualified ShellCheck.Formatter.JSON
|
||||||
import qualified ShellCheck.Formatter.TTY
|
import qualified ShellCheck.Formatter.TTY
|
||||||
@@ -40,6 +40,7 @@ import Data.List
|
|||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
|
import Data.Semigroup (Semigroup (..))
|
||||||
import Prelude hiding (catch)
|
import Prelude hiding (catch)
|
||||||
import System.Console.GetOpt
|
import System.Console.GetOpt
|
||||||
import System.Directory
|
import System.Directory
|
||||||
@@ -56,22 +57,27 @@ data Status =
|
|||||||
| RuntimeException
|
| RuntimeException
|
||||||
deriving (Ord, Eq, Show)
|
deriving (Ord, Eq, Show)
|
||||||
|
|
||||||
|
instance Semigroup Status where
|
||||||
|
(<>) = max
|
||||||
|
|
||||||
instance Monoid Status where
|
instance Monoid Status where
|
||||||
mempty = NoProblems
|
mempty = NoProblems
|
||||||
mappend = max
|
mappend = (Data.Semigroup.<>)
|
||||||
|
|
||||||
data Options = Options {
|
data Options = Options {
|
||||||
checkSpec :: CheckSpec,
|
checkSpec :: CheckSpec,
|
||||||
externalSources :: Bool,
|
externalSources :: Bool,
|
||||||
formatterOptions :: FormatterOptions
|
formatterOptions :: FormatterOptions,
|
||||||
|
minSeverity :: Severity
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultOptions = Options {
|
defaultOptions = Options {
|
||||||
checkSpec = emptyCheckSpec,
|
checkSpec = emptyCheckSpec,
|
||||||
externalSources = False,
|
externalSources = False,
|
||||||
formatterOptions = FormatterOptions {
|
formatterOptions = newFormatterOptions {
|
||||||
foColorOption = ColorAuto
|
foColorOption = ColorAuto
|
||||||
}
|
},
|
||||||
|
minSeverity = StyleC
|
||||||
}
|
}
|
||||||
|
|
||||||
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
|
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
|
||||||
@@ -89,8 +95,14 @@ options = [
|
|||||||
Option "s" ["shell"]
|
Option "s" ["shell"]
|
||||||
(ReqArg (Flag "shell") "SHELLNAME")
|
(ReqArg (Flag "shell") "SHELLNAME")
|
||||||
"Specify dialect (sh, bash, dash, ksh)",
|
"Specify dialect (sh, bash, dash, ksh)",
|
||||||
|
Option "S" ["severity"]
|
||||||
|
(ReqArg (Flag "severity") "SEVERITY")
|
||||||
|
"Minimum severity of errors to consider (error, warning, info, style)",
|
||||||
Option "V" ["version"]
|
Option "V" ["version"]
|
||||||
(NoArg $ Flag "version" "true") "Print version information",
|
(NoArg $ Flag "version" "true") "Print version information",
|
||||||
|
Option "W" ["wiki-link-count"]
|
||||||
|
(ReqArg (Flag "wiki-link-count") "NUM")
|
||||||
|
"The number of wiki links to show, when applicable.",
|
||||||
Option "x" ["external-sources"]
|
Option "x" ["external-sources"]
|
||||||
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES"
|
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES"
|
||||||
]
|
]
|
||||||
@@ -133,12 +145,6 @@ split char str =
|
|||||||
else split' rest (a:element)
|
else split' rest (a:element)
|
||||||
split' [] element = [reverse element]
|
split' [] element = [reverse element]
|
||||||
|
|
||||||
getExclusions options =
|
|
||||||
let elements = concatMap (split ',') $ getOptions options "exclude"
|
|
||||||
clean = dropWhile (not . isDigit)
|
|
||||||
in
|
|
||||||
map (Prelude.read . clean) elements :: [Int]
|
|
||||||
|
|
||||||
toStatus = fmap (either id id) . runExceptT
|
toStatus = fmap (either id id) . runExceptT
|
||||||
|
|
||||||
getEnvArgs = do
|
getEnvArgs = do
|
||||||
@@ -203,7 +209,7 @@ runFormatter sys format options files = do
|
|||||||
|
|
||||||
process :: FilePath -> IO Status
|
process :: FilePath -> IO Status
|
||||||
process filename = do
|
process filename = do
|
||||||
input <- (siReadFile sys) filename
|
input <- siReadFile sys filename
|
||||||
either (reportFailure filename) check input
|
either (reportFailure filename) check input
|
||||||
where
|
where
|
||||||
check contents = do
|
check contents = do
|
||||||
@@ -218,12 +224,28 @@ runFormatter sys format options files = do
|
|||||||
then NoProblems
|
then NoProblems
|
||||||
else SomeProblems
|
else SomeProblems
|
||||||
|
|
||||||
parseColorOption colorOption =
|
parseEnum name value list =
|
||||||
case colorOption of
|
case filter ((== value) . fst) list of
|
||||||
"auto" -> ColorAuto
|
[(name, value)] -> return value
|
||||||
"always" -> ColorAlways
|
[] -> do
|
||||||
"never" -> ColorNever
|
printErr $ "Unknown value for --" ++ name ++ ". " ++
|
||||||
_ -> error $ "Bad value for --color `" ++ colorOption ++ "'"
|
"Valid options are: " ++ (intercalate ", " $ map fst list)
|
||||||
|
throwError SupportFailure
|
||||||
|
|
||||||
|
parseColorOption value =
|
||||||
|
parseEnum "color" value [
|
||||||
|
("auto", ColorAuto),
|
||||||
|
("always", ColorAlways),
|
||||||
|
("never", ColorNever)
|
||||||
|
]
|
||||||
|
|
||||||
|
parseSeverityOption value =
|
||||||
|
parseEnum "severity" value [
|
||||||
|
("error", ErrorC),
|
||||||
|
("warning", WarningC),
|
||||||
|
("info", InfoC),
|
||||||
|
("style", StyleC)
|
||||||
|
]
|
||||||
|
|
||||||
parseOption flag options =
|
parseOption flag options =
|
||||||
case flag of
|
case flag of
|
||||||
@@ -237,7 +259,7 @@ parseOption flag options =
|
|||||||
}
|
}
|
||||||
|
|
||||||
Flag "exclude" str -> do
|
Flag "exclude" str -> do
|
||||||
new <- mapM parseNum $ split ',' str
|
new <- mapM parseNum $ filter (not . null) $ split ',' str
|
||||||
let old = csExcludedWarnings . checkSpec $ options
|
let old = csExcludedWarnings . checkSpec $ options
|
||||||
return options {
|
return options {
|
||||||
checkSpec = (checkSpec options) {
|
checkSpec = (checkSpec options) {
|
||||||
@@ -254,10 +276,11 @@ parseOption flag options =
|
|||||||
externalSources = True
|
externalSources = True
|
||||||
}
|
}
|
||||||
|
|
||||||
Flag "color" color ->
|
Flag "color" color -> do
|
||||||
|
option <- parseColorOption color
|
||||||
return options {
|
return options {
|
||||||
formatterOptions = (formatterOptions options) {
|
formatterOptions = (formatterOptions options) {
|
||||||
foColorOption = parseColorOption color
|
foColorOption = option
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
_ -> return options
|
||||||
where
|
where
|
||||||
die s = do
|
die s = do
|
||||||
@@ -276,7 +315,7 @@ parseOption flag options =
|
|||||||
parseNum ('S':'C':str) = parseNum str
|
parseNum ('S':'C':str) = parseNum str
|
||||||
parseNum num = do
|
parseNum num = do
|
||||||
unless (all isDigit num) $ do
|
unless (all isDigit num) $ do
|
||||||
printErr $ "Bad exclusion: " ++ num
|
printErr $ "Invalid number: " ++ num
|
||||||
throwError SyntaxFailure
|
throwError SyntaxFailure
|
||||||
return (Prelude.read num :: Integer)
|
return (Prelude.read num :: Integer)
|
||||||
|
|
||||||
@@ -378,4 +417,4 @@ printVersion = do
|
|||||||
putStrLn "ShellCheck - shell script analysis tool"
|
putStrLn "ShellCheck - shell script analysis tool"
|
||||||
putStrLn $ "version: " ++ shellcheckVersion
|
putStrLn $ "version: " ++ shellcheckVersion
|
||||||
putStrLn "license: GNU General Public License, version 3"
|
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
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
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
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.AST where
|
module ShellCheck.AST where
|
||||||
|
|
||||||
@@ -37,8 +37,8 @@ newtype Root = Root Token
|
|||||||
data Token =
|
data Token =
|
||||||
TA_Binary Id String Token Token
|
TA_Binary Id String Token Token
|
||||||
| TA_Assignment Id String Token Token
|
| TA_Assignment Id String Token Token
|
||||||
|
| TA_Variable Id String [Token]
|
||||||
| TA_Expansion Id [Token]
|
| TA_Expansion Id [Token]
|
||||||
| TA_Index Id Token
|
|
||||||
| TA_Sequence Id [Token]
|
| TA_Sequence Id [Token]
|
||||||
| TA_Trinary Id Token Token Token
|
| TA_Trinary Id Token Token Token
|
||||||
| TA_Unary Id String Token
|
| TA_Unary Id String Token
|
||||||
@@ -134,7 +134,8 @@ data Token =
|
|||||||
| T_Pipe Id String
|
| T_Pipe Id String
|
||||||
| T_CoProc Id (Maybe String) Token
|
| T_CoProc Id (Maybe String) Token
|
||||||
| T_CoProcBody Id 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)
|
deriving (Show)
|
||||||
|
|
||||||
data Annotation =
|
data Annotation =
|
||||||
@@ -266,11 +267,12 @@ analyze f g i =
|
|||||||
c <- round t3
|
c <- round t3
|
||||||
return $ TA_Trinary id a b c
|
return $ TA_Trinary id a b c
|
||||||
delve (TA_Expansion id t) = dl t $ TA_Expansion id
|
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_Annotation id anns t) = d1 t $ T_Annotation id anns
|
||||||
delve (T_CoProc id var body) = d1 body $ T_CoProc id var
|
delve (T_CoProc id var body) = d1 body $ T_CoProc id var
|
||||||
delve (T_CoProcBody id t) = d1 t $ T_CoProcBody id
|
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
|
delve t = return t
|
||||||
|
|
||||||
getId :: Token -> Id
|
getId :: Token -> Id
|
||||||
@@ -360,7 +362,6 @@ getId t = case t of
|
|||||||
TA_Sequence id _ -> id
|
TA_Sequence id _ -> id
|
||||||
TA_Trinary id _ _ _ -> id
|
TA_Trinary id _ _ _ -> id
|
||||||
TA_Expansion id _ -> id
|
TA_Expansion id _ -> id
|
||||||
TA_Index id _ -> id
|
|
||||||
T_ProcSub id _ _ -> id
|
T_ProcSub id _ _ -> id
|
||||||
T_Glob id _ -> id
|
T_Glob id _ -> id
|
||||||
T_ForArithmetic id _ _ _ _ -> id
|
T_ForArithmetic id _ _ _ _ -> id
|
||||||
@@ -371,9 +372,11 @@ getId t = case t of
|
|||||||
T_Pipe id _ -> id
|
T_Pipe id _ -> id
|
||||||
T_CoProc id _ _ -> id
|
T_CoProc id _ _ -> id
|
||||||
T_CoProcBody id _ -> id
|
T_CoProcBody id _ -> id
|
||||||
T_Include id _ _ -> id
|
T_Include id _ -> id
|
||||||
|
T_SourceCommand id _ _ -> id
|
||||||
T_UnparsedIndex id _ _ -> id
|
T_UnparsedIndex id _ _ -> id
|
||||||
TC_Empty id _ -> id
|
TC_Empty id _ -> id
|
||||||
|
TA_Variable id _ _ -> id
|
||||||
|
|
||||||
blank :: Monad m => Token -> m ()
|
blank :: Monad m => Token -> m ()
|
||||||
blank = const $ return ()
|
blank = const $ return ()
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
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
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.ASTLib where
|
module ShellCheck.ASTLib where
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@ import ShellCheck.AST
|
|||||||
|
|
||||||
import Control.Monad.Writer
|
import Control.Monad.Writer
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
|
import Data.Char
|
||||||
import Data.Functor
|
import Data.Functor
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
@@ -112,6 +113,7 @@ getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
|
|||||||
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
|
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
|
||||||
|
|
||||||
-- Get all flags in a GNU way, up until --
|
-- Get all flags in a GNU way, up until --
|
||||||
|
getAllFlags :: Token -> [(Token, String)]
|
||||||
getAllFlags = getFlagsUntil (== "--")
|
getAllFlags = getFlagsUntil (== "--")
|
||||||
-- Get all flags in a BSD way, up until first non-flag argument or --
|
-- Get all flags in a BSD way, up until first non-flag argument or --
|
||||||
getLeadingFlags = getFlagsUntil (\x -> x == "--" || (not $ "-" `isPrefixOf` x))
|
getLeadingFlags = getFlagsUntil (\x -> x == "--" || (not $ "-" `isPrefixOf` x))
|
||||||
@@ -225,8 +227,43 @@ getLiteralStringExt more = g
|
|||||||
g (T_SingleQuoted _ s) = return s
|
g (T_SingleQuoted _ s) = return s
|
||||||
g (T_Literal _ s) = return s
|
g (T_Literal _ s) = return s
|
||||||
g (T_ParamSubSpecialChar _ s) = return s
|
g (T_ParamSubSpecialChar _ s) = return s
|
||||||
|
g (T_DollarSingleQuoted _ s) = return $ decodeEscapes s
|
||||||
g x = more x
|
g x = more x
|
||||||
|
|
||||||
|
-- Bash style $'..' decoding
|
||||||
|
decodeEscapes ('\\':c:cs) =
|
||||||
|
case c of
|
||||||
|
'a' -> '\a' : rest
|
||||||
|
'b' -> '\b' : rest
|
||||||
|
'e' -> '\x1B' : rest
|
||||||
|
'f' -> '\f' : rest
|
||||||
|
'n' -> '\n' : rest
|
||||||
|
'r' -> '\r' : rest
|
||||||
|
't' -> '\t' : rest
|
||||||
|
'v' -> '\v' : rest
|
||||||
|
'\'' -> '\'' : rest
|
||||||
|
'"' -> '"' : rest
|
||||||
|
'\\' -> '\\' : rest
|
||||||
|
'x' ->
|
||||||
|
case cs of
|
||||||
|
(x:y:more) ->
|
||||||
|
if isHexDigit x && isHexDigit y
|
||||||
|
then chr (16*(digitToInt x) + (digitToInt y)) : rest
|
||||||
|
else '\\':c:rest
|
||||||
|
_ | isOctDigit c ->
|
||||||
|
let digits = take 3 $ takeWhile isOctDigit (c:cs)
|
||||||
|
num = parseOct digits
|
||||||
|
in (if num < 256 then chr num else '?') : rest
|
||||||
|
_ -> '\\' : c : rest
|
||||||
|
where
|
||||||
|
rest = decodeEscapes cs
|
||||||
|
parseOct = f 0
|
||||||
|
where
|
||||||
|
f n "" = n
|
||||||
|
f n (c:rest) = f (n * 8 + digitToInt c) rest
|
||||||
|
decodeEscapes (c:cs) = c : decodeEscapes cs
|
||||||
|
decodeEscapes [] = []
|
||||||
|
|
||||||
-- Is this token a string literal?
|
-- Is this token a string literal?
|
||||||
isLiteral t = isJust $ getLiteralString t
|
isLiteral t = isJust $ getLiteralString t
|
||||||
|
|
||||||
@@ -256,17 +293,27 @@ getCommand t =
|
|||||||
T_Annotation _ _ t -> getCommand t
|
T_Annotation _ _ t -> getCommand t
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
-- Maybe get the command name of a token representing a command
|
-- Maybe get the command name string of a token representing a command
|
||||||
getCommandName t = do
|
getCommandName :: Token -> Maybe String
|
||||||
|
getCommandName = fst . getCommandNameAndToken
|
||||||
|
|
||||||
|
-- Get the command name token from a command, i.e.
|
||||||
|
-- the token representing 'ls' in 'ls -la 2> foo'.
|
||||||
|
-- If it can't be determined, return the original token.
|
||||||
|
getCommandTokenOrThis = snd . getCommandNameAndToken
|
||||||
|
|
||||||
|
getCommandNameAndToken :: Token -> (Maybe String, Token)
|
||||||
|
getCommandNameAndToken t = fromMaybe (Nothing, t) $ do
|
||||||
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
|
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
|
||||||
s <- getLiteralString w
|
s <- getLiteralString w
|
||||||
if "busybox" `isSuffixOf` s
|
if "busybox" `isSuffixOf` s || "builtin" == s
|
||||||
then
|
then
|
||||||
case rest of
|
case rest of
|
||||||
(applet:_) -> getLiteralString applet
|
(applet:_) -> return (getLiteralString applet, applet)
|
||||||
_ -> return s
|
_ -> return (Just s, w)
|
||||||
else
|
else
|
||||||
return s
|
return (Just s, w)
|
||||||
|
|
||||||
|
|
||||||
-- If a command substitution is a single command, get its name.
|
-- If a command substitution is a single command, get its name.
|
||||||
-- $(date +%s) = Just "date"
|
-- $(date +%s) = Just "date"
|
||||||
@@ -434,3 +481,12 @@ pseudoGlobIsSuperSetof = matchable
|
|||||||
|
|
||||||
wordsCanBeEqual x y = fromMaybe True $
|
wordsCanBeEqual x y = fromMaybe True $
|
||||||
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
|
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
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
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
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,9 +15,10 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <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
|
module ShellCheck.Analytics (runAnalytics, ShellCheck.Analytics.runTests) where
|
||||||
|
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
@@ -119,7 +120,6 @@ nodeChecks = [
|
|||||||
,checkGlobbedRegex
|
,checkGlobbedRegex
|
||||||
,checkTestRedirects
|
,checkTestRedirects
|
||||||
,checkIndirectExpansion
|
,checkIndirectExpansion
|
||||||
,checkSudoRedirect
|
|
||||||
,checkPS1Assignments
|
,checkPS1Assignments
|
||||||
,checkBackticks
|
,checkBackticks
|
||||||
,checkInexplicablyUnquoted
|
,checkInexplicablyUnquoted
|
||||||
@@ -166,6 +166,10 @@ nodeChecks = [
|
|||||||
,checkFlagAsCommand
|
,checkFlagAsCommand
|
||||||
,checkEmptyCondition
|
,checkEmptyCondition
|
||||||
,checkPipeToNowhere
|
,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
|
mapM_ (f . (\ n -> take (length l) $ drop n commands)) indices
|
||||||
return . not . null $ indices
|
return . not . null $ indices
|
||||||
for' l f = for l (first f)
|
for' l f = for l (first f)
|
||||||
first func (x:_) = func (getId x)
|
first func (x:_) = func (getId $ getCommandTokenOrThis x)
|
||||||
first _ _ = return ()
|
first _ _ = return ()
|
||||||
hasShortParameter char = any (\x -> "-" `isPrefixOf` x && char `elem` x)
|
hasShortParameter char = any (\x -> "-" `isPrefixOf` x && char `elem` x)
|
||||||
hasParameter string =
|
hasParameter string =
|
||||||
@@ -428,17 +432,22 @@ prop_checkShebang4 = verifyNotTree checkShebang "#shellcheck shell=sh\nfoo"
|
|||||||
prop_checkShebang5 = verifyTree checkShebang "#!/usr/bin/env ash"
|
prop_checkShebang5 = verifyTree checkShebang "#!/usr/bin/env ash"
|
||||||
prop_checkShebang6 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=dash\n"
|
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_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) =
|
checkShebang params (T_Annotation _ list t) =
|
||||||
if any isOverride list then [] else checkShebang params t
|
if any isOverride list then [] else checkShebang params t
|
||||||
where
|
where
|
||||||
isOverride (ShellOverride _) = True
|
isOverride (ShellOverride _) = True
|
||||||
isOverride _ = False
|
isOverride _ = False
|
||||||
checkShebang params (T_Script id sb _) = execWriter $
|
checkShebang params (T_Script id sb _) = execWriter $ do
|
||||||
unless (shellTypeSpecified params) $ do
|
unless (shellTypeSpecified params) $ do
|
||||||
when (sb == "") $
|
when (sb == "") $
|
||||||
err id 2148 "Tips depend on target shell and yours is unknown. Add a shebang."
|
err id 2148 "Tips depend on target shell and yours is unknown. Add a shebang."
|
||||||
when (executableFromShebang sb == "ash") $
|
when (executableFromShebang sb == "ash") $
|
||||||
warn id 2187 "Ash scripts will be checked as Dash. Add '# shellcheck shell=dash' to silence."
|
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"
|
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_checkRedirectToSame5 = verifyNot checkRedirectToSame "foo > bar 2> bar"
|
||||||
prop_checkRedirectToSame6 = verifyNot checkRedirectToSame "echo foo > foo"
|
prop_checkRedirectToSame6 = verifyNot checkRedirectToSame "echo foo > foo"
|
||||||
prop_checkRedirectToSame7 = verifyNot checkRedirectToSame "sed 's/foo/bar/g' file | sponge file"
|
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) =
|
checkRedirectToSame params s@(T_Pipeline _ _ list) =
|
||||||
mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurrences x) l) (getAllRedirs list))) list
|
mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurrences x) l) (getAllRedirs list))) list
|
||||||
where
|
where
|
||||||
@@ -582,7 +592,8 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) =
|
|||||||
&& x == y
|
&& x == y
|
||||||
&& not (isOutput t && isOutput u)
|
&& not (isOutput t && isOutput u)
|
||||||
&& not (special t)
|
&& not (special t)
|
||||||
&& not (any isHarmlessCommand [t,u])) $ do
|
&& not (any isHarmlessCommand [t,u])
|
||||||
|
&& not (any containsAssignment [u])) $ do
|
||||||
addComment $ note newId
|
addComment $ note newId
|
||||||
addComment $ note exceptId
|
addComment $ note exceptId
|
||||||
checkOccurrences _ _ = return ()
|
checkOccurrences _ _ = return ()
|
||||||
@@ -609,6 +620,9 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) =
|
|||||||
cmd <- getClosestCommand (parentMap params) arg
|
cmd <- getClosestCommand (parentMap params) arg
|
||||||
name <- getCommandBasename cmd
|
name <- getCommandBasename cmd
|
||||||
return $ name `elem` ["echo", "printf", "sponge"]
|
return $ name `elem` ["echo", "printf", "sponge"]
|
||||||
|
containsAssignment arg = fromMaybe False $ do
|
||||||
|
cmd <- getClosestCommand (parentMap params) arg
|
||||||
|
return $ isAssignment cmd
|
||||||
|
|
||||||
checkRedirectToSame _ _ = return ()
|
checkRedirectToSame _ _ = return ()
|
||||||
|
|
||||||
@@ -702,6 +716,7 @@ prop_checkArrayWithoutIndex5 = verifyTree checkArrayWithoutIndex "a[0]=foo; echo
|
|||||||
prop_checkArrayWithoutIndex6 = verifyTree checkArrayWithoutIndex "echo $PIPESTATUS"
|
prop_checkArrayWithoutIndex6 = verifyTree checkArrayWithoutIndex "echo $PIPESTATUS"
|
||||||
prop_checkArrayWithoutIndex7 = verifyTree checkArrayWithoutIndex "a=(a b); a+=c"
|
prop_checkArrayWithoutIndex7 = verifyTree checkArrayWithoutIndex "a=(a b); a+=c"
|
||||||
prop_checkArrayWithoutIndex8 = verifyTree checkArrayWithoutIndex "declare -a foo; foo=bar;"
|
prop_checkArrayWithoutIndex8 = verifyTree checkArrayWithoutIndex "declare -a foo; foo=bar;"
|
||||||
|
prop_checkArrayWithoutIndex9 = verifyTree checkArrayWithoutIndex "read -r -a arr <<< 'foo bar'; echo \"$arr\""
|
||||||
checkArrayWithoutIndex params _ =
|
checkArrayWithoutIndex params _ =
|
||||||
doVariableFlowAnalysis readF writeF defaultMap (variableFlow params)
|
doVariableFlowAnalysis readF writeF defaultMap (variableFlow params)
|
||||||
where
|
where
|
||||||
@@ -742,6 +757,7 @@ prop_checkStderrRedirect3 = verifyNot checkStderrRedirect "test 2>&1 > file | gr
|
|||||||
prop_checkStderrRedirect4 = verifyNot checkStderrRedirect "errors=$(test 2>&1 > file)"
|
prop_checkStderrRedirect4 = verifyNot checkStderrRedirect "errors=$(test 2>&1 > file)"
|
||||||
prop_checkStderrRedirect5 = verifyNot checkStderrRedirect "read < <(test 2>&1 > file)"
|
prop_checkStderrRedirect5 = verifyNot checkStderrRedirect "read < <(test 2>&1 > file)"
|
||||||
prop_checkStderrRedirect6 = verify checkStderrRedirect "foo | bar 2>&1 > /dev/null"
|
prop_checkStderrRedirect6 = verify checkStderrRedirect "foo | bar 2>&1 > /dev/null"
|
||||||
|
prop_checkStderrRedirect7 = verifyNot checkStderrRedirect "{ cmd > file; } 2>&1"
|
||||||
checkStderrRedirect params redir@(T_Redirecting _ [
|
checkStderrRedirect params redir@(T_Redirecting _ [
|
||||||
T_FdRedirect id "2" (T_IoDuplicate _ (T_GREATAND _) "1"),
|
T_FdRedirect id "2" (T_IoDuplicate _ (T_GREATAND _) "1"),
|
||||||
T_FdRedirect _ _ (T_IoFile _ op _)
|
T_FdRedirect _ _ (T_IoFile _ op _)
|
||||||
@@ -760,7 +776,7 @@ checkStderrRedirect params redir@(T_Redirecting _ [
|
|||||||
isCaptured = any usesOutput $ getPath (parentMap params) redir
|
isCaptured = any usesOutput $ getPath (parentMap params) redir
|
||||||
|
|
||||||
error = unless isCaptured $
|
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 ()
|
checkStderrRedirect _ _ = return ()
|
||||||
|
|
||||||
@@ -786,6 +802,10 @@ prop_checkSingleQuotedVariables11= verifyNot checkSingleQuotedVariables "sed '${
|
|||||||
prop_checkSingleQuotedVariables12= verifyNot checkSingleQuotedVariables "eval 'echo $1'"
|
prop_checkSingleQuotedVariables12= verifyNot checkSingleQuotedVariables "eval 'echo $1'"
|
||||||
prop_checkSingleQuotedVariables13= verifyNot checkSingleQuotedVariables "busybox awk '{print $1}'"
|
prop_checkSingleQuotedVariables13= verifyNot checkSingleQuotedVariables "busybox awk '{print $1}'"
|
||||||
prop_checkSingleQuotedVariables14= verifyNot checkSingleQuotedVariables "[ -v 'bar[$foo]' ]"
|
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) =
|
checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
||||||
when (s `matches` re) $
|
when (s `matches` re) $
|
||||||
if "sed" == commandName
|
if "sed" == commandName
|
||||||
@@ -798,7 +818,7 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
|||||||
commandName = fromMaybe "" $ do
|
commandName = fromMaybe "" $ do
|
||||||
cmd <- getClosestCommand parents t
|
cmd <- getClosestCommand parents t
|
||||||
name <- getCommandBasename cmd
|
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 =
|
isProbablyOk =
|
||||||
any isOkAssignment (take 3 $ getPath parents t)
|
any isOkAssignment (take 3 $ getPath parents t)
|
||||||
@@ -813,8 +833,12 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
|||||||
,"xprop"
|
,"xprop"
|
||||||
,"alias"
|
,"alias"
|
||||||
,"sudo" -- covering "sudo sh" and such
|
,"sudo" -- covering "sudo sh" and such
|
||||||
|
,"docker" -- like above
|
||||||
,"dpkg-query"
|
,"dpkg-query"
|
||||||
,"jq" -- could also check that user provides --arg
|
,"jq" -- could also check that user provides --arg
|
||||||
|
,"rename"
|
||||||
|
,"unset"
|
||||||
|
,"git filter-branch"
|
||||||
]
|
]
|
||||||
|| "awk" `isSuffixOf` commandName
|
|| "awk" `isSuffixOf` commandName
|
||||||
|| "perl" `isPrefixOf` commandName
|
|| "perl" `isPrefixOf` commandName
|
||||||
@@ -838,6 +862,12 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
|||||||
_ -> "find"
|
_ -> "find"
|
||||||
getFindCommand (T_Redirecting _ _ cmd) = getFindCommand cmd
|
getFindCommand (T_Redirecting _ _ cmd) = getFindCommand cmd
|
||||||
getFindCommand _ = "find"
|
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 ()
|
checkSingleQuotedVariables _ _ = return ()
|
||||||
|
|
||||||
|
|
||||||
@@ -845,6 +875,7 @@ prop_checkUnquotedN = verify checkUnquotedN "if [ -n $foo ]; then echo cow; fi"
|
|||||||
prop_checkUnquotedN2 = verify checkUnquotedN "[ -n $cow ]"
|
prop_checkUnquotedN2 = verify checkUnquotedN "[ -n $cow ]"
|
||||||
prop_checkUnquotedN3 = verifyNot checkUnquotedN "[[ -n $foo ]] && echo cow"
|
prop_checkUnquotedN3 = verifyNot checkUnquotedN "[[ -n $foo ]] && echo cow"
|
||||||
prop_checkUnquotedN4 = verify checkUnquotedN "[ -n $cow -o -t 1 ]"
|
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 =
|
checkUnquotedN _ (TC_Unary _ SingleBracket "-n" (T_NormalWord id [t])) | willSplit t =
|
||||||
err id 2070 "-n doesn't work with unquoted arguments. Quote or use [[ ]]."
|
err id 2070 "-n doesn't work with unquoted arguments. Quote or use [[ ]]."
|
||||||
checkUnquotedN _ _ = return ()
|
checkUnquotedN _ _ = return ()
|
||||||
@@ -996,7 +1027,7 @@ checkQuotedCondRegex _ (TC_Binary _ _ "=~" _ rhs) =
|
|||||||
error t =
|
error t =
|
||||||
unless (isConstantNonRe t) $
|
unless (isConstantNonRe t) $
|
||||||
err (getId t) 2076
|
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 "[][*.+()|]"
|
re = mkRegex "[][*.+()|]"
|
||||||
hasMetachars s = s `matches` re
|
hasMetachars s = s `matches` re
|
||||||
isConstantNonRe t = fromMaybe False $ do
|
isConstantNonRe t = fromMaybe False $ do
|
||||||
@@ -1006,13 +1037,16 @@ checkQuotedCondRegex _ _ = return ()
|
|||||||
|
|
||||||
prop_checkGlobbedRegex1 = verify checkGlobbedRegex "[[ $foo =~ *foo* ]]"
|
prop_checkGlobbedRegex1 = verify checkGlobbedRegex "[[ $foo =~ *foo* ]]"
|
||||||
prop_checkGlobbedRegex2 = verify checkGlobbedRegex "[[ $foo =~ f* ]]"
|
prop_checkGlobbedRegex2 = verify checkGlobbedRegex "[[ $foo =~ f* ]]"
|
||||||
prop_checkGlobbedRegex2a = verify checkGlobbedRegex "[[ $foo =~ \\#* ]]"
|
|
||||||
prop_checkGlobbedRegex3 = verifyNot checkGlobbedRegex "[[ $foo =~ $foo ]]"
|
prop_checkGlobbedRegex3 = verifyNot checkGlobbedRegex "[[ $foo =~ $foo ]]"
|
||||||
prop_checkGlobbedRegex4 = verifyNot checkGlobbedRegex "[[ $foo =~ ^c.* ]]"
|
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) =
|
checkGlobbedRegex _ (TC_Binary _ DoubleBracket "=~" _ rhs) =
|
||||||
let s = concat $ oversimplify rhs in
|
let s = concat $ oversimplify rhs in
|
||||||
when (isConfusedGlobRegex s) $
|
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 ()
|
checkGlobbedRegex _ _ = return ()
|
||||||
|
|
||||||
|
|
||||||
@@ -1140,12 +1174,10 @@ checkArithmeticDeref params t@(TA_Expansion _ [b@(T_DollarBraced id _)]) =
|
|||||||
T_Arithmetic {} -> return normalWarning
|
T_Arithmetic {} -> return normalWarning
|
||||||
T_DollarArithmetic {} -> return normalWarning
|
T_DollarArithmetic {} -> return normalWarning
|
||||||
T_ForArithmetic {} -> return normalWarning
|
T_ForArithmetic {} -> return normalWarning
|
||||||
TA_Index {} -> return indexWarning
|
|
||||||
T_SimpleCommand {} -> return noWarning
|
T_SimpleCommand {} -> return noWarning
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
normalWarning = style id 2004 "$/${} is unnecessary on arithmetic variables."
|
normalWarning = style id 2004 "$/${} is unnecessary on arithmetic variables."
|
||||||
indexWarning = style id 2149 "Remove $/${} for numeric index, or escape it for string."
|
|
||||||
noWarning = return ()
|
noWarning = return ()
|
||||||
checkArithmeticDeref _ _ = return ()
|
checkArithmeticDeref _ _ = return ()
|
||||||
|
|
||||||
@@ -1165,11 +1197,12 @@ prop_checkComparisonAgainstGlob2 = verifyNot checkComparisonAgainstGlob "[[ $cow
|
|||||||
prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = *foo* ]"
|
prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = *foo* ]"
|
||||||
prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]"
|
prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]"
|
||||||
prop_checkComparisonAgainstGlob5 = verify checkComparisonAgainstGlob "[[ $cow != $bar ]]"
|
prop_checkComparisonAgainstGlob5 = verify checkComparisonAgainstGlob "[[ $cow != $bar ]]"
|
||||||
|
prop_checkComparisonAgainstGlob6 = verify checkComparisonAgainstGlob "[ $f != /* ]"
|
||||||
checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _]))
|
checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _]))
|
||||||
| op `elem` ["=", "==", "!="] =
|
| 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)
|
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."
|
err (getId word) 2081 "[ .. ] can't match globs. Use [[ .. ]] or case statement."
|
||||||
checkComparisonAgainstGlob _ _ = return ()
|
checkComparisonAgainstGlob _ _ = return ()
|
||||||
|
|
||||||
@@ -1276,33 +1309,6 @@ checkTestRedirects _ (T_Redirecting id redirs cmd) | cmd `isCommand` "test" =
|
|||||||
_ -> False
|
_ -> False
|
||||||
checkTestRedirects _ _ = return ()
|
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_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
|
||||||
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
|
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
|
||||||
prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
|
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_checkBackticks2 = verifyNot checkBackticks "echo $(foo)"
|
||||||
prop_checkBackticks3 = verifyNot checkBackticks "echo `#inlined comment` foo"
|
prop_checkBackticks3 = verifyNot checkBackticks "echo `#inlined comment` foo"
|
||||||
checkBackticks _ (T_Backticked id list) | not (null list) =
|
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 ()
|
checkBackticks _ _ = return ()
|
||||||
|
|
||||||
prop_checkIndirectExpansion1 = verify checkIndirectExpansion "${foo$n}"
|
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_checkSpuriousExec5 = verifyNot checkSpuriousExec "exec > file; cmd"
|
||||||
prop_checkSpuriousExec6 = verify checkSpuriousExec "exec foo > file; cmd"
|
prop_checkSpuriousExec6 = verify checkSpuriousExec "exec foo > file; cmd"
|
||||||
prop_checkSpuriousExec7 = verifyNot checkSpuriousExec "exec file; echo failed; exit 3"
|
prop_checkSpuriousExec7 = verifyNot checkSpuriousExec "exec file; echo failed; exit 3"
|
||||||
|
prop_checkSpuriousExec8 = verifyNot checkSpuriousExec "exec {origout}>&1- >tmp.log 2>&1; bar"
|
||||||
checkSpuriousExec _ = doLists
|
checkSpuriousExec _ = doLists
|
||||||
where
|
where
|
||||||
doLists (T_Script _ _ cmds) = doList cmds
|
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_checkSpacefulness33= verifyTree checkSpacefulness "for file; do echo $file; done"
|
||||||
prop_checkSpacefulness34= verifyTree checkSpacefulness "declare foo$n=$1"
|
prop_checkSpacefulness34= verifyTree checkSpacefulness "declare foo$n=$1"
|
||||||
prop_checkSpacefulness35= verifyNotTree checkSpacefulness "echo ${1+\"$1\"}"
|
prop_checkSpacefulness35= verifyNotTree checkSpacefulness "echo ${1+\"$1\"}"
|
||||||
|
prop_checkSpacefulness36= verifyNotTree checkSpacefulness "arg=$#; echo $arg"
|
||||||
|
|
||||||
checkSpacefulness params t =
|
checkSpacefulness params t =
|
||||||
doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
|
doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
|
||||||
@@ -1617,16 +1625,23 @@ checkSpacefulness params t =
|
|||||||
modify $ Map.insert name bool
|
modify $ Map.insert name bool
|
||||||
|
|
||||||
readF _ token name = do
|
readF _ token name = do
|
||||||
spaced <- hasSpaces name
|
spaces <- hasSpaces name
|
||||||
return [makeComment InfoC (getId token) 2086 warning |
|
return [warning |
|
||||||
isExpansion token && spaced
|
isExpansion token && spaces
|
||||||
&& not (isArrayExpansion token) -- There's another warning for this
|
&& not (isArrayExpansion token) -- There's another warning for this
|
||||||
&& not (isCountingReference token)
|
&& not (isCountingReference token)
|
||||||
&& not (isQuoteFree parents token)
|
&& not (isQuoteFree parents token)
|
||||||
&& not (isQuotedAlternativeReference token)
|
&& not (isQuotedAlternativeReference token)
|
||||||
&& not (usedAsCommandName parents token)]
|
&& not (usedAsCommandName parents token)]
|
||||||
where
|
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 SourceExternal) = setSpaces name True >> return []
|
||||||
writeF _ _ name (DataString SourceInteger) = setSpaces name False >> return []
|
writeF _ _ name (DataString SourceInteger) = setSpaces name False >> return []
|
||||||
@@ -1665,6 +1680,12 @@ checkSpacefulness params t =
|
|||||||
globspace = "*?[] \t\n"
|
globspace = "*?[] \t\n"
|
||||||
containsAny s = any (`elem` s)
|
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_checkQuotesInLiterals1 = verifyTree checkQuotesInLiterals "param='--foo=\"bar\"'; app $param"
|
||||||
prop_checkQuotesInLiterals1a= verifyTree checkQuotesInLiterals "param=\"--foo='lolbar'\"; app $param"
|
prop_checkQuotesInLiterals1a= verifyTree checkQuotesInLiterals "param=\"--foo='lolbar'\"; app $param"
|
||||||
prop_checkQuotesInLiterals2 = verifyNotTree checkQuotesInLiterals "param='--foo=\"bar\"'; 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_checkUnused35= verifyNotTree checkUnusedAssignments "a=foo; b=2; echo ${a:b}"
|
||||||
prop_checkUnused36= verifyNotTree checkUnusedAssignments "if [[ -v foo ]]; then true; fi"
|
prop_checkUnused36= verifyNotTree checkUnusedAssignments "if [[ -v foo ]]; then true; fi"
|
||||||
prop_checkUnused37= verifyNotTree checkUnusedAssignments "fd=2; exec {fd}>&-"
|
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)
|
checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
|
||||||
where
|
where
|
||||||
flow = variableFlow params
|
flow = variableFlow params
|
||||||
@@ -1828,7 +1852,7 @@ checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
|
|||||||
|
|
||||||
warnFor (name, token) =
|
warnFor (name, token) =
|
||||||
warn (getId token) 2034 $
|
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
|
stripSuffix = takeWhile isVariableChar
|
||||||
defaultMap = Map.fromList $ zip internalVariables $ repeat ()
|
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_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_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_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
|
checkUnassignedReferences params t = warnings
|
||||||
where
|
where
|
||||||
(readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)
|
(readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)
|
||||||
@@ -1924,11 +1952,13 @@ checkUnassignedReferences params t = warnings
|
|||||||
isArray _ = False
|
isArray _ = False
|
||||||
|
|
||||||
isGuarded (T_DollarBraced _ v) =
|
isGuarded (T_DollarBraced _ v) =
|
||||||
any (`isPrefixOf` rest) ["-", ":-", "?", ":?"]
|
rest `matches` guardRegex
|
||||||
where
|
where
|
||||||
name = concat $ oversimplify v
|
name = concat $ oversimplify v
|
||||||
rest = dropWhile isVariableChar $ dropWhile (`elem` "#!") name
|
rest = dropWhile isVariableChar $ dropWhile (`elem` "#!") name
|
||||||
isGuarded _ = False
|
isGuarded _ = False
|
||||||
|
-- :? or :- with optional array index and colon
|
||||||
|
guardRegex = mkRegex "^(\\[.*\\])?:?[-?]"
|
||||||
|
|
||||||
match var candidate =
|
match var candidate =
|
||||||
if var /= candidate && map toLower var == map toLower 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_checkCdAndBack2 = verifyNot checkCdAndBack "for f in *; do cd $f || continue; git pull; cd ..; done"
|
||||||
prop_checkCdAndBack3 = verifyNot checkCdAndBack "while [[ $PWD != / ]]; do cd ..; done"
|
prop_checkCdAndBack3 = verifyNot checkCdAndBack "while [[ $PWD != / ]]; do cd ..; done"
|
||||||
prop_checkCdAndBack4 = verify checkCdAndBack "cd $tmp; foo; cd -"
|
prop_checkCdAndBack4 = verify checkCdAndBack "cd $tmp; foo; cd -"
|
||||||
|
prop_checkCdAndBack5 = verifyNot checkCdAndBack "cd ..; foo; cd .."
|
||||||
checkCdAndBack params = doLists
|
checkCdAndBack params = doLists
|
||||||
where
|
where
|
||||||
shell = shellType params
|
shell = shellType params
|
||||||
@@ -2075,10 +2106,20 @@ checkCdAndBack params = doLists
|
|||||||
getCmd (T_Pipeline id _ [x]) = getCommandName x
|
getCmd (T_Pipeline id _ [x]) = getCommandName x
|
||||||
getCmd _ = Nothing
|
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 =
|
doList list =
|
||||||
let cds = filter ((== Just "cd") . getCmd) list in
|
let cds = filter ((== Just "cd") . getCmd) list in
|
||||||
when (length cds >= 2 && isCdRevert (last cds)) $
|
potentially $ do
|
||||||
info (getId $ last cds) 2103 message
|
cd <- findCdPair cds
|
||||||
|
return $ info cd 2103 message
|
||||||
|
|
||||||
message = "Use a ( subshell ) to avoid having to cd back."
|
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"
|
prop_checkReadWithoutR2 = verifyNot checkReadWithoutR "read -ar foo"
|
||||||
checkReadWithoutR _ t@T_SimpleCommand {} | t `isUnqualifiedCommand` "read" =
|
checkReadWithoutR _ t@T_SimpleCommand {} | t `isUnqualifiedCommand` "read" =
|
||||||
unless ("r" `elem` map snd (getAllFlags t)) $
|
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 ()
|
checkReadWithoutR _ _ = return ()
|
||||||
|
|
||||||
prop_checkUncheckedCd1 = verifyTree checkUncheckedCdPushdPopd "cd ~/src; rm -r foo"
|
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_checkUncheckedCd6 = verifyNotTree checkUncheckedCdPushdPopd "cd .."
|
||||||
prop_checkUncheckedCd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\ncd foo\nrm bar"
|
prop_checkUncheckedCd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\ncd foo\nrm bar"
|
||||||
prop_checkUncheckedCd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; cd foo; rm 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_checkUncheckedPushd1 = verifyTree checkUncheckedCdPushdPopd "pushd ~/src; rm -r foo"
|
||||||
prop_checkUncheckedPushd2 = verifyNotTree checkUncheckedCdPushdPopd "pushd ~/src || exit; rm -r foo"
|
prop_checkUncheckedPushd2 = verifyNotTree checkUncheckedCdPushdPopd "pushd ~/src || exit; rm -r foo"
|
||||||
prop_checkUncheckedPushd3 = verifyNotTree checkUncheckedCdPushdPopd "set -e; pushd ~/src; 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_checkUncheckedPopd6 = verifyTree checkUncheckedCdPushdPopd "popd"
|
||||||
prop_checkUncheckedPopd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\npopd\nrm bar"
|
prop_checkUncheckedPopd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\npopd\nrm bar"
|
||||||
prop_checkUncheckedPopd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; popd; rm bar"
|
prop_checkUncheckedPopd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; popd; rm bar"
|
||||||
|
prop_checkUncheckedPopd9 = verifyNotTree checkUncheckedCdPushdPopd "popd -n foo"
|
||||||
|
|
||||||
checkUncheckedCdPushdPopd params root =
|
checkUncheckedCdPushdPopd params root =
|
||||||
if hasSetE params then
|
if hasSetE params then
|
||||||
@@ -2493,7 +2536,7 @@ checkUncheckedCdPushdPopd params root =
|
|||||||
checkElement t@T_SimpleCommand {} =
|
checkElement t@T_SimpleCommand {} =
|
||||||
when(name t `elem` ["cd", "pushd", "popd"]
|
when(name t `elem` ["cd", "pushd", "popd"]
|
||||||
&& not (isSafeDir t)
|
&& 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)) $
|
&& not (isCondition $ getPath (parentMap params) t)) $
|
||||||
warn (getId t) 2164 "Use 'cd ... || exit' or 'cd ... || return' in case cd fails."
|
warn (getId t) 2164 "Use 'cd ... || exit' or 'cd ... || return' in case cd fails."
|
||||||
checkElement _ = return ()
|
checkElement _ = return ()
|
||||||
@@ -2525,7 +2568,7 @@ checkLoopVariableReassignment params token =
|
|||||||
T_ForArithmetic _
|
T_ForArithmetic _
|
||||||
(TA_Sequence _
|
(TA_Sequence _
|
||||||
[TA_Assignment _ "="
|
[TA_Assignment _ "="
|
||||||
(TA_Expansion _ [T_Literal _ var]) _])
|
(TA_Variable _ var _ ) _])
|
||||||
_ _ _ -> return var
|
_ _ _ -> return var
|
||||||
_ -> fail "not loop"
|
_ -> 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_checkUnmatchableCases6 = verifyNot checkUnmatchableCases "case $f in ?*) true;; *) false;; esac"
|
||||||
prop_checkUnmatchableCases7 = verifyNot checkUnmatchableCases "case $f in $(x)) true;; asdf) 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_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 =
|
checkUnmatchableCases _ t =
|
||||||
case t of
|
case t of
|
||||||
T_CaseExpression _ word list -> do
|
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
|
if isConstant word
|
||||||
then warn (getId word) 2194
|
then warn (getId word) 2194
|
||||||
"This word is constant. Did you forget the $ on a variable?"
|
"This word is constant. Did you forget the $ on a variable?"
|
||||||
else potentially $ do
|
else potentially $ do
|
||||||
pg <- wordToPseudoGlob word
|
pg <- wordToPseudoGlob word
|
||||||
return $ mapM_ (check pg) patterns
|
return $ mapM_ (check pg) allpatterns
|
||||||
|
|
||||||
let exactGlobs = tupMap wordToExactPseudoGlob patterns
|
let exactGlobs = tupMap wordToExactPseudoGlob breakpatterns
|
||||||
let fuzzyGlobs = tupMap wordToPseudoGlob patterns
|
let fuzzyGlobs = tupMap wordToPseudoGlob breakpatterns
|
||||||
let dominators = zip exactGlobs (tails $ drop 1 fuzzyGlobs)
|
let dominators = zip exactGlobs (tails $ drop 1 fuzzyGlobs)
|
||||||
|
|
||||||
mapM_ checkDoms dominators
|
mapM_ checkDoms dominators
|
||||||
|
|
||||||
_ -> return ()
|
_ -> return ()
|
||||||
where
|
where
|
||||||
|
fst3 (x,_,_) = x
|
||||||
snd3 (_,x,_) = x
|
snd3 (_,x,_) = x
|
||||||
check target candidate = potentially $ do
|
check target candidate = potentially $ do
|
||||||
candidateGlob <- wordToPseudoGlob candidate
|
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_checkPipeToNowhere5 = verifyNot checkPipeToNowhere "echo foo | xargs du"
|
||||||
prop_checkPipeToNowhere6 = verifyNot checkPipeToNowhere "ls | echo $(cat)"
|
prop_checkPipeToNowhere6 = verifyNot checkPipeToNowhere "ls | echo $(cat)"
|
||||||
prop_checkPipeToNowhere7 = verifyNot checkPipeToNowhere "echo foo | var=$(cat) ls"
|
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 :: Parameters -> Token -> WriterT [TokenComment] Identity ()
|
||||||
checkPipeToNowhere _ t =
|
checkPipeToNowhere _ t =
|
||||||
case t of
|
case t of
|
||||||
@@ -2831,15 +2881,25 @@ checkPipeToNowhere _ t =
|
|||||||
name <- getCommandBasename cmd
|
name <- getCommandBasename cmd
|
||||||
guard $ name `elem` nonReadingCommands
|
guard $ name `elem` nonReadingCommands
|
||||||
guard . not $ hasAdditionalConsumers cmd
|
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 $
|
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
|
checkRedir cmd = potentially $ do
|
||||||
name <- getCommandBasename cmd
|
name <- getCommandBasename cmd
|
||||||
guard $ name `elem` nonReadingCommands
|
guard $ name `elem` nonReadingCommands
|
||||||
guard . not $ hasAdditionalConsumers cmd
|
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 $
|
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)")?
|
-- Could any words in a SimpleCommand consume stdin (e.g. echo "$(cat)")?
|
||||||
hasAdditionalConsumers t = fromMaybe True $ do
|
hasAdditionalConsumers t = fromMaybe True $ do
|
||||||
@@ -2891,5 +2951,113 @@ checkUseBeforeDefinition _ t =
|
|||||||
then [x]
|
then [x]
|
||||||
else concatMap recursiveSequences list
|
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 []
|
return []
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
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
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.Analyzer (analyzeScript) where
|
module ShellCheck.Analyzer (analyzeScript) where
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ import qualified ShellCheck.Checks.ShellSupport
|
|||||||
|
|
||||||
-- TODO: Clean up the cruft this is layered on
|
-- TODO: Clean up the cruft this is layered on
|
||||||
analyzeScript :: AnalysisSpec -> AnalysisResult
|
analyzeScript :: AnalysisSpec -> AnalysisResult
|
||||||
analyzeScript spec = AnalysisResult {
|
analyzeScript spec = newAnalysisResult {
|
||||||
arComments =
|
arComments =
|
||||||
filterByAnnotation spec params . nub $
|
filterByAnnotation spec params . nub $
|
||||||
runAnalytics spec
|
runAnalytics spec
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
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
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,10 +15,10 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
module ShellCheck.AnalyzerLib where
|
module ShellCheck.AnalyzerLib where
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
import ShellCheck.ASTLib
|
import ShellCheck.ASTLib
|
||||||
@@ -34,11 +34,12 @@ import Control.Monad.State
|
|||||||
import Control.Monad.Writer
|
import Control.Monad.Writer
|
||||||
import Data.Char
|
import Data.Char
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
|
import Data.Maybe
|
||||||
|
import Data.Semigroup
|
||||||
|
|
||||||
import Test.QuickCheck.All (forAllProperties)
|
import Test.QuickCheck.All (forAllProperties)
|
||||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
import Test.QuickCheck.Test (maxSuccess, quickCheckWithResult, stdArgs)
|
||||||
|
|
||||||
type Analysis = AnalyzerM ()
|
type Analysis = AnalyzerM ()
|
||||||
type AnalyzerM a = RWS Parameters [TokenComment] Cache a
|
type AnalyzerM a = RWS Parameters [TokenComment] Cache a
|
||||||
@@ -57,16 +58,18 @@ runChecker params checker = notes
|
|||||||
check = perScript checker `composeAnalyzers` (\(Root x) -> void $ doAnalysis (perToken checker) x)
|
check = perScript checker `composeAnalyzers` (\(Root x) -> void $ doAnalysis (perToken checker) x)
|
||||||
notes = snd $ evalRWS (check $ Root root) params Cache
|
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
|
instance Monoid Checker where
|
||||||
mempty = Checker {
|
mempty = Checker {
|
||||||
perScript = nullCheck,
|
perScript = nullCheck,
|
||||||
perToken = nullCheck
|
perToken = nullCheck
|
||||||
}
|
}
|
||||||
mappend x y = Checker {
|
mappend = (Data.Semigroup.<>)
|
||||||
perScript = perScript x `composeAnalyzers` perScript y,
|
|
||||||
perToken = perToken x `composeAnalyzers` perToken y
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis
|
composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis
|
||||||
composeAnalyzers f g x = f x >> g x
|
composeAnalyzers f g x = f x >> g x
|
||||||
@@ -106,19 +109,17 @@ data DataSource =
|
|||||||
|
|
||||||
data VariableState = Dead Token String | Alive deriving (Show)
|
data VariableState = Dead Token String | Alive deriving (Show)
|
||||||
|
|
||||||
defaultSpec root = AnalysisSpec {
|
defaultSpec root = spec {
|
||||||
asScript = root,
|
|
||||||
asShellType = Nothing,
|
asShellType = Nothing,
|
||||||
asCheckSourced = False,
|
asCheckSourced = False,
|
||||||
asExecutionMode = Executed
|
asExecutionMode = Executed
|
||||||
}
|
} where spec = newAnalysisSpec root
|
||||||
|
|
||||||
pScript s =
|
pScript s =
|
||||||
let
|
let
|
||||||
pSpec = ParseSpec {
|
pSpec = newParseSpec {
|
||||||
psFilename = "script",
|
psFilename = "script",
|
||||||
psScript = s,
|
psScript = s
|
||||||
psCheckSourced = False
|
|
||||||
}
|
}
|
||||||
in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec
|
in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec
|
||||||
|
|
||||||
@@ -132,7 +133,14 @@ producesComments c s = do
|
|||||||
|
|
||||||
makeComment :: Severity -> Id -> Code -> String -> TokenComment
|
makeComment :: Severity -> Id -> Code -> String -> TokenComment
|
||||||
makeComment severity id code note =
|
makeComment severity id code note =
|
||||||
TokenComment id $ Comment severity code note
|
newTokenComment {
|
||||||
|
tcId = id,
|
||||||
|
tcComment = newComment {
|
||||||
|
cSeverity = severity,
|
||||||
|
cCode = code,
|
||||||
|
cMessage = note
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addComment note = tell [note]
|
addComment note = tell [note]
|
||||||
|
|
||||||
@@ -232,8 +240,9 @@ getParentTree t =
|
|||||||
where
|
where
|
||||||
pre t = modify (first ((:) t))
|
pre t = modify (first ((:) t))
|
||||||
post t = do
|
post t = do
|
||||||
(_:rest, map) <- get
|
(x, map) <- get
|
||||||
case rest of [] -> put (rest, map)
|
case x of
|
||||||
|
_:rest -> case rest of [] -> put (rest, map)
|
||||||
(x:_) -> put (rest, Map.insert (getId t) x map)
|
(x:_) -> put (rest, Map.insert (getId t) x map)
|
||||||
|
|
||||||
-- Given a root node, make a map from Id to Token
|
-- Given a root node, make a map from Id to Token
|
||||||
@@ -444,15 +453,12 @@ getModifiedVariables t =
|
|||||||
c@T_SimpleCommand {} ->
|
c@T_SimpleCommand {} ->
|
||||||
getModifiedVariableCommand c
|
getModifiedVariableCommand c
|
||||||
|
|
||||||
TA_Unary _ "++|" var -> maybeToList $ do
|
TA_Unary _ "++|" v@(TA_Variable _ name _) ->
|
||||||
name <- getLiteralString var
|
[(t, v, name, DataString $ SourceFrom [v])]
|
||||||
return (t, t, name, DataString $ SourceFrom [t])
|
TA_Unary _ "|++" v@(TA_Variable _ name _) ->
|
||||||
TA_Unary _ "|++" var -> maybeToList $ do
|
[(t, v, name, DataString $ SourceFrom [v])]
|
||||||
name <- getLiteralString var
|
TA_Assignment _ op (TA_Variable _ name _) rhs -> maybeToList $ do
|
||||||
return (t, t, name, DataString $ SourceFrom [t])
|
|
||||||
TA_Assignment _ op lhs rhs -> maybeToList $ do
|
|
||||||
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
||||||
name <- getLiteralString lhs
|
|
||||||
return (t, t, name, DataString $ SourceFrom [rhs])
|
return (t, t, name, DataString $ SourceFrom [rhs])
|
||||||
|
|
||||||
-- Count [[ -v foo ]] as an "assignment".
|
-- Count [[ -v foo ]] as an "assignment".
|
||||||
@@ -465,7 +471,7 @@ getModifiedVariables t =
|
|||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
guard . not . null $ str
|
guard . not . null $ str
|
||||||
return (t, token, str, DataString $ SourceChecked)
|
return (t, token, str, DataString SourceChecked)
|
||||||
|
|
||||||
T_DollarBraced _ l -> maybeToList $ do
|
T_DollarBraced _ l -> maybeToList $ do
|
||||||
let string = bracedString t
|
let string = bracedString t
|
||||||
@@ -498,7 +504,9 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
|
|||||||
"export" -> if "f" `elem` flags
|
"export" -> if "f" `elem` flags
|
||||||
then []
|
then []
|
||||||
else concatMap getReference rest
|
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
|
then concatMap getReference rest
|
||||||
else []
|
else []
|
||||||
"readonly" ->
|
"readonly" ->
|
||||||
@@ -518,12 +526,22 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
|
|||||||
|
|
||||||
getReferencedVariableCommand _ = []
|
getReferencedVariableCommand _ = []
|
||||||
|
|
||||||
|
-- The function returns a tuple consisting of four items describing an assignment.
|
||||||
|
-- Given e.g. declare foo=bar
|
||||||
|
-- (
|
||||||
|
-- BaseCommand :: Token, -- The command/structure assigning the variable, i.e. declare foo=bar
|
||||||
|
-- AssignmentToken :: Token, -- The specific part that assigns this variable, i.e. foo=bar
|
||||||
|
-- VariableName :: String, -- The variable name, i.e. foo
|
||||||
|
-- VariableValue :: DataType -- A description of the value being assigned, i.e. "Literal string with value foo"
|
||||||
|
-- )
|
||||||
getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
||||||
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
|
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
|
||||||
case x of
|
case x of
|
||||||
"read" ->
|
"read" ->
|
||||||
let params = map getLiteral rest in
|
let params = map getLiteral rest
|
||||||
catMaybes . takeWhile isJust . reverse $ params
|
readArrayVars = getReadArrayVariables rest
|
||||||
|
in
|
||||||
|
catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params
|
||||||
"getopts" ->
|
"getopts" ->
|
||||||
case rest of
|
case rest of
|
||||||
opts:var:_ -> maybeToList $ getLiteral var
|
opts:var:_ -> maybeToList $ getLiteral var
|
||||||
@@ -566,10 +584,14 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
|
|||||||
where
|
where
|
||||||
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
|
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
|
||||||
|
|
||||||
getLiteral t = do
|
getLiteralOfDataType t d = do
|
||||||
s <- getLiteralString t
|
s <- getLiteralString t
|
||||||
when ("-" `isPrefixOf` s) $ fail "argument"
|
when ("-" `isPrefixOf` s) $ fail "argument"
|
||||||
return (base, t, s, DataString SourceExternal)
|
return (base, t, s, d)
|
||||||
|
|
||||||
|
getLiteral t = getLiteralOfDataType t (DataString SourceExternal)
|
||||||
|
|
||||||
|
getLiteralArray t = getLiteralOfDataType t (DataArray SourceExternal)
|
||||||
|
|
||||||
getModifierParamString = getModifierParam DataString
|
getModifierParamString = getModifierParam DataString
|
||||||
|
|
||||||
@@ -611,6 +633,11 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
|
|||||||
guard $ isVariableName name
|
guard $ isVariableName name
|
||||||
return (base, lastArg, name, DataArray SourceExternal)
|
return (base, lastArg, name, DataArray SourceExternal)
|
||||||
|
|
||||||
|
-- get all the array variables used in read, e.g. read -a arr
|
||||||
|
getReadArrayVariables args = do
|
||||||
|
map (getLiteralArray . snd)
|
||||||
|
(filter (\(x,_) -> getLiteralString x == Just "-a") (zip (args) (tail args)))
|
||||||
|
|
||||||
getModifiedVariableCommand _ = []
|
getModifiedVariableCommand _ = []
|
||||||
|
|
||||||
getIndexReferences s = fromMaybe [] $ do
|
getIndexReferences s = fromMaybe [] $ do
|
||||||
@@ -620,12 +647,17 @@ getIndexReferences s = fromMaybe [] $ do
|
|||||||
where
|
where
|
||||||
re = mkRegex "(\\[.*\\])"
|
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
|
getOffsetReferences mods = fromMaybe [] $ do
|
||||||
|
-- if mods start with [, then drop until ]
|
||||||
match <- matchRegex re mods
|
match <- matchRegex re mods
|
||||||
offsets <- match !!! 0
|
offsets <- match !!! 1
|
||||||
return $ matchAllStrings variableNameRegex offsets
|
return $ matchAllStrings variableNameRegex offsets
|
||||||
where
|
where
|
||||||
re = mkRegex "^ *:([^-=?+].*)"
|
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
|
||||||
|
|
||||||
getReferencedVariables parents t =
|
getReferencedVariables parents t =
|
||||||
case t of
|
case t of
|
||||||
@@ -634,10 +666,10 @@ getReferencedVariables parents t =
|
|||||||
map (\x -> (l, l, x)) (
|
map (\x -> (l, l, x)) (
|
||||||
getIndexReferences str
|
getIndexReferences str
|
||||||
++ getOffsetReferences (getBracedModifier str))
|
++ getOffsetReferences (getBracedModifier str))
|
||||||
TA_Expansion id _ ->
|
TA_Variable id name _ ->
|
||||||
if isArithmeticAssignment t
|
if isArithmeticAssignment t
|
||||||
then []
|
then []
|
||||||
else getIfReference t t
|
else [(t, t, name)]
|
||||||
T_Assignment id mode str _ word ->
|
T_Assignment id mode str _ word ->
|
||||||
[(t, t, str) | mode == Append] ++ specialReferences str t word
|
[(t, t, str) | mode == Append] ++ specialReferences str t word
|
||||||
|
|
||||||
@@ -664,7 +696,6 @@ getReferencedVariables parents t =
|
|||||||
else []
|
else []
|
||||||
|
|
||||||
literalizer t = case t of
|
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
|
T_Glob _ s -> return s -- Also when parsed as globs
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
@@ -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.
|
-- Compare a command to a literal. Like above, but checks full path.
|
||||||
isUnqualifiedCommand token str = isCommandMatch token (== str)
|
isUnqualifiedCommand token str = isCommandMatch token (== str)
|
||||||
|
|
||||||
isCommandMatch token matcher = fromMaybe False $ do
|
isCommandMatch token matcher = fromMaybe False $
|
||||||
cmd <- getCommandName token
|
fmap matcher (getCommandName token)
|
||||||
return $ matcher cmd
|
|
||||||
|
|
||||||
-- Does this regex look like it was intended as a glob?
|
-- Does this regex look like it was intended as a glob?
|
||||||
-- True: *foo*
|
-- True: *foo*
|
||||||
@@ -807,10 +837,9 @@ filterByAnnotation asSpec params =
|
|||||||
filter (not . shouldIgnore)
|
filter (not . shouldIgnore)
|
||||||
where
|
where
|
||||||
token = asScript asSpec
|
token = asScript asSpec
|
||||||
idFor (TokenComment id _) = id
|
|
||||||
shouldIgnore note =
|
shouldIgnore note =
|
||||||
any (shouldIgnoreFor (getCode note)) $
|
any (shouldIgnoreFor (getCode note)) $
|
||||||
getPath parents (T_Bang $ idFor note)
|
getPath parents (T_Bang $ tcId note)
|
||||||
shouldIgnoreFor num (T_Annotation _ anns _) =
|
shouldIgnoreFor num (T_Annotation _ anns _) =
|
||||||
any hasNum anns
|
any hasNum anns
|
||||||
where
|
where
|
||||||
@@ -819,7 +848,7 @@ filterByAnnotation asSpec params =
|
|||||||
shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec
|
shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec
|
||||||
shouldIgnoreFor _ _ = False
|
shouldIgnoreFor _ _ = False
|
||||||
parents = parentMap params
|
parents = parentMap params
|
||||||
getCode (TokenComment _ (Comment _ c _)) = c
|
getCode = cCode . tcComment
|
||||||
|
|
||||||
-- Is this a ${#anything}, to get string length or array count?
|
-- Is this a ${#anything}, to get string length or array count?
|
||||||
isCountingReference (T_DollarBraced id token) =
|
isCountingReference (T_DollarBraced id token) =
|
||||||
@@ -837,7 +866,38 @@ isQuotedAlternativeReference t =
|
|||||||
where
|
where
|
||||||
re = mkRegex "(^|\\]):?\\+"
|
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 []
|
return []
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
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
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
|
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
|
||||||
@@ -37,25 +37,30 @@ import Control.Monad
|
|||||||
|
|
||||||
import Test.QuickCheck.All
|
import Test.QuickCheck.All
|
||||||
|
|
||||||
tokenToPosition map (TokenComment id c) = fromMaybe fail $ do
|
tokenToPosition startMap t = fromMaybe fail $ do
|
||||||
position <- Map.lookup id map
|
span <- Map.lookup (tcId t) startMap
|
||||||
return $ PositionedComment position position c
|
return $ newPositionedComment {
|
||||||
|
pcStartPos = fst span,
|
||||||
|
pcEndPos = snd span,
|
||||||
|
pcComment = tcComment t
|
||||||
|
}
|
||||||
where
|
where
|
||||||
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
|
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
|
||||||
|
|
||||||
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
|
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
|
||||||
checkScript sys spec = do
|
checkScript sys spec = do
|
||||||
results <- checkScript (csScript spec)
|
results <- checkScript (csScript spec)
|
||||||
return CheckResult {
|
return emptyCheckResult {
|
||||||
crFilename = csFilename spec,
|
crFilename = csFilename spec,
|
||||||
crComments = results
|
crComments = results
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
checkScript contents = do
|
checkScript contents = do
|
||||||
result <- parseScript sys ParseSpec {
|
result <- parseScript sys newParseSpec {
|
||||||
psFilename = csFilename spec,
|
psFilename = csFilename spec,
|
||||||
psScript = contents,
|
psScript = contents,
|
||||||
psCheckSourced = csCheckSourced spec
|
psCheckSourced = csCheckSourced spec,
|
||||||
|
psShellTypeOverride = csShellTypeOverride spec
|
||||||
}
|
}
|
||||||
let parseMessages = prComments result
|
let parseMessages = prComments result
|
||||||
let analysisMessages =
|
let analysisMessages =
|
||||||
@@ -66,27 +71,38 @@ checkScript sys spec = do
|
|||||||
return . nub . sortMessages . filter shouldInclude $
|
return . nub . sortMessages . filter shouldInclude $
|
||||||
(parseMessages ++ map translator analysisMessages)
|
(parseMessages ++ map translator analysisMessages)
|
||||||
|
|
||||||
shouldInclude (PositionedComment _ _ (Comment _ code _)) =
|
shouldInclude pc =
|
||||||
code `notElem` csExcludedWarnings spec
|
let code = cCode (pcComment pc)
|
||||||
|
severity = cSeverity (pcComment pc)
|
||||||
|
in
|
||||||
|
code `notElem` csExcludedWarnings spec &&
|
||||||
|
severity <= csMinSeverity spec
|
||||||
|
|
||||||
sortMessages = sortBy (comparing order)
|
sortMessages = sortBy (comparing order)
|
||||||
order (PositionedComment pos _ (Comment severity code message)) =
|
order pc =
|
||||||
(posFile pos, posLine pos, posColumn pos, severity, code, message)
|
let pos = pcStartPos pc
|
||||||
getPosition (PositionedComment pos _ _) = pos
|
comment = pcComment pc in
|
||||||
|
(posFile pos,
|
||||||
|
posLine pos,
|
||||||
|
posColumn pos,
|
||||||
|
cSeverity comment,
|
||||||
|
cCode comment,
|
||||||
|
cMessage comment)
|
||||||
|
getPosition = pcStartPos
|
||||||
|
|
||||||
analysisSpec root =
|
analysisSpec root =
|
||||||
AnalysisSpec {
|
as {
|
||||||
asScript = root,
|
asScript = root,
|
||||||
asShellType = csShellTypeOverride spec,
|
asShellType = csShellTypeOverride spec,
|
||||||
asCheckSourced = csCheckSourced spec,
|
asCheckSourced = csCheckSourced spec,
|
||||||
asExecutionMode = Executed
|
asExecutionMode = Executed
|
||||||
}
|
} where as = newAnalysisSpec root
|
||||||
|
|
||||||
getErrors sys spec =
|
getErrors sys spec =
|
||||||
sort . map getCode . crComments $
|
sort . map getCode . crComments $
|
||||||
runIdentity (checkScript sys spec)
|
runIdentity (checkScript sys spec)
|
||||||
where
|
where
|
||||||
getCode (PositionedComment _ _ (Comment _ code _)) = code
|
getCode = cCode . pcComment
|
||||||
|
|
||||||
check = checkWithIncludes []
|
check = checkWithIncludes []
|
||||||
|
|
||||||
@@ -136,6 +152,21 @@ prop_optionDisablesIssue2 =
|
|||||||
csExcludedWarnings = [2148, 1037]
|
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 =
|
prop_canParseDevNull =
|
||||||
[] == check "source /dev/null"
|
[] == check "source /dev/null"
|
||||||
|
|
||||||
@@ -180,7 +211,7 @@ prop_filewideAnnotation1 = null $
|
|||||||
prop_filewideAnnotation2 = null $
|
prop_filewideAnnotation2 = null $
|
||||||
check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
|
check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
|
||||||
prop_filewideAnnotation3 = null $
|
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 $
|
prop_filewideAnnotation4 = null $
|
||||||
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
|
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
|
||||||
prop_filewideAnnotation5 = null $
|
prop_filewideAnnotation5 = null $
|
||||||
@@ -194,5 +225,8 @@ prop_filewideAnnotationBase2 = [2086, 2181] == check "true\n[ $? == 0 ] && echo
|
|||||||
prop_filewideAnnotation8 = null $
|
prop_filewideAnnotation8 = null $
|
||||||
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
|
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 []
|
return []
|
||||||
runTests = $quickCheckAll
|
runTests = $quickCheckAll
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
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
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,15 +15,13 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
|
||||||
-- This module contains checks that examine specific commands by name.
|
-- This module contains checks that examine specific commands by name.
|
||||||
module ShellCheck.Checks.Commands (checker
|
module ShellCheck.Checks.Commands (checker , ShellCheck.Checks.Commands.runTests) where
|
||||||
, ShellCheck.Checks.Commands.runTests
|
|
||||||
) where
|
|
||||||
|
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
import ShellCheck.ASTLib
|
import ShellCheck.ASTLib
|
||||||
@@ -63,6 +61,7 @@ commandChecks = [
|
|||||||
,checkGrepRe
|
,checkGrepRe
|
||||||
,checkTrapQuotes
|
,checkTrapQuotes
|
||||||
,checkReturn
|
,checkReturn
|
||||||
|
,checkExit
|
||||||
,checkFindExecWithSingleArgument
|
,checkFindExecWithSingleArgument
|
||||||
,checkUnusedEchoEscapes
|
,checkUnusedEchoEscapes
|
||||||
,checkInjectableFindSh
|
,checkInjectableFindSh
|
||||||
@@ -88,6 +87,13 @@ commandChecks = [
|
|||||||
,checkWhileGetoptsCase
|
,checkWhileGetoptsCase
|
||||||
,checkCatastrophicRm
|
,checkCatastrophicRm
|
||||||
,checkLetUsage
|
,checkLetUsage
|
||||||
|
,checkMvArguments, checkCpArguments, checkLnArguments
|
||||||
|
,checkFindRedirections
|
||||||
|
,checkReadExpansions
|
||||||
|
,checkWhich
|
||||||
|
,checkSudoRedirect
|
||||||
|
,checkSudoArgs
|
||||||
|
,checkSourceArgs
|
||||||
]
|
]
|
||||||
|
|
||||||
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
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_checkTr9 = verifyNot checkTr "a-z n-za-m"
|
||||||
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
|
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
|
||||||
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
|
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
|
||||||
|
prop_checkTr12= verifyNot checkTr "tr '[=e=]' 'e'"
|
||||||
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
|
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
|
||||||
where
|
where
|
||||||
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
|
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
|
||||||
@@ -148,7 +155,7 @@ checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
|
|||||||
Just s -> do -- Eliminate false positives by only looking for dupes in SET2?
|
Just s -> do -- Eliminate false positives by only looking for dupes in SET2?
|
||||||
when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $
|
when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $
|
||||||
info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)."
|
info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)."
|
||||||
unless ("[:" `isPrefixOf` s) $
|
unless ("[:" `isPrefixOf` s || "[=" `isPrefixOf` s) $
|
||||||
when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $
|
when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $
|
||||||
info (getId word) 2021 "Don't use [] around classes in tr, it replaces literal square brackets."
|
info (getId word) 2021 "Don't use [] around classes in tr, it replaces literal square brackets."
|
||||||
Nothing -> return ()
|
Nothing -> return ()
|
||||||
@@ -179,7 +186,7 @@ prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)
|
|||||||
checkNeedlessExpr = CommandCheck (Basename "expr") f where
|
checkNeedlessExpr = CommandCheck (Basename "expr") f where
|
||||||
f t =
|
f t =
|
||||||
when (all (`notElem` exceptions) (words $ arguments t)) $
|
when (all (`notElem` exceptions) (words $ arguments t)) $
|
||||||
style (getId t) 2003
|
style (getId $ getCommandTokenOrThis t) 2003
|
||||||
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
|
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
|
||||||
-- These operators are hard to replicate in POSIX
|
-- These operators are hard to replicate in POSIX
|
||||||
exceptions = [ ":", "<", ">", "<=", ">=" ]
|
exceptions = [ ":", "<", ">", "<=", ">=" ]
|
||||||
@@ -275,15 +282,28 @@ prop_checkReturn4 = verifyNot checkReturn "return $((a|b))"
|
|||||||
prop_checkReturn5 = verify checkReturn "return -1"
|
prop_checkReturn5 = verify checkReturn "return -1"
|
||||||
prop_checkReturn6 = verify checkReturn "return 1000"
|
prop_checkReturn6 = verify checkReturn "return 1000"
|
||||||
prop_checkReturn7 = verify checkReturn "return 'hello world'"
|
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
|
where
|
||||||
f (first:second:_) =
|
f (first:second:_) =
|
||||||
err (getId second) 2151
|
multi (getId first)
|
||||||
"Only one integer 0-255 can be returned. Use stdout for other data."
|
|
||||||
f [value] =
|
f [value] =
|
||||||
when (isInvalid $ literal value) $
|
when (isInvalid $ literal value) $
|
||||||
err (getId value) 2152
|
invalid (getId value)
|
||||||
"Can only return 0-255. Other data should be written to stdout."
|
|
||||||
f _ = return ()
|
f _ = return ()
|
||||||
|
|
||||||
isInvalid s = s == "" || any (not . isDigit) s || length s > 5
|
isInvalid s = s == "" || any (not . isDigit) s || length s > 5
|
||||||
@@ -320,26 +340,18 @@ prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\n
|
|||||||
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
|
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
|
||||||
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
|
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
|
||||||
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
|
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
|
||||||
checkUnusedEchoEscapes = CommandCheck (Basename "echo") (f . arguments)
|
checkUnusedEchoEscapes = CommandCheck (Basename "echo") f
|
||||||
where
|
where
|
||||||
isDashE = mkRegex "^-.*e"
|
|
||||||
hasEscapes = mkRegex "\\\\[rnt]"
|
hasEscapes = mkRegex "\\\\[rnt]"
|
||||||
f args | concat (concatMap oversimplify allButLast) `matches` isDashE =
|
f cmd =
|
||||||
return ()
|
whenShell [Sh, Bash, Ksh] $
|
||||||
where allButLast = reverse . drop 1 . reverse $ args
|
unless (cmd `hasFlag` "e") $
|
||||||
f args = mapM_ checkEscapes args
|
mapM_ examine $ arguments cmd
|
||||||
|
|
||||||
checkEscapes (T_NormalWord _ args) =
|
examine token = do
|
||||||
mapM_ checkEscapes args
|
let str = onlyLiteralString token
|
||||||
checkEscapes (T_DoubleQuoted id args) =
|
|
||||||
mapM_ checkEscapes args
|
|
||||||
checkEscapes (T_Literal id str) = examine id str
|
|
||||||
checkEscapes (T_SingleQuoted id str) = examine id str
|
|
||||||
checkEscapes _ = return ()
|
|
||||||
|
|
||||||
examine id str =
|
|
||||||
when (str `matches` hasEscapes) $
|
when (str `matches` hasEscapes) $
|
||||||
info id 2028 "echo won't expand escape sequences. Consider printf."
|
info (getId token) 2028 "echo may not expand escape sequences. Use printf."
|
||||||
|
|
||||||
|
|
||||||
prop_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
|
prop_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_checkSshCmdStr1 = verify checkSshCommandString "ssh host \"echo $PS1\""
|
||||||
prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\""
|
prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\""
|
||||||
prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""
|
prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""
|
||||||
|
prop_checkSshCmdStr4 = verifyNot checkSshCommandString "ssh -i key \"$host\""
|
||||||
checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
|
checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
|
||||||
where
|
where
|
||||||
nonOptions =
|
isOption x = "-" `isPrefixOf` (concat $ oversimplify x)
|
||||||
filter (\x -> not $ "-" `isPrefixOf` concat (oversimplify x))
|
|
||||||
f args =
|
f args =
|
||||||
case nonOptions args of
|
case partition isOption args of
|
||||||
(hostport:r@(_:_)) -> checkArg $ last r
|
([], hostport:r@(_:_)) -> checkArg $ last r
|
||||||
_ -> return ()
|
_ -> return ()
|
||||||
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
|
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
|
||||||
case filter (not . isConstant) parts of
|
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_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
|
||||||
prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
|
prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
|
||||||
prop_checkPrintfVar11= verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
|
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
|
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||||
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
|
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
|
||||||
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
|
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
|
||||||
@@ -517,10 +536,20 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
|||||||
case string of
|
case string of
|
||||||
'%':'%':rest -> countFormats rest
|
'%':'%':rest -> countFormats rest
|
||||||
'%':'(':rest -> 1 + countFormats (dropWhile (/= ')') rest)
|
'%':'(':rest -> 1 + countFormats (dropWhile (/= ')') rest)
|
||||||
'%':rest -> 1 + countFormats rest
|
'%':rest -> regexBasedCountFormats rest + countFormats (dropWhile (/= '%') rest)
|
||||||
_:rest -> countFormats rest
|
_:rest -> countFormats rest
|
||||||
[] -> 0
|
[] -> 0
|
||||||
|
|
||||||
|
regexBasedCountFormats rest =
|
||||||
|
maybe 1 (foldl (\acc group -> acc + (if group == "*" then 1 else 0)) 1) (matchRegex re rest)
|
||||||
|
where
|
||||||
|
-- constructed based on specifications in "man printf"
|
||||||
|
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*).?(\\d*|\\*)[diouxXfFeEgGaAcsb]"
|
||||||
|
-- \____ _____/\___ ____/ \____ ____/\________ ________/
|
||||||
|
-- V V V V
|
||||||
|
-- flags field width precision format character
|
||||||
|
-- field width and precision can be specified with a '*' instead of a digit,
|
||||||
|
-- in which case printf will accept one more argument for each '*' used
|
||||||
check format more = do
|
check format more = do
|
||||||
fromMaybe (return ()) $ do
|
fromMaybe (return ()) $ do
|
||||||
string <- getLiteralString format
|
string <- getLiteralString format
|
||||||
@@ -532,7 +561,7 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
|||||||
"This printf format string has no variables. Other arguments are ignored."
|
"This printf format string has no variables. Other arguments are ignored."
|
||||||
|
|
||||||
when (vars > 0
|
when (vars > 0
|
||||||
&& length more < vars
|
&& ((length more) `mod` vars /= 0 || null more)
|
||||||
&& all (not . mayBecomeMultipleArgs) more) $
|
&& all (not . mayBecomeMultipleArgs) more) $
|
||||||
warn (getId format) 2183 $
|
warn (getId format) 2183 $
|
||||||
"This format string has " ++ show vars ++ " variables, but is passed " ++ show (length more) ++ " arguments."
|
"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_checkExportedExpansions1 = verify checkExportedExpansions "export $foo"
|
||||||
prop_checkExportedExpansions2 = verify checkExportedExpansions "export \"$foo\""
|
prop_checkExportedExpansions2 = verify checkExportedExpansions "export \"$foo\""
|
||||||
prop_checkExportedExpansions3 = verifyNot 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
|
where
|
||||||
check = mapM_ checkForVariables
|
check t = potentially $ do
|
||||||
checkForVariables f =
|
var <- getSingleUnmodifiedVariable t
|
||||||
case getWordParts f of
|
let name = bracedString var
|
||||||
[t@(T_DollarBraced {})] ->
|
return . warn (getId t) 2163 $
|
||||||
warn (getId t) 2163 "Exporting an expansion rather than a variable."
|
"This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
||||||
_ -> return ()
|
|
||||||
|
|
||||||
|
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_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
|
||||||
prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'"
|
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_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"
|
||||||
prop_checkFindWithoutPath5 = verifyNot checkFindWithoutPath "find -O3 ."
|
prop_checkFindWithoutPath5 = verifyNot checkFindWithoutPath "find -O3 ."
|
||||||
prop_checkFindWithoutPath6 = verifyNot checkFindWithoutPath "find -D exec ."
|
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
|
checkFindWithoutPath = CommandCheck (Basename "find") f
|
||||||
where
|
where
|
||||||
f (T_SimpleCommand _ _ (cmd:args)) =
|
f t@(T_SimpleCommand _ _ (cmd:args)) =
|
||||||
unless (hasPath args) $
|
unless (t `hasFlag` "help" || hasPath args) $
|
||||||
info (getId cmd) 2185 "Some finds don't have a default path. Specify '.' explicitly."
|
info (getId cmd) 2185 "Some finds don't have a default path. Specify '.' explicitly."
|
||||||
|
|
||||||
-- This is a bit of a kludge. find supports flag arguments both before and after the path,
|
-- This is a bit of a kludge. find supports flag arguments both before and
|
||||||
-- as well as multiple non-flag arguments that are not the path. We assume that all the
|
-- after the path, as well as multiple non-flag arguments that are not the
|
||||||
-- pre-path flags are single characters, which is generally the case except for -O3.
|
-- path. We assume that all the pre-path flags are single characters from a
|
||||||
|
-- list of GNU and macOS flags.
|
||||||
hasPath (first:rest) =
|
hasPath (first:rest) =
|
||||||
let flag = fromJust $ getLiteralStringExt (const $ return "___") first in
|
let flag = fromJust $ getLiteralStringExt (const $ return "___") first in
|
||||||
not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest
|
not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest
|
||||||
hasPath [] = False
|
hasPath [] = False
|
||||||
isLeadingFlag flag = length flag <= 2 || "-O" `isPrefixOf` flag
|
isLeadingFlag flag = length flag <= 2 || all (`elem` leadingFlagChars) flag
|
||||||
|
leadingFlagChars="-EHLPXdfsxO0123456789"
|
||||||
|
|
||||||
|
|
||||||
prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10"
|
prop_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
|
whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local
|
||||||
path <- getPathM t
|
path <- getPathM t
|
||||||
unless (any isFunction path) $
|
unless (any isFunction path) $
|
||||||
err (getId t) 2168 "'local' is only valid in functions."
|
err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
|
||||||
|
|
||||||
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
|
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
|
||||||
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
|
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
|
||||||
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
|
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
|
||||||
\t -> warn (getId t) 2186 "tempfile is deprecated. Use mktemp instead."
|
\t -> warn (getId $ getCommandTokenOrThis t) 2186 "tempfile is deprecated. Use mktemp instead."
|
||||||
|
|
||||||
prop_checkDeprecatedEgrep = verify checkDeprecatedEgrep "egrep '.+'"
|
prop_checkDeprecatedEgrep = verify checkDeprecatedEgrep "egrep '.+'"
|
||||||
checkDeprecatedEgrep = CommandCheck (Basename "egrep") $
|
checkDeprecatedEgrep = CommandCheck (Basename "egrep") $
|
||||||
\t -> info (getId t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."
|
\t -> info (getId $ getCommandTokenOrThis t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."
|
||||||
|
|
||||||
prop_checkDeprecatedFgrep = verify checkDeprecatedFgrep "fgrep '*' files"
|
prop_checkDeprecatedFgrep = verify checkDeprecatedFgrep "fgrep '*' files"
|
||||||
checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $
|
checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $
|
||||||
\t -> info (getId t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
|
\t -> info (getId $ getCommandTokenOrThis t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
|
||||||
|
|
||||||
prop_checkWhileGetoptsCase1 = verify checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; esac; done"
|
prop_checkWhileGetoptsCase1 = verify checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; esac; done"
|
||||||
prop_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; b) bar;; esac; done"
|
prop_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
|
f t = whenShell [Bash,Ksh] $ do
|
||||||
style (getId t) 2219 $ "Instead of 'let expr', prefer (( expr )) ."
|
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 []
|
return []
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2016 Vidar Holen
|
Copyright 2012-2016 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
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
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,13 +15,11 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
module ShellCheck.Checks.ShellSupport (checker
|
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
|
||||||
, ShellCheck.Checks.ShellSupport.runTests
|
|
||||||
) where
|
|
||||||
|
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
import ShellCheck.ASTLib
|
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_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
|
||||||
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
|
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
|
||||||
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
|
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_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
|
||||||
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
|
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
|
||||||
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
||||||
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
||||||
prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
||||||
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"
|
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
|
checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||||
params <- ask
|
params <- ask
|
||||||
kludge params t
|
kludge params t
|
||||||
@@ -191,11 +191,9 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||||||
bashism (T_Glob id str) | "[^" `isInfixOf` str =
|
bashism (T_Glob id str) | "[^" `isInfixOf` str =
|
||||||
warnMsg id "^ in place of ! in glob bracket expressions is"
|
warnMsg id "^ in place of ! in glob bracket expressions is"
|
||||||
|
|
||||||
bashism t@(TA_Expansion id _) | isBashism =
|
bashism t@(TA_Variable id str _) | isBashVariable str =
|
||||||
warnMsg id $ fromJust str ++ " is"
|
warnMsg id $ str ++ " is"
|
||||||
where
|
|
||||||
str = getLiteralString t
|
|
||||||
isBashism = isJust str && isBashVariable (fromJust str)
|
|
||||||
bashism t@(T_DollarBraced id token) = do
|
bashism t@(T_DollarBraced id token) = do
|
||||||
mapM_ check expansion
|
mapM_ check expansion
|
||||||
when (isBashVariable var) $
|
when (isBashVariable var) $
|
||||||
@@ -279,14 +277,18 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||||||
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
||||||
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
|
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
|
||||||
"typeset"
|
"typeset"
|
||||||
] ++ if not isDash then ["local", "type"] else []
|
] ++ if not isDash then ["local"] else []
|
||||||
allowedFlags = Map.fromList [
|
allowedFlags = Map.fromList [
|
||||||
("read", if isDash then ["r", "p"] else ["r"]),
|
("exec", []),
|
||||||
("ulimit", ["f"]),
|
("export", ["-p"]),
|
||||||
("printf", []),
|
("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 ()
|
bashism _ = return ()
|
||||||
|
|
||||||
varChars="_0-9a-zA-Z"
|
varChars="_0-9a-zA-Z"
|
||||||
@@ -295,15 +297,16 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||||||
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
|
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
|
||||||
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
|
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
|
||||||
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
|
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
|
||||||
(re $ "^[" ++ varChars ++ "]+:[^-=?+]", "string indexing is"),
|
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", "string indexing is"),
|
||||||
(re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is")
|
(re $ "^([*@][%#]|#[@*])", "string operations on $@/$* are"),
|
||||||
|
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")
|
||||||
]
|
]
|
||||||
bashVars = [
|
bashVars = [
|
||||||
"LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
||||||
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
|
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
|
||||||
]
|
]
|
||||||
bashDynamicVars = [ "RANDOM", "SECONDS" ]
|
bashDynamicVars = [ "RANDOM", "SECONDS" ]
|
||||||
dashVars = [ "LINENO" ]
|
dashVars = [ ]
|
||||||
isBashVariable var =
|
isBashVariable var =
|
||||||
(var `elem` bashDynamicVars
|
(var `elem` bashDynamicVars
|
||||||
|| var `elem` bashVars && not (isAssigned var))
|
|| var `elem` bashVars && not (isAssigned var))
|
@@ -39,7 +39,7 @@ internalVariables = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
variablesWithoutSpaces = [
|
variablesWithoutSpaces = [
|
||||||
"$", "-", "?", "!",
|
"$", "-", "?", "!", "#",
|
||||||
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
|
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
|
||||||
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
|
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
|
||||||
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
|
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
|
||||||
@@ -79,10 +79,10 @@ commonCommands = [
|
|||||||
|
|
||||||
nonReadingCommands = [
|
nonReadingCommands = [
|
||||||
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
|
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
|
||||||
"cp", "du", "echo", "export", "fg", "fuser", "getconf", "getopt",
|
"cp", "du", "echo", "export", "false", "fg", "fuser", "getconf",
|
||||||
"getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls", "locale", "mv",
|
"getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
|
||||||
"nice", "printf", "ps", "pwd", "renice", "rm", "rmdir", "set", "sleep",
|
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
|
||||||
"touch", "trap", "ulimit", "unalias", "uname"
|
"set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname"
|
||||||
]
|
]
|
||||||
|
|
||||||
sampleWords = [
|
sampleWords = [
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
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
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.Formatter.CheckStyle (format) where
|
module ShellCheck.Formatter.CheckStyle (format) where
|
||||||
|
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
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
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.Formatter.Format where
|
module ShellCheck.Formatter.Format where
|
||||||
|
|
||||||
@@ -30,17 +30,17 @@ data Formatter = Formatter {
|
|||||||
footer :: IO ()
|
footer :: IO ()
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceFile (PositionedComment pos _ _) = posFile pos
|
sourceFile = posFile . pcStartPos
|
||||||
lineNo (PositionedComment pos _ _) = posLine pos
|
lineNo = posLine . pcStartPos
|
||||||
endLineNo (PositionedComment _ end _) = posLine end
|
endLineNo = posLine . pcEndPos
|
||||||
colNo (PositionedComment pos _ _) = posColumn pos
|
colNo = posColumn . pcStartPos
|
||||||
endColNo (PositionedComment _ end _) = posColumn end
|
endColNo = posColumn . pcEndPos
|
||||||
codeNo (PositionedComment _ _ (Comment _ code _)) = code
|
codeNo = cCode . pcComment
|
||||||
messageText (PositionedComment _ _ (Comment _ _ t)) = t
|
messageText = cMessage . pcComment
|
||||||
|
|
||||||
severityText :: PositionedComment -> String
|
severityText :: PositionedComment -> String
|
||||||
severityText (PositionedComment _ _ (Comment c _ _)) =
|
severityText pc =
|
||||||
case c of
|
case cSeverity (pcComment pc) of
|
||||||
ErrorC -> "error"
|
ErrorC -> "error"
|
||||||
WarningC -> "warning"
|
WarningC -> "warning"
|
||||||
InfoC -> "info"
|
InfoC -> "info"
|
||||||
@@ -51,11 +51,14 @@ makeNonVirtual comments contents =
|
|||||||
map fix comments
|
map fix comments
|
||||||
where
|
where
|
||||||
ls = lines contents
|
ls = lines contents
|
||||||
fix c@(PositionedComment start end comment) = PositionedComment start {
|
fix c = c {
|
||||||
|
pcStartPos = (pcStartPos c) {
|
||||||
posColumn = realignColumn lineNo colNo c
|
posColumn = realignColumn lineNo colNo c
|
||||||
} end {
|
}
|
||||||
|
, pcEndPos = (pcEndPos c) {
|
||||||
posColumn = realignColumn endLineNo endColNo c
|
posColumn = realignColumn endLineNo endColNo c
|
||||||
} comment
|
}
|
||||||
|
}
|
||||||
realignColumn lineNo colNo c =
|
realignColumn lineNo colNo c =
|
||||||
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
|
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
|
||||||
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
|
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
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
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.Formatter.GCC (format) where
|
module ShellCheck.Formatter.GCC (format) where
|
||||||
|
|
@@ -1,8 +1,9 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-
|
{-
|
||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
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
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,17 +16,19 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.Formatter.JSON (format) where
|
module ShellCheck.Formatter.JSON (format) where
|
||||||
|
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Formatter.Format
|
import ShellCheck.Formatter.Format
|
||||||
|
|
||||||
|
import Data.Aeson
|
||||||
import Data.IORef
|
import Data.IORef
|
||||||
|
import Data.Monoid
|
||||||
import GHC.Exts
|
import GHC.Exts
|
||||||
import System.IO
|
import System.IO
|
||||||
import Text.JSON
|
import qualified Data.ByteString.Lazy.Char8 as BL
|
||||||
|
|
||||||
format = do
|
format = do
|
||||||
ref <- newIORef []
|
ref <- newIORef []
|
||||||
@@ -36,19 +39,36 @@ format = do
|
|||||||
footer = finish ref
|
footer = finish ref
|
||||||
}
|
}
|
||||||
|
|
||||||
instance JSON (PositionedComment) where
|
instance ToJSON (PositionedComment) where
|
||||||
showJSON comment@(PositionedComment start end (Comment level code string)) = makeObj [
|
toJSON comment =
|
||||||
("file", showJSON $ posFile start),
|
let start = pcStartPos comment
|
||||||
("line", showJSON $ posLine start),
|
end = pcEndPos comment
|
||||||
("endLine", showJSON $ posLine end),
|
c = pcComment comment in
|
||||||
("column", showJSON $ posColumn start),
|
object [
|
||||||
("endColumn", showJSON $ posColumn end),
|
"file" .= posFile start,
|
||||||
("level", showJSON $ severityText comment),
|
"line" .= posLine start,
|
||||||
("code", showJSON code),
|
"endLine" .= posLine end,
|
||||||
("message", showJSON string)
|
"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
|
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
|
||||||
collectResult ref result _ =
|
collectResult ref result _ =
|
||||||
@@ -56,5 +76,5 @@ collectResult ref result _ =
|
|||||||
|
|
||||||
finish ref = do
|
finish ref = do
|
||||||
list <- readIORef ref
|
list <- readIORef ref
|
||||||
putStrLn $ encodeStrict list
|
BL.putStrLn $ encode list
|
||||||
|
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
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
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,25 +15,34 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.Formatter.TTY (format) where
|
module ShellCheck.Formatter.TTY (format) where
|
||||||
|
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Formatter.Format
|
import ShellCheck.Formatter.Format
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import Data.IORef
|
||||||
import Data.List
|
import Data.List
|
||||||
import GHC.Exts
|
import GHC.Exts
|
||||||
import System.Info
|
|
||||||
import System.IO
|
import System.IO
|
||||||
|
import System.Info
|
||||||
|
|
||||||
|
wikiLink = "https://www.shellcheck.net/wiki/"
|
||||||
|
|
||||||
|
-- An arbitrary Ord thing to order warnings
|
||||||
|
type Ranking = (Char, Severity, Integer)
|
||||||
|
|
||||||
format :: FormatterOptions -> IO Formatter
|
format :: FormatterOptions -> IO Formatter
|
||||||
format options = return Formatter {
|
format options = do
|
||||||
|
topErrorRef <- newIORef []
|
||||||
|
return Formatter {
|
||||||
header = return (),
|
header = return (),
|
||||||
footer = return (),
|
footer = outputWiki topErrorRef,
|
||||||
onFailure = outputError options,
|
onFailure = outputError options,
|
||||||
onResult = outputResult options
|
onResult = outputResult options topErrorRef
|
||||||
}
|
}
|
||||||
|
|
||||||
colorForLevel level =
|
colorForLevel level =
|
||||||
case level of
|
case level of
|
||||||
@@ -45,13 +54,60 @@ colorForLevel level =
|
|||||||
"source" -> 0 -- none
|
"source" -> 0 -- none
|
||||||
_ -> 0 -- none
|
_ -> 0 -- none
|
||||||
|
|
||||||
|
rankError :: PositionedComment -> Ranking
|
||||||
|
rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err)
|
||||||
|
where
|
||||||
|
ranking =
|
||||||
|
if cCode (pcComment err) `elem` uninteresting
|
||||||
|
then 'Z'
|
||||||
|
else 'A'
|
||||||
|
|
||||||
|
-- A list of the most generic, least directly helpful
|
||||||
|
-- error codes to downrank.
|
||||||
|
uninteresting = [
|
||||||
|
1009, -- Mentioned parser error was..
|
||||||
|
1019, -- Expected this to be an argument
|
||||||
|
1036, -- ( is invalid here
|
||||||
|
1047, -- Expected 'fi'
|
||||||
|
1062, -- Expected 'done'
|
||||||
|
1070, -- Parsing stopped here (generic)
|
||||||
|
1072, -- Missing/unexpected ..
|
||||||
|
1073, -- Couldn't parse this ..
|
||||||
|
1088, -- Parsing stopped here (paren)
|
||||||
|
1089 -- Parsing stopped here (keyword)
|
||||||
|
]
|
||||||
|
|
||||||
|
appendComments errRef comments max = do
|
||||||
|
previous <- readIORef errRef
|
||||||
|
let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments
|
||||||
|
writeIORef errRef . take max . nubBy equal . sort $ previous ++ current
|
||||||
|
where
|
||||||
|
fst3 (x,_,_) = x
|
||||||
|
equal x y = fst3 x == fst3 y
|
||||||
|
|
||||||
|
outputWiki :: IORef [(Ranking, Integer, String)] -> IO ()
|
||||||
|
outputWiki errRef = do
|
||||||
|
issues <- readIORef errRef
|
||||||
|
unless (null issues) $ do
|
||||||
|
putStrLn "For more information:"
|
||||||
|
mapM_ showErr issues
|
||||||
|
where
|
||||||
|
showErr (_, code, msg) =
|
||||||
|
putStrLn $ " " ++ wikiLink ++ "SC" ++ show code ++ " -- " ++ shorten msg
|
||||||
|
limit = 36
|
||||||
|
shorten msg =
|
||||||
|
if length msg < limit
|
||||||
|
then msg
|
||||||
|
else (take (limit-3) msg) ++ "..."
|
||||||
|
|
||||||
outputError options file error = do
|
outputError options file error = do
|
||||||
color <- getColorFunc $ foColorOption options
|
color <- getColorFunc $ foColorOption options
|
||||||
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
|
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
|
||||||
|
|
||||||
outputResult options result sys = do
|
outputResult options ref result sys = do
|
||||||
color <- getColorFunc $ foColorOption options
|
color <- getColorFunc $ foColorOption options
|
||||||
let comments = crComments result
|
let comments = crComments result
|
||||||
|
appendComments ref comments (fromIntegral $ foWikiLinkCount options)
|
||||||
let fileGroups = groupWith sourceFile comments
|
let fileGroups = groupWith sourceFile comments
|
||||||
mapM_ (outputForFile color sys) fileGroups
|
mapM_ (outputForFile color sys) fileGroups
|
||||||
|
|
||||||
@@ -78,9 +134,16 @@ outputForFile color sys comments = do
|
|||||||
cuteIndent :: PositionedComment -> String
|
cuteIndent :: PositionedComment -> String
|
||||||
cuteIndent comment =
|
cuteIndent comment =
|
||||||
replicate (fromIntegral $ colNo comment - 1) ' ' ++
|
replicate (fromIntegral $ colNo comment - 1) ' ' ++
|
||||||
"^-- " ++ code (codeNo comment) ++ ": " ++ messageText comment
|
makeArrow ++ " " ++ code (codeNo comment) ++ ": " ++ messageText comment
|
||||||
|
where
|
||||||
|
arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^"
|
||||||
|
makeArrow =
|
||||||
|
let sameLine = lineNo comment == endLineNo comment
|
||||||
|
delta = endColNo comment - colNo comment
|
||||||
|
in
|
||||||
|
if sameLine && delta > 2 && delta < 32 then arrow delta else "^--"
|
||||||
|
|
||||||
code code = "SC" ++ show code
|
code num = "SC" ++ show num
|
||||||
|
|
||||||
getColorFunc colorOption = do
|
getColorFunc colorOption = do
|
||||||
term <- hIsTerminalDevice stdout
|
term <- hIsTerminalDevice stdout
|
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
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
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
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
|
@@ -1,5 +1,5 @@
|
|||||||
# This file was automatically generated by stack init
|
# 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)
|
# 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
|
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