mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 00:39:19 +08:00
Compare commits
420 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
21462b11b3 | ||
|
88aef838f1 | ||
|
3d61b73e91 | ||
|
138080bdc7 | ||
|
5b3f17c29d | ||
|
b47e083ee3 | ||
|
3cba76dc7d | ||
|
eb588f62f6 | ||
|
bcd13614eb | ||
|
a8376a09a9 | ||
|
5ed89d2241 | ||
|
4a87d2a3de | ||
|
41613babd9 | ||
|
cb57b4a74f | ||
|
e3f0243c0e | ||
|
66b5f13c6f | ||
|
a7a404a5a8 | ||
|
0761f5c923 | ||
|
b55149b22d | ||
|
4097bb5154 | ||
|
1b207b3d43 | ||
|
135b4aa485 | ||
|
cb76951ad2 | ||
|
705e476e4c | ||
|
e705552c97 | ||
|
198aa4fc3d | ||
|
f4044fbcc7 | ||
|
2827b35696 | ||
|
de95c376ea | ||
|
5e1b1e010a | ||
|
620c9c2023 | ||
|
359b1467a2 | ||
|
df0a0d41fa | ||
|
b815242506 | ||
|
07b5aa2971 | ||
|
75949fe51e | ||
|
f7b82658f4 | ||
|
e0e46e979a | ||
|
79319558a5 | ||
|
8d13add1ed | ||
|
8940e60300 | ||
|
5f1c969546 | ||
|
dadfdfde97 | ||
|
3e2cb26119 | ||
|
1a6ae4f19e | ||
|
95a376aad1 | ||
|
a06d7c1841 | ||
|
5202072a34 | ||
|
72af1cfd59 | ||
|
228af7df54 | ||
|
6db392511b | ||
|
d510a3ef6c | ||
|
5516596b26 | ||
|
9e7539c10b | ||
|
a5a7b332f1 | ||
|
a68e3aeb26 | ||
|
259b1a5dc6 | ||
|
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 | ||
|
c36f6d89ba | ||
|
e801da0621 | ||
|
51e6bf809f | ||
|
3413a076ff | ||
|
53f63b85bb | ||
|
df068bc8ed | ||
|
102683ab04 | ||
|
acead72c93 | ||
|
0c1e2bbd4d | ||
|
5d9cb81008 | ||
|
1491402dcb | ||
|
436a46ebab | ||
|
db1e24d140 | ||
|
35daf7534b | ||
|
76ad5dbb9f | ||
|
f73736e5c9 | ||
|
3785a08906 | ||
|
74c199b51a | ||
|
371dcdda3a | ||
|
38044e3f75 | ||
|
b0f6f935f3 | ||
|
bd2facb245 | ||
|
895ba31337 | ||
|
ccc037d458 | ||
|
a1b370efbc | ||
|
7f36c369f3 | ||
|
7b55e73e03 | ||
|
6c068e7d29 | ||
|
8dd40efb44 | ||
|
751aebf984 | ||
|
3bf6913a15 | ||
|
73d06c4f47 | ||
|
72ed234291 | ||
|
b94c03e5a1 | ||
|
226bc4409c | ||
|
4a6acb6ff0 | ||
|
1d76abc439 | ||
|
807d899f3b | ||
|
d6803ffa24 | ||
|
4ec8d73a14 | ||
|
81388cefd2 | ||
|
43bb6a20ad | ||
|
8f99d2b008 | ||
|
79ae89076a | ||
|
aa33280cb0 | ||
|
bd13224907 | ||
|
b064cf3038 | ||
|
79d6066450 | ||
|
1463cf773a | ||
|
31bb02d6b7 | ||
|
5bd33dbf92 | ||
|
a3c6aff0fb | ||
|
8184ef1e8b | ||
|
a839a6657b | ||
|
a10b924570 | ||
|
8f31ae913b | ||
|
a06ad41bfa | ||
|
21f5bf01eb | ||
|
2ded4df6fa | ||
|
90da31f226 | ||
|
b1486ec1e9 | ||
|
954aa99b11 | ||
|
79872f92f8 | ||
|
bf9b841b07 | ||
|
5fad708df5 | ||
|
5cece759cc | ||
|
50c8172de4 | ||
|
ce950edbfd | ||
|
f8e75d3e89 | ||
|
6f4e06d83c | ||
|
505ff7832f | ||
|
ac3f0b3360 | ||
|
070a465b64 | ||
|
4243c6a0bf | ||
|
8bc89bc451 | ||
|
5099ebf9b9 | ||
|
d943ef6f77 | ||
|
5e4c288cf4 | ||
|
9e35aa7ce8 | ||
|
21d7068bc8 | ||
|
324aa3cc88 | ||
|
9c4f651e6b | ||
|
3cf8b9ceab | ||
|
5c01b6c7f5 | ||
|
7604e5eb58 | ||
|
4fb1080809 | ||
|
4f9a80db15 | ||
|
3a38c50b8e | ||
|
fd79e80e78 | ||
|
1fd9b474ba | ||
|
faafc99704 | ||
|
bc882fd85a | ||
|
41b6e3d5eb | ||
|
da1691912b | ||
|
0feb95b337 | ||
|
f0e0d9ffdb | ||
|
3c75674b50 | ||
|
8e5e77ad76 | ||
|
66c7cf19e2 | ||
|
36573b5b26 | ||
|
9e4a9c8c6c | ||
|
c2fcb742db | ||
|
e8b4a79b65 | ||
|
08d1d37094 | ||
|
e28e90133d | ||
|
2688a81526 | ||
|
82c3084438 | ||
|
c6ff1933b7 | ||
|
22c86256ac | ||
|
a3a4873190 | ||
|
750212af39 | ||
|
2154583fd3 | ||
|
35c74e4747 | ||
|
46fb91b44d | ||
|
128d5d6013 | ||
|
41176c23a6 | ||
|
342726f480 | ||
|
1863f2f12d | ||
|
8809a36952 | ||
|
7f307c5775 | ||
|
eb12086d05 | ||
|
6259a32601 | ||
|
4e13c7cbc1 | ||
|
edb01fa855 | ||
|
85e6c35845 | ||
|
43f667a8f9 | ||
|
daacc98a8f | ||
|
40907b1636 | ||
|
96168fc707 | ||
|
6aee12a572 | ||
|
0048f34b11 | ||
|
e679ff222a | ||
|
6f5648faca | ||
|
30e94ea7ab | ||
|
d8f8a2fa14 | ||
|
df3cc70658 | ||
|
5669702362 | ||
|
bd9d05c759 | ||
|
af87fe9315 | ||
|
c2850e436f | ||
|
d73fc8f91d | ||
|
7124c113e8 | ||
|
f594f01d35 | ||
|
6ccb7e9129 | ||
|
838f0ce4dc | ||
|
105b09792c | ||
|
f6618d4332 | ||
|
0a381be37b | ||
|
46e47dad45 | ||
|
fee6c94d40 | ||
|
cf1c46d852 | ||
|
a40efffec9 | ||
|
44b96fca66 | ||
|
b4fb439191 | ||
|
0897ab7092 | ||
|
9703f89f79 | ||
|
aadf02e635 | ||
|
090051bdc1 | ||
|
b8c96b4361 | ||
|
f26038125d | ||
|
08f7ff37c5 | ||
|
60fc33ebdf | ||
|
3a006f7bcb | ||
|
89b6fd58fa | ||
|
069ddeffcc | ||
|
59c4ed106c |
27
.github/ISSUE_TEMPLATE.md
vendored
Normal file
27
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
#### For bugs
|
||||
- Rule Id (if any, e.g. SC1000):
|
||||
- My shellcheck version (`shellcheck --version` or "online"):
|
||||
- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
|
||||
- [ ] I tried on shellcheck.net and verified that this is still a problem on the latest commit
|
||||
|
||||
#### For new checks and feature suggestions
|
||||
- [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this
|
||||
- [ ] I searched through https://github.com/koalaman/shellcheck/issues and didn't find anything related
|
||||
|
||||
|
||||
#### Here's a snippet or screenshot that shows the problem:
|
||||
|
||||
```sh
|
||||
|
||||
#!/your/interpreter
|
||||
your script here
|
||||
|
||||
```
|
||||
|
||||
#### Here's what shellcheck currently says:
|
||||
|
||||
|
||||
|
||||
#### Here's what I wanted or expected to see:
|
||||
|
||||
|
12
.gitignore
vendored
12
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
# Created by http://www.gitignore.io
|
||||
# Created by https://www.gitignore.io
|
||||
|
||||
### Haskell ###
|
||||
dist
|
||||
@@ -13,3 +13,13 @@ cabal-dev
|
||||
cabal.sandbox.config
|
||||
cabal.config
|
||||
.stack-work
|
||||
dist-newstyle/
|
||||
.ghc.environment.*
|
||||
cabal.project.local
|
||||
|
||||
### Snap ###
|
||||
/snap/.snapcraft/
|
||||
/stage/
|
||||
/parts/
|
||||
/prime/
|
||||
*.snap
|
||||
|
50
.prepare_deploy
Executable file
50
.prepare_deploy
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# This script packages up Travis compiled binaries
|
||||
set -ex
|
||||
shopt -s nullglob
|
||||
cd deploy
|
||||
|
||||
cp ../LICENSE LICENSE.txt
|
||||
sed -e $'s/$/\r/' > README.txt << END
|
||||
This is a precompiled ShellCheck binary.
|
||||
https://www.shellcheck.net/
|
||||
|
||||
ShellCheck is a static analysis tool for shell scripts.
|
||||
It's licensed under the GNU General Public License v3.0.
|
||||
Information and source code is available on the website.
|
||||
|
||||
This binary was compiled on $(date -u).
|
||||
|
||||
|
||||
|
||||
====== Latest commits ======
|
||||
|
||||
$(git log -n 3)
|
||||
END
|
||||
|
||||
for file in ./*.exe
|
||||
do
|
||||
zip "${file%.*}.zip" README.txt LICENSE.txt "$file"
|
||||
done
|
||||
|
||||
for file in *.linux-x86_64
|
||||
do
|
||||
base="${file%.*}"
|
||||
cp "$file" "shellcheck"
|
||||
tar -cJf "$base.linux.x86_64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
|
||||
rm "shellcheck"
|
||||
done
|
||||
|
||||
for file in *.linux-armv6hf
|
||||
do
|
||||
base="${file%.*}"
|
||||
cp "$file" "shellcheck"
|
||||
tar -cJf "$base.linux.armv6hf.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
|
||||
rm "shellcheck"
|
||||
done
|
||||
|
||||
for file in ./*
|
||||
do
|
||||
sha512sum "$file" > "$file.sha512sum"
|
||||
done
|
||||
|
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
|
||||
|
77
.travis.yml
77
.travis.yml
@@ -6,16 +6,75 @@ services:
|
||||
- docker
|
||||
|
||||
before_install:
|
||||
- export DOCKER_REPO=koalaman/shellcheck
|
||||
- |-
|
||||
export TAG=$([ "$TRAVIS_BRANCH" == "master" ] && echo "latest" || echo $TRAVIS_BRANCH)
|
||||
- DOCKER_BASE="$DOCKER_USERNAME/shellcheck"
|
||||
- DOCKER_BUILDS=""
|
||||
- TAGS=""
|
||||
- test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
|
||||
- test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
|
||||
- echo "Tags are $TAGS"
|
||||
|
||||
script:
|
||||
- docker build -t builder -f Dockerfile_builder .
|
||||
- docker run --rm -it -v $(pwd):/mnt builder
|
||||
- docker build -t $DOCKER_REPO:$TAG .
|
||||
- mkdir deploy
|
||||
# Remove all tests to reduce binary size
|
||||
- ./striptests
|
||||
# Linux Docker image
|
||||
- name="$DOCKER_BASE"
|
||||
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
|
||||
- docker build -t "$name:current" .
|
||||
- docker run "$name:current" --version
|
||||
- printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
|
||||
- docker run -v "$PWD:/mnt" "$name:current" myscript
|
||||
# Copy static executable from docker image
|
||||
- id=$(docker create "$name:current")
|
||||
- docker cp "$id:/bin/shellcheck" "shellcheck"
|
||||
- docker rm "$id"
|
||||
- ls -l shellcheck
|
||||
- ./shellcheck myscript
|
||||
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux-x86_64"; done
|
||||
# Linux Alpine based Docker image
|
||||
- name="$DOCKER_BASE-alpine"
|
||||
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
|
||||
- sed -e '/DELETE-MARKER/,$d' Dockerfile > Dockerfile.alpine
|
||||
- docker build -f Dockerfile.alpine -t "$name:current" .
|
||||
- docker run "$name:current" sh -c 'shellcheck --version'
|
||||
# Linux armv6hf static executable
|
||||
- docker run -v "$PWD:/mnt" koalaman/armv6hf-builder -c 'compile-shellcheck'
|
||||
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux-armv6hf"; done
|
||||
- rm -f shellcheck || true
|
||||
# Windows .exe
|
||||
- docker run --user="$UID" -v "$PWD:/appdata" koalaman/winghc cuib
|
||||
- for tag in $TAGS; do cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe"; done
|
||||
- rm -rf dist shellcheck || true
|
||||
# Misc packaging
|
||||
- ./.prepare_deploy
|
||||
|
||||
after_success:
|
||||
- docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
||||
- |-
|
||||
[ "$TRAVIS_BRANCH" == "master" ] && docker push $DOCKER_REPO:$TAG
|
||||
- docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
||||
- for repo in $DOCKER_BUILDS;
|
||||
do
|
||||
for tag in $TAGS;
|
||||
do
|
||||
echo "Deploying $repo:current as $repo:$tag...";
|
||||
docker tag "$repo:current" "$repo:$tag" || exit 1;
|
||||
docker push "$repo:$tag" || exit 1;
|
||||
done;
|
||||
done;
|
||||
|
||||
after_failure:
|
||||
- id
|
||||
- pwd
|
||||
- df -h
|
||||
- find . -name '*.log' -type f -exec grep "" /dev/null {} +
|
||||
- find . -ls
|
||||
|
||||
deploy:
|
||||
provider: gcs
|
||||
skip_cleanup: true
|
||||
access_key_id: GOOG7MDN7WEH6IIGBDCA
|
||||
secret_access_key:
|
||||
secure: Bcx2cT0/E2ikj7sdamVq52xlLZF9dz9ojGPtoKfPyQhkkZa+McVI4xgUSuyyoSxyKj77sofx2y8m6PJYYumT4g5hREV1tfeUkl0J2DQFMbGDYEt7kxVkXCxojNvhHwTzLFv0ezstrxWWxQm81BfQQ4U9lggRXtndAP4czZnOeHPINPSiue1QNwRAEw05r5UoIUJXy/5xyUrjIxn381pAs+gJqP2COeN9kTKYH53nS/AAws29RprfZFnPlo7xxWmcjRcdS5KPdGXI/c6tQp5zl2iTh510VC1PN2w1Wvnn/oNWhiNdqPyVDsojIX5+sS3nejzJA+KFMxXSBlyXIY3wPpS/MdscU79X6Q5f9ivsFfsm7gNBmxHUPNn0HAvU4ROT/CCE9j6jSbs5PC7QBo3CK4++jxAwE/pd9HUc2rs3k0ofx3rgveJ7txpy5yPKfwIIBi98kVKlC4w7dLvNTOfjW1Imt2yH87XTfsE0UIG9st1WII6s4l/WgBx2GuwKdt6+3QUYiAlCFckkxWi+fAvpHZUEL43Qxub5fN+ZV7Zib1n7opchH4QKGBb6/y0WaDCmtCfu0lppoe/TH6saOTjDFj67NJSElK6ZDxGZ3uw4R+ret2gm6WRKT2Oeub8J33VzSa7VkmFpMPrAAfPa9N1Z4ewBLoTmvxSg2A0dDrCdJio=
|
||||
bucket: shellcheck
|
||||
local-dir: deploy
|
||||
on:
|
||||
repo: koalaman/shellcheck
|
||||
all_branches: true
|
||||
|
364
CHANGELOG.md
Normal file
364
CHANGELOG.md
Normal file
@@ -0,0 +1,364 @@
|
||||
## Since previous release
|
||||
### Added
|
||||
- Preliminary support for fix suggestions
|
||||
|
||||
## v0.6.0 - 2018-12-02
|
||||
### Added
|
||||
- Command line option --severity/-S for filtering by minimum severity
|
||||
- Command line option --wiki-link-count/-W for showing wiki links
|
||||
- SC2152/SC2151: Warn about bad `exit` values like `1234` and `"foo"`
|
||||
- SC2236/SC2237: Suggest -n/-z instead of ! -z/-n
|
||||
- SC2238: Warn when redirecting to a known command name, e.g. ls > rm
|
||||
- SC2239: Warn if the shebang is not an absolute path, e.g. #!bin/sh
|
||||
- SC2240: Warn when passing additional arguments to dot (.) in sh/dash
|
||||
- SC1133: Better diagnostics when starting a line with |/||/&&
|
||||
|
||||
### Changed
|
||||
- Most warnings now have useful end positions
|
||||
- SC1117 about unknown double-quoted escape sequences has been retired
|
||||
|
||||
### Fixed
|
||||
- SC2021 no longer triggers for equivalence classes like `[=e=]`
|
||||
- SC2221/SC2222 no longer mistriggers on fall-through case branches
|
||||
- SC2081 about glob matches in `[ .. ]` now also triggers for `!=`
|
||||
- SC2086 no longer warns about spaces in `$#`
|
||||
- SC2164 no longer suggests subshells for `cd ..; cmd; cd ..`
|
||||
- `read -a` is now correctly considered an array assignment
|
||||
- SC2039 no longer warns about LINENO now that it's POSIX
|
||||
|
||||
## v0.5.0 - 2018-05-31
|
||||
### Added
|
||||
- SC2233/SC2234/SC2235: Suggest removing or replacing (..) around tests
|
||||
- SC2232: Warn about invalid arguments to sudo
|
||||
- SC2231: Suggest quoting expansions in for loop globs
|
||||
- SC2229: Warn about 'read $var'
|
||||
- SC2227: Warn about redirections in the middle of 'find' commands
|
||||
- SC2224/SC2225/SC2226: Warn when using mv/cp/ln without a destination
|
||||
- SC2223: Quote warning specific to `: ${var=value}`
|
||||
- SC1131: Warn when using `elseif` or `elsif`
|
||||
- SC1128: Warn about blanks/comments before shebang
|
||||
- SC1127: Warn about C-style comments
|
||||
|
||||
### Fixed
|
||||
- Annotations intended for a command's here documents now work
|
||||
- Escaped characters inside groups in =~ regexes now parse
|
||||
- Associative arrays are now respected in arithmetic contexts
|
||||
- SC1087 about `$var[@]` now correctly triggers on any index
|
||||
- Bad expansions in here documents are no longer ignored
|
||||
- FD move operations like {fd}>1- now parse correctly
|
||||
|
||||
### Changed
|
||||
- Here docs are now terminated as per spec, rather than by presumed intent
|
||||
- SC1073: 'else if' is now parsed correctly and not like 'elif'
|
||||
- SC2163: 'export $name' can now be silenced with 'export ${name?}'
|
||||
- SC2183: Now warns when printf arg count is not a multiple of format count
|
||||
|
||||
## v0.4.7 - 2017-12-08
|
||||
### Added
|
||||
- Statically linked binaries for Linux and Windows (see README.md)!
|
||||
- `-a` flag to also include warnings in `source`d files
|
||||
- SC2221/SC2222: Warn about overridden case branches
|
||||
- SC2220: Warn about unhandled error cases in getopt loops
|
||||
- SC2218: Warn when using functions before they're defined
|
||||
- SC2216/SC2217: Warn when piping/redirecting to mv/cp and other non-readers
|
||||
- SC2215: Warn about commands starting with leading dash
|
||||
- SC2214: Warn about superfluous getopt flags
|
||||
- SC2213: Warn about unhandled getopt flags
|
||||
- SC2212: Suggest `false` over `[ ]`
|
||||
- SC2211: Warn when using a glob as a command name
|
||||
- SC2210: Warn when redirecting to an integer, e.g. `foo 1>2`
|
||||
- SC2206/SC2207: Suggest alternatives when using word splitting in arrays
|
||||
- SC1117: Warn about double quoted, undefined backslash sequences
|
||||
- SC1113/SC1114/SC1115: Recognized more malformed shebangs
|
||||
|
||||
### Fixed
|
||||
- `[ -v foo ]` no longer warns if `foo` is undefined
|
||||
- SC2037 is now suppressed by quotes, e.g. `PAGER="cat" man foo`
|
||||
- Ksh nested array declarations now parse correctly
|
||||
- Parameter Expansion without colons are now recognized, e.g. `${foo+bar}`
|
||||
- The `lastpipe` option is now respected with regard to subshell warnings
|
||||
- `\(` is now respected for grouping in `[`
|
||||
- Leading `\` is now ignored for commands, to allow alias suppression
|
||||
- Comments are now allowed after directives to e.g. explain 'disable'
|
||||
|
||||
|
||||
## v0.4.6 - 2017-03-26
|
||||
### Added
|
||||
- SC2204/SC2205: Warn about `( -z foo )` and `( foo -eq bar )`
|
||||
- SC2200/SC2201: Warn about brace expansion in [/[[
|
||||
- SC2198/SC2199: Warn about arrays in [/[[
|
||||
- SC2196/SC2197: Warn about deprected egrep/fgrep
|
||||
- SC2195: Warn about unmatchable case branches
|
||||
- SC2194: Warn about constant 'case' statements
|
||||
- SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables
|
||||
- SC2188/SC2189: Warn about redirections without commands
|
||||
- SC2186: Warn about deprecated `tempfile`
|
||||
- SC1109: Warn when finding `&`/`>`/`<` unquoted
|
||||
- SC1108: Warn about missing spaces in `[ var= foo ]`
|
||||
|
||||
### Changed
|
||||
- All files are now read as UTF-8 with lenient latin1 fallback, ignoring locale
|
||||
- Unicode quotes are no longer considered syntactic quotes
|
||||
- `ash` scripts will now be checked as `dash` with a warning
|
||||
|
||||
### Fixed
|
||||
- `-c` no longer suggested when using `grep -o | wc`
|
||||
- Comments and whitespace are now allowed before filewide directives
|
||||
- Here doc delimters with esoteric quoting like `foo""` are now handled
|
||||
- SC2095 about `ssh` in while read loops is now suppressed when using `-n`
|
||||
- `%(%Y%M%D)T` now recognized as a single formatter in `printf` checks
|
||||
- `grep -F` now suppresses regex related suggestions
|
||||
- Command name checks now recognize busybox applet names
|
||||
|
||||
|
||||
## v0.4.5 - 2016-10-21
|
||||
### Added
|
||||
- A Docker build (thanks, kpankonen!)
|
||||
- SC2185: Suggest explicitly adding path for `find`
|
||||
- SC2184: Warn about unsetting globs (e.g. `unset foo[1]`)
|
||||
- SC2183: Warn about `printf` with more formatters than variables
|
||||
- SC2182: Warn about ignored arguments with `printf`
|
||||
- SC2181: Suggest using command directly instead of `if [ $? -eq 0 ]`
|
||||
- SC1106: Warn when using `test` operators in `(( 1 -eq 2 ))`
|
||||
|
||||
### Changed
|
||||
- Unrecognized directives now causes a warning rather than parse failure.
|
||||
|
||||
### Fixed
|
||||
- Indices in associative arrays are now parsed correctly
|
||||
- Missing shebang warning squashed when specifying with a directive
|
||||
- Ksh multidimensional arrays are now supported
|
||||
- Variables in substring ${a:x:y} expansions now count as referenced
|
||||
- SC1102 now also handles ambiguous `$((`
|
||||
- Using `$(seq ..)` will no longer suggest quoting
|
||||
- SC2148 (missing shebang) is now suppressed when using shell directives
|
||||
- `[ a '>' b ]` is now recognized as being correctly escaped
|
||||
|
||||
|
||||
## v0.4.4 - 2016-05-15
|
||||
### Added
|
||||
- Haskell Stack support (thanks, Arguggi!)
|
||||
- SC2179/SC2178: Warn when assigning/appending strings to arrays
|
||||
- SC1102: Warn about ambiguous `$(((`
|
||||
- SC1101: Warn when \\ linebreaks have trailing spaces
|
||||
|
||||
### Changed
|
||||
- Directives directly after the shebang now apply to the entire file
|
||||
|
||||
### Fixed
|
||||
- `{$i..10}` is now flagged similar to `{1..$i}`
|
||||
|
||||
|
||||
## v0.4.3 - 2016-01-13
|
||||
### Fixed
|
||||
- Build now works on GHC 7.6.3 as found on Debian Stable/Ubuntu LTS
|
||||
|
||||
|
||||
## v0.4.2 - 2016-01-09
|
||||
### Added
|
||||
- First class support for the `dash` shell
|
||||
- The `--color` flag similar to ls/grep's (thanks, haguenau!)
|
||||
- SC2174: Warn about unexpected behavior of `mkdir -pm` (thanks, eatnumber1!)
|
||||
- SC2172: Warn about non-portable use of signal numbers in `trap`
|
||||
- SC2171: Warn about `]]` without leading `[[`
|
||||
- SC2168: Warn about `local` outside functions
|
||||
|
||||
### Fixed
|
||||
- Warnings about unchecked `cd` will no longer trigger with `set -e`
|
||||
- `[ a -nt/-ot/-ef b ]` no longer warns about being constant
|
||||
- Quoted test operators like `[ foo "<" bar ]` now parse
|
||||
- Escaped quotes in backticks now parse correctly
|
||||
|
||||
|
||||
## v0.4.1 - 2015-09-05
|
||||
### Fixed
|
||||
- Added missing files to Cabal, fixing the build
|
||||
|
||||
|
||||
## v0.4.0 - 2015-09-05
|
||||
### Added
|
||||
- Support for following `source`d files
|
||||
- Support for setting default flags in `SHELLCHECK_OPTS`
|
||||
- An `--external-sources` flag for following arbitrary `source`d files
|
||||
- A `source` directive to override the filename to `source`
|
||||
- SC2166: Suggest using `[ p ] && [ q ]` over `[ p -a q ]`
|
||||
- SC2165: Warn when nested `for` loops use the same variable name
|
||||
- SC2164: Warn when using `cd` without checking that it succeeds
|
||||
- SC2163: Warn about `export $var`
|
||||
- SC2162: Warn when using `read` without `-r`
|
||||
- SC2157: Warn about `[ "$var " ]` and similar never-empty string matches
|
||||
|
||||
### Fixed
|
||||
- `cat -vnE file` and similar will no longer flag as UUOC
|
||||
- Nested trinary operators in `(( ))` now parse correctly
|
||||
- Ksh `${ ..; }` command expansions now parse
|
||||
|
||||
|
||||
## v0.3.8 - 2015-06-20
|
||||
### Changed
|
||||
- ShellCheck's license has changed from AGPLv3 to GPLv3.
|
||||
|
||||
### Added
|
||||
- SC2156: Warn about injecting filenames in `find -exec sh -c "{}" \;`
|
||||
|
||||
### Fixed
|
||||
- Variables and command substitutions in brace expansions are now parsed
|
||||
- ANSI colors are now disabled on Windows
|
||||
- Empty scripts now parse
|
||||
|
||||
|
||||
## v0.3.7 - 2015-04-16
|
||||
### Fixed
|
||||
- Build now works on GHC 7.10
|
||||
- Use `regex-tdfa` over `regex-compat` since the latter crashes on OS X.
|
||||
|
||||
## v0.3.6 - 2015-03-28
|
||||
### Added
|
||||
- SC2155: Warn about masked return values in `export foo=$(exit 1)`
|
||||
- SC2154: Warn when a lowercase variable is referenced but not assigned
|
||||
- SC2152/SC2151: Warn about bad `return` values like `1234` and `"foo"`
|
||||
- SC2150: Warn about `find -exec "shell command" \;`
|
||||
|
||||
### Fixed
|
||||
- `coproc` is now supported
|
||||
- Trinary operator now recognized in `((..))`
|
||||
|
||||
### Removed
|
||||
- Zsh support has been removed
|
||||
|
||||
|
||||
## v0.3.5 - 2014-11-09
|
||||
### Added
|
||||
- SC2148: Warn when not including a shebang
|
||||
- SC2147: Warn about literal ~ in PATH
|
||||
- SC1086: Warn about `$` in for loop variables, e.g. `for $i in ..`
|
||||
- SC1084: Warn when the shebang uses `!#` instead of `#!`
|
||||
|
||||
### Fixed
|
||||
- Empty and comment-only backtick expansions now parse
|
||||
- Variables used in PS1/PROMPT\_COMMAND/trap now count as referenced
|
||||
- ShellCheck now skips unreadable files and directories
|
||||
- `-f gcc` on empty files no longer crashes
|
||||
- Variables in $".." are now considered quoted
|
||||
- Warnings about expansions in single quotes now include backticks
|
||||
|
||||
|
||||
## v0.3.4 - 2014-07-08
|
||||
### Added
|
||||
- SC2146: Warn about precedence when combining `find -o` with actions
|
||||
- SC2145: Warn when concatenating arrays and strings
|
||||
|
||||
### Fixed
|
||||
- Case statements now support `;&` and `;;&`
|
||||
- Indices in array declarations now parse correctly
|
||||
- `let` expressions now parsed as arithmetic expressions
|
||||
- Escaping is now respected in here documents
|
||||
|
||||
### Changed
|
||||
- Completely drop Makefile in favor of Cabal (thanks rodrigosetti!)
|
||||
|
||||
|
||||
## v0.3.3 - 2014-05-29
|
||||
### Added
|
||||
- SC2144: Warn when using globs in `[/[[`
|
||||
- SC2143: Suggesting using `grep -q` over `[ "$(.. | grep)" ]`
|
||||
- SC2142: Warn when referencing positional parameters in aliases
|
||||
- SC2141: Warn about suspicious IFS assignments like `IFS="\n"`
|
||||
- SC2140: Warn about bad embedded quotes like `echo "var="value""`
|
||||
- SC2130: Warn when using `-eq` on strings
|
||||
- SC2139: Warn about define time expansions in alias definitions
|
||||
- SC2129: Suggest command grouping over `a >> log; b >> log; c >> log`
|
||||
- SC2128: Warn when expanding arrays without an index
|
||||
- SC2126: Suggest `grep -c` over `grep|wc`
|
||||
- SC2123: Warn about accidentally overriding `$PATH`, e.g. `PATH=/my/dir`
|
||||
- SC1083: Warn about literal `{/}` outside of quotes
|
||||
- SC1082: Warn about UTF-8 BOMs
|
||||
|
||||
### Fixed
|
||||
- SC2051 no longer triggers for `{1,$n}`, only `{1..$n}`
|
||||
- Improved detection of single quoted `sed` variables, e.g. `sed '$s///'`
|
||||
- Stop warning about single quoted variables in `PS1` and similar
|
||||
- Support for Zsh short form loops, `=(..)`
|
||||
|
||||
### Removed
|
||||
- SC1000 about unescaped lonely `$`, e.g. `grep "^foo$"`
|
||||
|
||||
|
||||
## v0.3.2 - 2014-03-22
|
||||
### Added
|
||||
- SC2121: Warn about trying to `set` variables, e.g. `set var = value`
|
||||
- SC2120/SC2119: Warn when a function uses `$1..` if none are ever passed
|
||||
- SC2117: Warn when using `su` in interactive mode, e.g. `su foo; whoami`
|
||||
- SC2116: Detect useless use of echo, e.g. `for i in $(echo $var)`
|
||||
- SC2115/SC2114: Detect some catastrophic `rm -r "$empty/"` mistakes
|
||||
- SC1081: Warn when capitalizing keywords like `While`
|
||||
- SC1077: Warn when using acute accents instead of backticks
|
||||
|
||||
### Fixed
|
||||
- Shells are now properly recognized in shebangs containing flags
|
||||
- Stop warning about math on decimals in ksh/zsh
|
||||
- Stop warning about decimal comparisons with `=`, e.g. `[ $version = 1.2 ]`
|
||||
- Parsing of `|&`
|
||||
- `${a[x]}` not counting as a reference of `x`
|
||||
- `(( x[0] ))` not counting as a reference of `x`
|
||||
|
||||
|
||||
## v0.3.1 - 2014-02-03
|
||||
### Added
|
||||
- The `-s` flag to specify shell dialect
|
||||
- SC2105/SC2104: Warn about `break/continue` outside loops
|
||||
- SC1076: Detect invalid `[/[[` arithmetic like `[ 1 + 2 = 3 ]`
|
||||
- SC1075: Suggest using `elif` over `else if`
|
||||
|
||||
### Fixed
|
||||
- Don't warn when comma separating elements in brace expansions
|
||||
- Improved detection of single quoted `sed` variables, e.g. `sed '$d'`
|
||||
- Parsing of arithmetic for loops using `{..}` instead of `do..done`
|
||||
- Don't treat the last pipeline stage as a subshell in ksh/zsh
|
||||
|
||||
|
||||
## v0.3.0 - 2014-01-19
|
||||
### Added
|
||||
- A man page (thanks Dridi!)
|
||||
- GCC compatible error reporting (`shellcheck -f gcc`)
|
||||
- CheckStyle compatible XML error reporting (`shellcheck -f checkstyle`)
|
||||
- Error codes for each warning, e.g. SC1234
|
||||
- Allow disabling warnings with `# shellcheck disable=SC1234`
|
||||
- Allow disabling warnings with `--exclude`
|
||||
- SC2103: Suggest using subshells over `cd foo; bar; cd ..`
|
||||
- SC2102: Warn about duplicates in char ranges, e.g. `[10-15]`
|
||||
- SC2101: Warn about named classes not inside a char range, e.g. `[:digit:]`
|
||||
- SC2100/SC2099: Warn about bad math expressions like `i=i+5`
|
||||
- SC2098/SC2097: Warn about `foo=bar echo $foo`
|
||||
- SC2095: Warn when using `ssh`/`ffmpeg` in `while read` loops
|
||||
- Better warnings for missing here doc tokens
|
||||
|
||||
### Fixed
|
||||
- Don't warn when single quoting variables with `ssh/perl/eval`
|
||||
- `${!var}` is now counted as a variable reference
|
||||
|
||||
### Removed
|
||||
- Suggestions about using parameter expansion over basename
|
||||
- The `jsoncheck` binary. Use `shellcheck -f json` instead.
|
||||
|
||||
|
||||
## v0.2.0 - 2013-10-27
|
||||
### Added
|
||||
- Suggest `./*` instead of `*` when passing globs to commands
|
||||
- Suggest `pgrep` over `ps | grep`
|
||||
- Warn about unicode quotes
|
||||
- Warn about assigned but unused variables
|
||||
- Inform about client side expansion when using `ssh`
|
||||
|
||||
### Fixed
|
||||
- CLI tool now uses exit codes and stderr canonically
|
||||
- Parsing of extglobs containing empty patterns
|
||||
- Parsing of bash style `eval foo=(bar)`
|
||||
- Parsing of expansions in here documents
|
||||
- Parsing of function names containing :+-
|
||||
- Don't warn about `find|xargs` when using `-print0`
|
||||
|
||||
|
||||
## v0.1.0 - 2013-07-23
|
||||
### Added
|
||||
- First release
|
72
Dockerfile
72
Dockerfile
@@ -1,11 +1,69 @@
|
||||
# Build-only image
|
||||
FROM ubuntu:18.04 AS build
|
||||
USER root
|
||||
WORKDIR /opt/shellCheck
|
||||
|
||||
# Install OS deps, including GHC from HVR-PPA
|
||||
# https://launchpad.net/~hvr/+archive/ubuntu/ghc
|
||||
RUN apt-get -yq update \
|
||||
&& apt-get -yq install software-properties-common \
|
||||
&& apt-add-repository -y "ppa:hvr/ghc" \
|
||||
&& apt-get -yq update \
|
||||
&& apt-get -yq install cabal-install-2.4 ghc-8.4.3 pandoc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PATH="/opt/ghc/bin:${PATH}"
|
||||
|
||||
# Use gold linker and check tools versions
|
||||
RUN ln -s $(which ld.gold) /usr/local/bin/ld && \
|
||||
cabal --version \
|
||||
&& ghc --version \
|
||||
&& ld --version
|
||||
|
||||
# Install Haskell deps
|
||||
# (This is a separate copy/run so that source changes don't require rebuilding)
|
||||
#
|
||||
# We also patch regex-tdfa and aeson removing hard-coded -O2 flag.
|
||||
# This makes compilation faster and binary smaller.
|
||||
# Performance loss is unnoticeable for ShellCheck
|
||||
#
|
||||
# Remember to update versions, once in a while.
|
||||
COPY ShellCheck.cabal ./
|
||||
RUN cabal update && \
|
||||
cabal get regex-tdfa-1.2.3.1 && sed -i 's/-O2//' regex-tdfa-1.2.3.1/regex-tdfa.cabal && \
|
||||
cabal get aeson-1.4.0.0 && sed -i 's/-O2//' aeson-1.4.0.0/aeson.cabal && \
|
||||
echo 'packages: . regex-tdfa-1.2.3.1 aeson-1.4.0.0 > cabal.project' && \
|
||||
cabal new-build --dependencies-only \
|
||||
--disable-executable-dynamic --enable-split-sections --disable-tests
|
||||
|
||||
# Copy source and build it
|
||||
COPY LICENSE Setup.hs shellcheck.hs shellcheck.1.md ./
|
||||
COPY src src
|
||||
COPY test test
|
||||
# This SED is the only "nastyness" we have to do
|
||||
# Hopefully soon we could add per-component ld-options to cabal.project
|
||||
RUN sed -i 's/-- STATIC/ld-options: -static -pthread -Wl,--gc-sections/' ShellCheck.cabal && \
|
||||
cat ShellCheck.cabal && \
|
||||
cabal new-build \
|
||||
--disable-executable-dynamic --enable-split-sections --disable-tests && \
|
||||
cp $(find dist-newstyle -type f -name shellcheck) . && \
|
||||
strip --strip-all shellcheck && \
|
||||
file shellcheck && \
|
||||
ls -l shellcheck
|
||||
|
||||
RUN mkdir -p /out/bin && \
|
||||
cp shellcheck /out/bin/
|
||||
|
||||
# Resulting Alpine image
|
||||
FROM alpine:latest
|
||||
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
|
||||
COPY --from=build /out /
|
||||
|
||||
MAINTAINER Nikyle Nguyen <NLKNguyen@MSN.com>
|
||||
|
||||
COPY package/bin/shellcheck /usr/local/bin/
|
||||
COPY package/lib/ /usr/local/lib/
|
||||
|
||||
RUN ldconfig /usr/local/lib
|
||||
# DELETE-MARKER (Remove everything below to keep the alpine image)
|
||||
|
||||
# Resulting ShellCheck image
|
||||
FROM scratch
|
||||
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
|
||||
WORKDIR /mnt
|
||||
ENTRYPOINT ["shellcheck"]
|
||||
COPY --from=build /out /
|
||||
ENTRYPOINT ["/bin/shellcheck"]
|
||||
|
@@ -1,54 +0,0 @@
|
||||
FROM mitchty/alpine-ghc:latest
|
||||
|
||||
MAINTAINER Nikyle Nguyen <NLKNguyen@MSN.com>
|
||||
|
||||
RUN apk add --no-cache build-base
|
||||
|
||||
RUN mkdir -p /usr/src/shellcheck
|
||||
WORKDIR /usr/src/shellcheck
|
||||
|
||||
# # ------------------------------------------------------------
|
||||
# # Build & Test
|
||||
# # ------------------------------------------------------------
|
||||
|
||||
# Obtain the dependencies first, which are less likely to change, in order to reduce
|
||||
# subsequent build time by leveraging image cache. This benefits developers when they
|
||||
# build their code with this image locally. In case of Travis CI, this doesn't help
|
||||
# reduce building time because Travis CI doesn't use cache.
|
||||
COPY ShellCheck.cabal .
|
||||
RUN cabal update && cabal install --only-dependencies
|
||||
|
||||
# Copy the rest of the source files, including ShellCheck.cabal again but doesn't matter
|
||||
COPY . .
|
||||
|
||||
# Build
|
||||
RUN cabal install
|
||||
|
||||
# Test
|
||||
RUN cabal test
|
||||
|
||||
# # ------------------------------------------------------------
|
||||
# # Set PATH
|
||||
# # ------------------------------------------------------------
|
||||
|
||||
# Add runtime path to easily reach the executable file. This only exists during build.
|
||||
ENV PATH "/root/.cabal/bin:$PATH"
|
||||
|
||||
# Make it permanent for someone who login to the container of this image
|
||||
RUN echo "export PATH=${PATH}" >> /etc/profile
|
||||
|
||||
# # ------------------------------------------------------------
|
||||
# # Extract Binaries
|
||||
# # ------------------------------------------------------------
|
||||
|
||||
# Get shellcheck binary
|
||||
RUN mkdir -p /package/bin/
|
||||
RUN cp $(which shellcheck) /package/bin/
|
||||
|
||||
# Get shared libraries using magic
|
||||
RUN mkdir -p /package/lib/
|
||||
RUN ldd $(which shellcheck) | grep "=> /" | awk '{print $3}' | xargs -I '{}' cp -v '{}' /package/lib/
|
||||
|
||||
|
||||
# Copy shellcheck package out to mounted directory
|
||||
CMD ["cp", "-avr", "/package", "/mnt/"]
|
18
LICENSE
18
LICENSE
@@ -1,7 +1,17 @@
|
||||
Employer mandated disclaimer:
|
||||
|
||||
I am providing code in the repository to you under an open source license.
|
||||
Because this is my personal repository, the license you receive to my code is
|
||||
from me and other individual contributors, and not my employer (Facebook).
|
||||
|
||||
- Vidar "koala_man" Holen
|
||||
|
||||
----
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
@@ -645,7 +655,7 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
@@ -664,11 +674,11 @@ might be different; for a GUI interface, you would use an "about box".
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
<https://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
|
417
README.md
417
README.md
@@ -1,46 +1,76 @@
|
||||
[](https://travis-ci.org/koalaman/shellcheck)
|
||||
|
||||
# ShellCheck - A shell script static analysis tool
|
||||
|
||||
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts:
|
||||
|
||||
.
|
||||

|
||||
|
||||
The goals of ShellCheck are
|
||||
|
||||
- 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 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 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.
|
||||
- To point out subtle caveats, corner cases and pitfalls that may cause an
|
||||
advanced user's otherwise working script to fail under future circumstances.
|
||||
|
||||
See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for examples of what ShellCheck can help you identify!
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [How to use](#how-to-use)
|
||||
- [On the web](#on-the-web)
|
||||
- [From your terminal](#from-your-terminal)
|
||||
- [In your editor](#in-your-editor)
|
||||
- [In your build or test suites](#in-your-build-or-test-suites)
|
||||
- [Installing](#installing)
|
||||
- [Travis CI](#travis-ci)
|
||||
- [Compiling from source](#compiling-from-source)
|
||||
- [Installing Cabal](#installing-cabal)
|
||||
- [Compiling ShellCheck](#compiling-shellcheck)
|
||||
- [Running tests](#running-tests)
|
||||
- [Gallery of bad code](#gallery-of-bad-code)
|
||||
- [Quoting](#quoting)
|
||||
- [Conditionals](#conditionals)
|
||||
- [Frequently misused commands](#frequently-misused-commands)
|
||||
- [Common beginner's mistakes](#common-beginners-mistakes)
|
||||
- [Style](#style)
|
||||
- [Data and typing errors](#data-and-typing-errors)
|
||||
- [Robustness](#robustness)
|
||||
- [Portability](#portability)
|
||||
- [Miscellaneous](#miscellaneous)
|
||||
- [Testimonials](#testimonials)
|
||||
- [Ignoring issues](#ignoring-issues)
|
||||
- [Reporting bugs](#reporting-bugs)
|
||||
- [Contributing](#contributing)
|
||||
- [Copyright](#copyright)
|
||||
|
||||
## How to use
|
||||
There are a variety of ways to use ShellCheck!
|
||||
|
||||
There are a number of ways to use ShellCheck!
|
||||
|
||||
#### On the web
|
||||
Paste a shell script on http://www.shellcheck.net for instant feedback.
|
||||
### On the web
|
||||
|
||||
[ShellCheck.net](http://www.shellcheck.net) is always synchronized to the latest git commit, and is the simplest way to give ShellCheck a go. Tell your friends!
|
||||
Paste a shell script on https://www.shellcheck.net for instant feedback.
|
||||
|
||||
[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
|
||||
Run `shellcheck yourscript` in your terminal for instant output, as seen above.
|
||||
|
||||
|
||||
#### In your editor
|
||||
### In your editor
|
||||
|
||||
You can see ShellCheck suggestions directly in a variety of editors.
|
||||
|
||||
* Vim, through [Syntastic](https://github.com/scrooloose/syntastic):
|
||||
* Vim, through [ALE](https://github.com/w0rp/ale), [Neomake](https://github.com/neomake/neomake), or [Syntastic](https://github.com/scrooloose/syntastic):
|
||||
|
||||
.
|
||||
.
|
||||
|
||||
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck):
|
||||
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck) or [Flymake](https://github.com/federicotdn/flymake-shellcheck):
|
||||
|
||||
.
|
||||
|
||||
@@ -48,14 +78,15 @@ You can see ShellCheck suggestions directly in a variety of editors.
|
||||
|
||||
* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
|
||||
|
||||
* VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck).
|
||||
|
||||
* Most other editors, through [GCC error compatibility](shellcheck.1.md#user-content-formats).
|
||||
|
||||
### In your build or test suites
|
||||
|
||||
#### In your build or test suites
|
||||
While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites.
|
||||
|
||||
Use ShellCheck's exit code, or its [CheckStyle compatible XML output](shellcheck.1.md#user-content-formats). There's also a simple JSON output format for easy integration.
|
||||
|
||||
ShellCheck makes canonical use of exit codes, and can output simple JSON, CheckStyle compatible XML, GCC compatible warnings as well as human readable text (with or without ANSI colors). See the [Integration](https://github.com/koalaman/shellcheck/wiki/Integration) wiki page for more documentation.
|
||||
|
||||
## Installing
|
||||
|
||||
@@ -64,50 +95,121 @@ The easiest way to install ShellCheck locally is through your package manager.
|
||||
On systems with Cabal (installs to `~/.cabal/bin`):
|
||||
|
||||
cabal update
|
||||
cabal install shellcheck
|
||||
cabal install ShellCheck
|
||||
|
||||
On systems with Stack (installs to `~/.local/bin`):
|
||||
|
||||
stack update
|
||||
stack install ShellCheck
|
||||
|
||||
On Debian based distros:
|
||||
|
||||
apt-get install shellcheck
|
||||
|
||||
On Arch Linux based distros:
|
||||
|
||||
pacman -S shellcheck
|
||||
|
||||
or get the dependency free [shellcheck-static](https://aur.archlinux.org/packages/shellcheck-static/) from the AUR.
|
||||
|
||||
On Gentoo based distros:
|
||||
|
||||
emerge --ask shellcheck
|
||||
|
||||
On EPEL based distros:
|
||||
|
||||
yum -y install epel-release
|
||||
yum install ShellCheck
|
||||
|
||||
On Fedora based distros:
|
||||
|
||||
dnf install ShellCheck
|
||||
|
||||
On FreeBSD:
|
||||
|
||||
pkg install hs-ShellCheck
|
||||
|
||||
On OS X with homebrew:
|
||||
|
||||
brew install shellcheck
|
||||
|
||||
On OS X with MacPorts:
|
||||
On OpenBSD:
|
||||
|
||||
port install shellcheck
|
||||
pkg_add shellcheck
|
||||
|
||||
On openSUSE:Tumbleweed:
|
||||
On openSUSE
|
||||
|
||||
zypper in ShellCheck
|
||||
|
||||
On other openSUSE distributions:
|
||||
Or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
|
||||
|
||||
add OBS devel:languages:haskell repository from https://build.opensuse.org/project/repositories/devel:languages:haskell
|
||||
On Solus:
|
||||
|
||||
zypper ar http://download.opensuse.org/repositories/devel:/languages:/haskell/openSUSE_$(version)/devel:languages:haskell.repo
|
||||
zypper in ShellCheck
|
||||
eopkg install shellcheck
|
||||
|
||||
On Windows (via [scoop](http://scoop.sh)):
|
||||
|
||||
or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
|
||||
scoop install shellcheck
|
||||
|
||||
From Snap Store:
|
||||
|
||||
snap install --channel=edge shellcheck
|
||||
|
||||
From Docker Hub:
|
||||
|
||||
```sh
|
||||
docker pull koalaman/shellcheck:stable # Or :v0.4.7 for that version, or :latest for daily builds
|
||||
docker run -v "$PWD:/mnt" koalaman/shellcheck myscript
|
||||
```
|
||||
|
||||
or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend. It works exactly like a regular Alpine image, but has shellcheck preinstalled.
|
||||
|
||||
Alternatively, you can download pre-compiled binaries for the latest release here:
|
||||
|
||||
* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
|
||||
* [Linux, armv6hf](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
|
||||
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-stable.zip)
|
||||
|
||||
or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds.
|
||||
|
||||
Distro packages already come with a `man` page. If you are building from source, it can be installed with:
|
||||
|
||||
pandoc -s -t man shellcheck.1.md -o shellcheck.1
|
||||
sudo mv shellcheck.1 /usr/share/man/man1
|
||||
|
||||
## Travis CI
|
||||
|
||||
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
|
||||
|
||||
If you still want to do so in order to upgrade at your leisure or ensure the latest release, follow the steps to install the shellcheck binary, bellow.
|
||||
|
||||
## Installing the shellcheck binary
|
||||
|
||||
*Pre-requisite*: the program 'xz' needs to be installed on the system.
|
||||
To install it on debian/ubuntu/linux mint, run `apt install xz-utils`.
|
||||
To install it on Redhat/Fedora/CentOS, run `yum -y install xz`.
|
||||
|
||||
```bash
|
||||
export scversion="stable" # or "v0.4.7", or "latest"
|
||||
wget "https://storage.googleapis.com/shellcheck/shellcheck-${scversion}.linux.x86_64.tar.xz"
|
||||
tar --xz -xvf shellcheck-"${scversion}".linux.x86_64.tar.xz
|
||||
cp shellcheck-"${scversion}"/shellcheck /usr/bin/
|
||||
shellcheck --version
|
||||
```
|
||||
|
||||
## Compiling from source
|
||||
|
||||
This section describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile.
|
||||
|
||||
### Installing Cabal
|
||||
|
||||
#### Installing Cabal
|
||||
ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `brew`, `emerge`, `yum`, or `zypper`).
|
||||
|
||||
ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `yum`, `zypper` or `brew`).
|
||||
On MacOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source.
|
||||
|
||||
brew install cask
|
||||
brew cask install haskell-platform
|
||||
cabal install cabal-install
|
||||
|
||||
On MacPorts, the package is instead called `hs-cabal-install`, while native Windows users should install the latest version of the Haskell platform from https://www.haskell.org/platform/
|
||||
|
||||
@@ -115,22 +217,30 @@ Verify that `cabal` is installed and update its dependency list with
|
||||
|
||||
$ cabal update
|
||||
|
||||
#### Compiling ShellCheck
|
||||
### Compiling ShellCheck
|
||||
|
||||
`git clone` this repository, and `cd` to the ShellCheck source directory to build/install:
|
||||
|
||||
$ cabal install
|
||||
|
||||
Or if you intend to run the tests:
|
||||
|
||||
$ cabal install --enable-tests
|
||||
|
||||
This will compile ShellCheck and install it to your `~/.cabal/bin` directory.
|
||||
|
||||
Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
|
||||
|
||||
export PATH="$HOME/.cabal/bin:$PATH"
|
||||
```sh
|
||||
export PATH="$HOME/.cabal/bin:$PATH"
|
||||
```
|
||||
|
||||
Log out and in again, and verify that your PATH is set up correctly:
|
||||
|
||||
$ which shellcheck
|
||||
~/.cabal/bin/shellcheck
|
||||
```sh
|
||||
$ which shellcheck
|
||||
~/.cabal/bin/shellcheck
|
||||
```
|
||||
|
||||
On native Windows, the `PATH` should already be set up, but the system
|
||||
may use a legacy codepage. In `cmd.exe`, `powershell.exe` and Powershell ISE,
|
||||
@@ -144,147 +254,170 @@ In Powershell ISE, you may need to additionally update the output encoding:
|
||||
|
||||
> [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
#### Running tests
|
||||
### Running tests
|
||||
|
||||
To run the unit test suite:
|
||||
|
||||
$ cabal test
|
||||
|
||||
|
||||
## Gallery of bad code
|
||||
|
||||
So what kind of things does ShellCheck look for? Here is an incomplete list of detected issues.
|
||||
|
||||
#### Quoting
|
||||
### Quoting
|
||||
|
||||
ShellCheck can recognize several types of incorrect quoting:
|
||||
|
||||
echo $1 # Unquoted variables
|
||||
find . -name *.ogg # Unquoted find/grep patterns
|
||||
rm "~/my file.txt" # Quoted tilde expansion
|
||||
v='--verbose="true"'; cmd $v # Literal quotes in variables
|
||||
for f in "*.ogg" # Incorrectly quoted 'for' loops
|
||||
touch $@ # Unquoted $@
|
||||
echo 'Don't forget to restart!' # Singlequote closed by apostrophe
|
||||
echo 'Don\'t try this at home' # Attempting to escape ' in ''
|
||||
echo 'Path is $PATH' # Variables in single quotes
|
||||
trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap
|
||||
```sh
|
||||
echo $1 # Unquoted variables
|
||||
find . -name *.ogg # Unquoted find/grep patterns
|
||||
rm "~/my file.txt" # Quoted tilde expansion
|
||||
v='--verbose="true"'; cmd $v # Literal quotes in variables
|
||||
for f in "*.ogg" # Incorrectly quoted 'for' loops
|
||||
touch $@ # Unquoted $@
|
||||
echo 'Don't forget to restart!' # Singlequote closed by apostrophe
|
||||
echo 'Don\'t try this at home' # Attempting to escape ' in ''
|
||||
echo 'Path is $PATH' # Variables in single quotes
|
||||
trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap
|
||||
```
|
||||
|
||||
|
||||
#### Conditionals
|
||||
### Conditionals
|
||||
|
||||
ShellCheck can recognize many types of incorrect test statements.
|
||||
|
||||
[[ n != 0 ]] # Constant test expressions
|
||||
[[ -e *.mpg ]] # Existence checks of globs
|
||||
[[ $foo==0 ]] # Always true due to missing spaces
|
||||
[[ -n "$foo " ]] # Always true due to literals
|
||||
[[ $foo =~ "fo+" ]] # Quoted regex in =~
|
||||
[ foo =~ re ] # Unsupported [ ] operators
|
||||
[ $1 -eq "shellcheck" ] # Numerical comparison of strings
|
||||
[ $n && $m ] # && in [ .. ]
|
||||
[ grep -q foo file ] # Command without $(..)
|
||||
```sh
|
||||
[[ n != 0 ]] # Constant test expressions
|
||||
[[ -e *.mpg ]] # Existence checks of globs
|
||||
[[ $foo==0 ]] # Always true due to missing spaces
|
||||
[[ -n "$foo " ]] # Always true due to literals
|
||||
[[ $foo =~ "fo+" ]] # Quoted regex in =~
|
||||
[ foo =~ re ] # Unsupported [ ] operators
|
||||
[ $1 -eq "shellcheck" ] # Numerical comparison of strings
|
||||
[ $n && $m ] # && in [ .. ]
|
||||
[ grep -q foo file ] # Command without $(..)
|
||||
[[ "$$file" == *.jpg ]] # Comparisons that can't succeed
|
||||
(( 1 -lt 2 )) # Using test operators in ((..))
|
||||
```
|
||||
|
||||
|
||||
#### Frequently misused commands
|
||||
### Frequently misused commands
|
||||
|
||||
ShellCheck can recognize instances where commands are used incorrectly:
|
||||
|
||||
grep '*foo*' file # Globs in regex contexts
|
||||
find . -exec foo {} && bar {} \; # Prematurely terminated find -exec
|
||||
sudo echo 'Var=42' > /etc/profile # Redirecting sudo
|
||||
time --format=%s sleep 10 # Passing time(1) flags to time builtin
|
||||
while read h; do ssh "$h" uptime # Commands eating while loop input
|
||||
alias archive='mv $1 /backup' # Defining aliases with arguments
|
||||
tr -cd '[a-zA-Z0-9]' # [] around ranges in tr
|
||||
exec foo; echo "Done!" # Misused 'exec'
|
||||
find -name \*.bak -o -name \*~ -delete # Implicit precedence in find
|
||||
f() { whoami; }; sudo f # External use of internal functions
|
||||
```sh
|
||||
grep '*foo*' file # Globs in regex contexts
|
||||
find . -exec foo {} && bar {} \; # Prematurely terminated find -exec
|
||||
sudo echo 'Var=42' > /etc/profile # Redirecting sudo
|
||||
time --format=%s sleep 10 # Passing time(1) flags to time builtin
|
||||
while read h; do ssh "$h" uptime # Commands eating while loop input
|
||||
alias archive='mv $1 /backup' # Defining aliases with arguments
|
||||
tr -cd '[a-zA-Z0-9]' # [] around ranges in tr
|
||||
exec foo; echo "Done!" # Misused 'exec'
|
||||
find -name \*.bak -o -name \*~ -delete # Implicit precedence in find
|
||||
# find . -exec foo > bar \; # Redirections in find
|
||||
f() { whoami; }; sudo f # External use of internal functions
|
||||
```
|
||||
|
||||
|
||||
#### Common beginner's mistakes
|
||||
### Common beginner's mistakes
|
||||
|
||||
ShellCheck recognizes many common beginner's syntax errors:
|
||||
|
||||
var = 42 # Spaces around = in assignments
|
||||
$foo=42 # $ in assignments
|
||||
for $var in *; do ... # $ in for loop variables
|
||||
var$n="Hello" # Wrong indirect assignment
|
||||
echo ${var$n} # Wrong indirect reference
|
||||
var=(1, 2, 3) # Comma separated arrays
|
||||
echo "Argument 10 is $10" # Positional parameter misreference
|
||||
if $(myfunction); then ..; fi # Wrapping commands in $()
|
||||
else if othercondition; then .. # Using 'else if'
|
||||
```sh
|
||||
var = 42 # Spaces around = in assignments
|
||||
$foo=42 # $ in assignments
|
||||
for $var in *; do ... # $ in for loop variables
|
||||
var$n="Hello" # Wrong indirect assignment
|
||||
echo ${var$n} # Wrong indirect reference
|
||||
var=(1, 2, 3) # Comma separated arrays
|
||||
array=( [index] = value ) # Incorrect index initialization
|
||||
echo $var[14] # Missing {} in array references
|
||||
echo "Argument 10 is $10" # Positional parameter misreference
|
||||
if $(myfunction); then ..; fi # Wrapping commands in $()
|
||||
else if othercondition; then .. # Using 'else if'
|
||||
f; f() { echo "hello world; } # Using function before definition
|
||||
[ false ] # 'false' being true
|
||||
if ( -f file ) # Using (..) instead of test
|
||||
```
|
||||
|
||||
|
||||
#### Style
|
||||
### Style
|
||||
|
||||
ShellCheck can make suggestions to improve style:
|
||||
|
||||
[[ -z $(find /tmp | grep mpg) ]] # Use grep -q instead
|
||||
a >> log; b >> log; c >> log # Use a redirection block instead
|
||||
echo "The time is `date`" # Use $() instead
|
||||
cd dir; process *; cd ..; # Use subshells instead
|
||||
echo $[1+2] # Use standard $((..)) instead of old $[]
|
||||
echo $(($RANDOM % 6)) # Don't use $ on variables in $((..))
|
||||
echo "$(date)" # Useless use of echo
|
||||
cat file | grep foo # Useless use of cat
|
||||
```sh
|
||||
[[ -z $(find /tmp | grep mpg) ]] # Use grep -q instead
|
||||
a >> log; b >> log; c >> log # Use a redirection block instead
|
||||
echo "The time is `date`" # Use $() instead
|
||||
cd dir; process *; cd ..; # Use subshells instead
|
||||
echo $[1+2] # Use standard $((..)) instead of old $[]
|
||||
echo $(($RANDOM % 6)) # Don't use $ on variables in $((..))
|
||||
echo "$(date)" # Useless use of echo
|
||||
cat file | grep foo # Useless use of cat
|
||||
```
|
||||
|
||||
|
||||
#### Data and typing errors
|
||||
### Data and typing errors
|
||||
|
||||
ShellCheck can recognize issues related to data and typing:
|
||||
|
||||
args="$@" # Assigning arrays to strings
|
||||
files=(foo bar); echo "$files" # Referencing arrays as strings
|
||||
printf "%s\n" "Arguments: $@." # Concatenating strings and arrays.
|
||||
[[ $# > 2 ]] # Comparing numbers as strings
|
||||
var=World; echo "Hello " var # Unused lowercase variables
|
||||
echo "Hello $name" # Unassigned lowercase variables
|
||||
cmd | read bar; echo $bar # Assignments in subshells
|
||||
```sh
|
||||
args="$@" # Assigning arrays to strings
|
||||
files=(foo bar); echo "$files" # Referencing arrays as strings
|
||||
declare -A arr=(foo bar) # Associative arrays without index
|
||||
printf "%s\n" "Arguments: $@." # Concatenating strings and arrays
|
||||
[[ $# > 2 ]] # Comparing numbers as strings
|
||||
var=World; echo "Hello " var # Unused lowercase variables
|
||||
echo "Hello $name" # Unassigned lowercase variables
|
||||
cmd | read bar; echo $bar # Assignments in subshells
|
||||
cat foo | cp bar # Piping to commands that don't read
|
||||
printf '%s: %s\n' foo # Mismatches in printf argument count
|
||||
```
|
||||
|
||||
|
||||
#### Robustness
|
||||
### Robustness
|
||||
|
||||
ShellCheck can make suggestions for improving the robustness of a script:
|
||||
|
||||
rm -rf "$STEAMROOT/"* # Catastrophic rm
|
||||
touch ./-l; ls * # Globs that could become options
|
||||
find . -exec sh -c 'a && b {}' \; # Find -exec shell injection
|
||||
printf "Hello $name" # Variables in printf format
|
||||
for f in $(ls *.txt); do # Iterating over ls output
|
||||
export MYVAR=$(cmd) # Masked exit codes
|
||||
```sh
|
||||
rm -rf "$STEAMROOT/"* # Catastrophic rm
|
||||
touch ./-l; ls * # Globs that could become options
|
||||
find . -exec sh -c 'a && b {}' \; # Find -exec shell injection
|
||||
printf "Hello $name" # Variables in printf format
|
||||
for f in $(ls *.txt); do # Iterating over ls output
|
||||
export MYVAR=$(cmd) # Masked exit codes
|
||||
case $version in 2.*) :;; 2.6.*) # Shadowed case branches
|
||||
```
|
||||
|
||||
|
||||
#### Portability
|
||||
### Portability
|
||||
|
||||
ShellCheck will warn when using features not supported by the shebang. For example, if you set the shebang to `#!/bin/sh`, ShellCheck will warn about portability issues similar to `checkbashisms`:
|
||||
|
||||
```sh
|
||||
echo {1..$n} # Works in ksh, but not bash/dash/sh
|
||||
echo {1..10} # Works in ksh and bash, but not dash/sh
|
||||
echo -n 42 # Works in ksh, bash and dash, undefined in sh
|
||||
trap 'exit 42' sigint # Unportable signal spec
|
||||
cmd &> file # Unportable redirection operator
|
||||
read foo < /dev/tcp/host/22 # Unportable intercepted files
|
||||
foo-bar() { ..; } # Undefined/unsupported function name
|
||||
[ $UID = 0 ] # Variable undefined in dash/sh
|
||||
local var=value # local is undefined in sh
|
||||
time sleep 1 | sleep 5 # Undefined uses of 'time'
|
||||
```
|
||||
|
||||
echo {1..$n} # Works in ksh, but not bash/dash/sh
|
||||
echo {1..10} # Works in ksh and bash, but not dash/sh
|
||||
echo -n 42 # Works in ksh, bash and dash, undefined in sh
|
||||
trap 'exit 42' sigint # Unportable signal spec
|
||||
cmd &> file # Unportable redirection operator
|
||||
read foo < /dev/tcp/host/22 # Unportable intercepted files
|
||||
foo-bar() { ..; } # Undefined/unsupported function name
|
||||
[ $UID = 0 ] # Variable undefined in dash/sh
|
||||
local var=value # local is undefined in sh
|
||||
|
||||
|
||||
#### Miscellaneous
|
||||
### Miscellaneous
|
||||
|
||||
ShellCheck recognizes a menagerie of other issues:
|
||||
|
||||
PS1='\e[0;32m\$\e[0m ' # PS1 colors not in \[..\]
|
||||
PATH="$PATH:~/bin" # Literal tilde in $PATH
|
||||
rm “file” # Unicode quotes
|
||||
echo "Hello world" # Carriage return / DOS line endings
|
||||
var=42 echo $var # Expansion of inlined environment
|
||||
#!/bin/bash -x -e # Common shebang errors
|
||||
echo $((n/180*100)) # Unnecessary loss of precision
|
||||
ls *[:digit:].txt # Bad character class globs
|
||||
sed 's/foo/bar/' file > file # Redirecting to input
|
||||
|
||||
```sh
|
||||
PS1='\e[0;32m\$\e[0m ' # PS1 colors not in \[..\]
|
||||
PATH="$PATH:~/bin" # Literal tilde in $PATH
|
||||
rm “file” # Unicode quotes
|
||||
echo "Hello world" # Carriage return / DOS line endings
|
||||
echo hello \ # Trailing spaces after \
|
||||
var=42 echo $var # Expansion of inlined environment
|
||||
#!/bin/bash -x -e # Common shebang errors
|
||||
echo $((n/180*100)) # Unnecessary loss of precision
|
||||
ls *[:digit:].txt # Bad character class globs
|
||||
sed 's/foo/bar/' file > file # Redirecting to input
|
||||
while getopts "a" f; do case $f in "b") # Unhandled getopts flags
|
||||
```
|
||||
|
||||
## Testimonials
|
||||
|
||||
@@ -293,6 +426,11 @@ ShellCheck recognizes a menagerie of other issues:
|
||||
Alexander Tarasikov,
|
||||
[via Twitter](https://twitter.com/astarasikov/status/568825996532707330)
|
||||
|
||||
## Ignoring issues
|
||||
|
||||
Issues can be ignored via environmental variable, command line, individually or globally within a file:
|
||||
|
||||
https://github.com/koalaman/shellcheck/wiki/Ignore
|
||||
|
||||
## Reporting bugs
|
||||
|
||||
@@ -300,19 +438,24 @@ Please use the GitHub issue tracker for any bugs or feature suggestions:
|
||||
|
||||
https://github.com/koalaman/shellcheck/issues
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Please submit patches to code or documentation as GitHub pull requests!
|
||||
Please submit patches to code or documentation as GitHub pull requests! Check
|
||||
out the [DevGuide](https://github.com/koalaman/shellcheck/wiki/DevGuide) on the
|
||||
ShellCheck Wiki.
|
||||
|
||||
Contributions must be licensed under the GNU GPLv3.
|
||||
The contributor retains the copyright.
|
||||
|
||||
|
||||
## Copyright
|
||||
|
||||
ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE).
|
||||
|
||||
Copyright 2012-2015, Vidar 'koala_man' Holen and contributors.
|
||||
Copyright 2012-2018, Vidar 'koala_man' Holen and contributors.
|
||||
|
||||
Happy ShellChecking!
|
||||
|
||||
|
||||
## Other Resources
|
||||
* The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221).
|
||||
* ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)!
|
||||
|
53
Setup.hs
53
Setup.hs
@@ -1,3 +1,8 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# OPTIONS_GHC -Wall #-}
|
||||
|
||||
module Main (main) where
|
||||
|
||||
import Distribution.PackageDescription (
|
||||
HookedBuildInfo,
|
||||
emptyHookedBuildInfo )
|
||||
@@ -9,12 +14,42 @@ import Distribution.Simple (
|
||||
import Distribution.Simple.Setup ( SDistFlags )
|
||||
|
||||
import System.Process ( system )
|
||||
import System.Directory ( doesFileExist, getModificationTime )
|
||||
|
||||
#ifndef MIN_VERSION_cabal_doctest
|
||||
#define MIN_VERSION_cabal_doctest(x,y,z) 0
|
||||
#endif
|
||||
|
||||
#if MIN_VERSION_cabal_doctest(1,0,0)
|
||||
|
||||
import Distribution.Extra.Doctest ( addDoctestsUserHook )
|
||||
main :: IO ()
|
||||
main = defaultMainWithHooks $ addDoctestsUserHook "doctests" myHooks
|
||||
where
|
||||
myHooks = simpleUserHooks { preSDist = myPreSDist }
|
||||
|
||||
#else
|
||||
|
||||
#ifdef MIN_VERSION_Cabal
|
||||
-- If the macro is defined, we have new cabal-install,
|
||||
-- but for some reason we don't have cabal-doctest in package-db
|
||||
--
|
||||
-- Probably we are running cabal sdist, when otherwise using new-build
|
||||
-- workflow
|
||||
#warning You are configuring this package without cabal-doctest installed. \
|
||||
The doctests test-suite will not work as a result. \
|
||||
To fix this, install cabal-doctest before configuring.
|
||||
#endif
|
||||
|
||||
main :: IO ()
|
||||
main = defaultMainWithHooks myHooks
|
||||
where
|
||||
myHooks = simpleUserHooks { preSDist = myPreSDist }
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
-- | This hook will be executed before e.g. @cabal sdist@. It runs
|
||||
-- pandoc to create the man page from shellcheck.1.md. If the pandoc
|
||||
-- command is not found, this will fail with an error message:
|
||||
@@ -27,10 +62,20 @@ main = defaultMainWithHooks myHooks
|
||||
--
|
||||
myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo
|
||||
myPreSDist _ _ = do
|
||||
putStrLn "Building the man page (shellcheck.1) with pandoc..."
|
||||
putStrLn pandoc_cmd
|
||||
result <- system pandoc_cmd
|
||||
putStrLn $ "pandoc exited with " ++ show result
|
||||
exists <- doesFileExist "shellcheck.1"
|
||||
if exists
|
||||
then do
|
||||
source <- getModificationTime "shellcheck.1.md"
|
||||
target <- getModificationTime "shellcheck.1"
|
||||
if target < source
|
||||
then makeManPage
|
||||
else putStrLn "shellcheck.1 is more recent than shellcheck.1.md"
|
||||
else makeManPage
|
||||
return emptyHookedBuildInfo
|
||||
where
|
||||
makeManPage = do
|
||||
putStrLn "Building the man page (shellcheck.1) with pandoc..."
|
||||
putStrLn pandoc_cmd
|
||||
result <- system pandoc_cmd
|
||||
putStrLn $ "pandoc exited with " ++ show result
|
||||
pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"
|
||||
|
@@ -1,12 +1,12 @@
|
||||
Name: ShellCheck
|
||||
Version: 0.4.5
|
||||
Version: 0.6.0
|
||||
Synopsis: Shell script analysis tool
|
||||
License: GPL-3
|
||||
License-file: LICENSE
|
||||
Category: Static Analysis
|
||||
Author: Vidar Holen
|
||||
Maintainer: vidar@vidarholen.net
|
||||
Homepage: http://www.shellcheck.net/
|
||||
Homepage: https://www.shellcheck.net/
|
||||
Build-Type: Custom
|
||||
Cabal-Version: >= 1.8
|
||||
Bug-reports: https://github.com/koalaman/shellcheck/issues
|
||||
@@ -28,23 +28,36 @@ Extra-Source-Files:
|
||||
shellcheck.1.md
|
||||
-- built with a cabal sdist hook
|
||||
shellcheck.1
|
||||
-- tests
|
||||
test/shellcheck.hs
|
||||
|
||||
custom-setup
|
||||
setup-depends:
|
||||
base >= 4 && <5,
|
||||
directory >= 1.2 && <1.4,
|
||||
process >= 1.0 && <1.7,
|
||||
cabal-doctest >= 1.0.6 && <1.1,
|
||||
Cabal >= 1.10 && <2.5
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: git://github.com/koalaman/shellcheck.git
|
||||
|
||||
library
|
||||
hs-source-dirs: src
|
||||
if impl(ghc < 8.0)
|
||||
build-depends:
|
||||
semigroups
|
||||
build-depends:
|
||||
base >= 4 && < 5,
|
||||
containers,
|
||||
-- GHC 7.6.3 (base 4.6.0.1) is buggy (#1131, #1119) in optimized mode.
|
||||
-- Just disable that version entirely to fail fast.
|
||||
aeson,
|
||||
base > 4.6.0.1 && < 5,
|
||||
bytestring,
|
||||
containers >= 0.5,
|
||||
deepseq >= 1.4.0.0,
|
||||
directory,
|
||||
json,
|
||||
mtl >= 2.2.1,
|
||||
parsec,
|
||||
regex-tdfa,
|
||||
QuickCheck >= 2.7.4,
|
||||
-- When cabal supports it, move this to setup-depends:
|
||||
process
|
||||
exposed-modules:
|
||||
@@ -55,6 +68,7 @@ library
|
||||
ShellCheck.AnalyzerLib
|
||||
ShellCheck.Checker
|
||||
ShellCheck.Checks.Commands
|
||||
ShellCheck.Checks.ShellSupport
|
||||
ShellCheck.Data
|
||||
ShellCheck.Formatter.Format
|
||||
ShellCheck.Formatter.CheckStyle
|
||||
@@ -68,29 +82,36 @@ library
|
||||
Paths_ShellCheck
|
||||
|
||||
executable shellcheck
|
||||
if impl(ghc < 8.0)
|
||||
build-depends:
|
||||
semigroups
|
||||
build-depends:
|
||||
ShellCheck,
|
||||
aeson,
|
||||
base >= 4 && < 5,
|
||||
bytestring,
|
||||
deepseq >= 1.4.0.0,
|
||||
ShellCheck,
|
||||
containers,
|
||||
directory,
|
||||
json,
|
||||
mtl >= 2.2.1,
|
||||
parsec,
|
||||
regex-tdfa,
|
||||
QuickCheck >= 2.7.4
|
||||
parsec >= 3.0,
|
||||
regex-tdfa
|
||||
main-is: shellcheck.hs
|
||||
|
||||
test-suite test-shellcheck
|
||||
type: exitcode-stdio-1.0
|
||||
build-depends:
|
||||
ShellCheck,
|
||||
base >= 4 && < 5,
|
||||
containers,
|
||||
directory,
|
||||
json,
|
||||
mtl >= 2.2.1,
|
||||
parsec,
|
||||
regex-tdfa,
|
||||
QuickCheck >= 2.7.4
|
||||
main-is: test/shellcheck.hs
|
||||
-- Marker to add flags for static linking
|
||||
-- STATIC
|
||||
|
||||
test-suite doctests
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: doctests.hs
|
||||
build-depends:
|
||||
base,
|
||||
doctest >= 0.16.0 && <0.17,
|
||||
QuickCheck >=2.11 && <2.13,
|
||||
ShellCheck,
|
||||
template-haskell
|
||||
|
||||
x-doctest-options: --fast
|
||||
|
||||
ghc-options: -Wall -threaded
|
||||
hs-source-dirs: test
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,668 +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/>.
|
||||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
module ShellCheck.AnalyzerLib where
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Arrow (first)
|
||||
import Control.Monad.Identity
|
||||
import Control.Monad.Reader
|
||||
import Control.Monad.State
|
||||
import Control.Monad.Writer
|
||||
import Data.Char
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import qualified Data.Map as Map
|
||||
|
||||
import Test.QuickCheck.All (forAllProperties)
|
||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||
|
||||
type Analysis = ReaderT Parameters (Writer [TokenComment]) ()
|
||||
|
||||
|
||||
data Parameters = Parameters {
|
||||
variableFlow :: [StackData],
|
||||
parentMap :: Map.Map Id Token,
|
||||
shellType :: Shell,
|
||||
shellTypeSpecified :: Bool
|
||||
}
|
||||
|
||||
data Scope = SubshellScope String | NoneScope deriving (Show, Eq)
|
||||
data StackData =
|
||||
StackScope Scope
|
||||
| StackScopeEnd
|
||||
-- (Base expression, specific position, var name, assigned values)
|
||||
| Assignment (Token, Token, String, DataType)
|
||||
| Reference (Token, Token, String)
|
||||
deriving (Show)
|
||||
|
||||
data DataType = DataString DataSource | DataArray DataSource
|
||||
deriving (Show)
|
||||
|
||||
data DataSource = SourceFrom [Token] | SourceExternal | SourceDeclaration | SourceInteger
|
||||
deriving (Show)
|
||||
|
||||
data VariableState = Dead Token String | Alive deriving (Show)
|
||||
|
||||
defaultSpec root = AnalysisSpec {
|
||||
asScript = root,
|
||||
asShellType = Nothing,
|
||||
asExecutionMode = Executed
|
||||
}
|
||||
|
||||
pScript s =
|
||||
let
|
||||
pSpec = ParseSpec {
|
||||
psFilename = "script",
|
||||
psScript = s
|
||||
}
|
||||
in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec
|
||||
|
||||
makeComment :: Severity -> Id -> Code -> String -> TokenComment
|
||||
makeComment severity id code note =
|
||||
TokenComment id $ Comment severity code note
|
||||
|
||||
addComment note = tell [note]
|
||||
|
||||
warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m ()
|
||||
warn id code str = addComment $ makeComment WarningC id code str
|
||||
err id code str = addComment $ makeComment ErrorC id code str
|
||||
info id code str = addComment $ makeComment InfoC id code str
|
||||
style id code str = addComment $ makeComment StyleC id code str
|
||||
|
||||
makeParameters spec =
|
||||
let params = Parameters {
|
||||
shellType = fromMaybe (determineShell root) $ asShellType spec,
|
||||
shellTypeSpecified = isJust $ asShellType spec,
|
||||
parentMap = getParentTree root,
|
||||
variableFlow =
|
||||
getVariableFlow (shellType params) (parentMap params) root
|
||||
} in params
|
||||
where root = asScript spec
|
||||
|
||||
prop_determineShell0 = determineShell (fromJust $ pScript "#!/bin/sh") == Sh
|
||||
prop_determineShell1 = determineShell (fromJust $ pScript "#!/usr/bin/env ksh") == Ksh
|
||||
prop_determineShell2 = determineShell (fromJust $ pScript "") == Bash
|
||||
prop_determineShell3 = determineShell (fromJust $ pScript "#!/bin/sh -e") == Sh
|
||||
prop_determineShell4 = determineShell (fromJust $ pScript
|
||||
"#!/bin/ksh\n#shellcheck shell=sh\nfoo") == Sh
|
||||
prop_determineShell5 = determineShell (fromJust $ pScript
|
||||
"#shellcheck shell=sh\nfoo") == Sh
|
||||
prop_determineShell6 = determineShell (fromJust $ pScript "#! /bin/sh") == Sh
|
||||
determineShell t = fromMaybe Bash $ do
|
||||
shellString <- foldl mplus Nothing $ getCandidates t
|
||||
shellForExecutable shellString
|
||||
where
|
||||
forAnnotation t =
|
||||
case t of
|
||||
(ShellOverride s) -> return s
|
||||
_ -> fail ""
|
||||
getCandidates :: Token -> [Maybe String]
|
||||
getCandidates t@(T_Script {}) = [Just $ fromShebang t]
|
||||
getCandidates (T_Annotation _ annotations s) =
|
||||
map forAnnotation annotations ++
|
||||
[Just $ fromShebang s]
|
||||
fromShebang (T_Script _ s t) = shellFor s
|
||||
|
||||
shellFor s | "/env " `isInfixOf` s = head (drop 1 (words s)++[""])
|
||||
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
|
||||
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
|
||||
|
||||
|
||||
--- Context seeking
|
||||
|
||||
getParentTree t =
|
||||
snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
|
||||
where
|
||||
pre t = modify (first ((:) t))
|
||||
post t = do
|
||||
(_:rest, map) <- get
|
||||
case rest of [] -> put (rest, map)
|
||||
(x:_) -> put (rest, Map.insert (getId t) x map)
|
||||
|
||||
getTokenMap t =
|
||||
execState (doAnalysis f t) Map.empty
|
||||
where
|
||||
f t = modify (Map.insert (getId t) t)
|
||||
|
||||
|
||||
-- Is this node self quoting for a regular element?
|
||||
isQuoteFree = isQuoteFreeNode False
|
||||
|
||||
-- Is this node striclty self quoting, for array expansions
|
||||
isStrictlyQuoteFree = isQuoteFreeNode True
|
||||
|
||||
|
||||
isQuoteFreeNode strict tree t =
|
||||
(isQuoteFreeElement t == Just True) ||
|
||||
head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False])
|
||||
where
|
||||
-- Is this node self-quoting in itself?
|
||||
isQuoteFreeElement t =
|
||||
case t of
|
||||
T_Assignment {} -> return True
|
||||
T_FdRedirect {} -> return True
|
||||
_ -> Nothing
|
||||
|
||||
-- Are any subnodes inherently self-quoting?
|
||||
isQuoteFreeContext t =
|
||||
case t of
|
||||
TC_Noary _ DoubleBracket _ -> return True
|
||||
TC_Unary _ DoubleBracket _ _ -> return True
|
||||
TC_Binary _ DoubleBracket _ _ _ -> return True
|
||||
TA_Sequence {} -> return True
|
||||
T_Arithmetic {} -> return True
|
||||
T_Assignment {} -> return True
|
||||
T_Redirecting {} -> return $
|
||||
if strict then False else
|
||||
-- Not true, just a hack to prevent warning about non-expansion refs
|
||||
any (isCommand t) ["local", "declare", "typeset", "export", "trap", "readonly"]
|
||||
T_DoubleQuoted _ _ -> return True
|
||||
T_DollarDoubleQuoted _ _ -> return True
|
||||
T_CaseExpression {} -> return True
|
||||
T_HereDoc {} -> return True
|
||||
T_DollarBraced {} -> return True
|
||||
-- When non-strict, pragmatically assume it's desirable to split here
|
||||
T_ForIn {} -> return (not strict)
|
||||
T_SelectIn {} -> return (not strict)
|
||||
_ -> Nothing
|
||||
|
||||
isParamTo tree cmd =
|
||||
go
|
||||
where
|
||||
go x = case Map.lookup (getId x) tree of
|
||||
Nothing -> False
|
||||
Just parent -> check parent
|
||||
check t =
|
||||
case t of
|
||||
T_SingleQuoted _ _ -> go t
|
||||
T_DoubleQuoted _ _ -> go t
|
||||
T_NormalWord _ _ -> go t
|
||||
T_SimpleCommand {} -> isCommand t cmd
|
||||
T_Redirecting {} -> isCommand t cmd
|
||||
_ -> False
|
||||
|
||||
getClosestCommand tree t =
|
||||
msum . map getCommand $ getPath tree t
|
||||
where
|
||||
getCommand t@(T_Redirecting {}) = return t
|
||||
getCommand _ = Nothing
|
||||
|
||||
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
||||
where
|
||||
go currentId (T_NormalWord id [word]:rest)
|
||||
| currentId == getId word = go id rest
|
||||
go currentId (T_DoubleQuoted id [word]:rest)
|
||||
| currentId == getId word = go id rest
|
||||
go currentId (T_SimpleCommand _ _ (word:_):_)
|
||||
| currentId == getId word = True
|
||||
go _ _ = False
|
||||
|
||||
-- A list of the element and all its parents
|
||||
getPath tree t = t :
|
||||
case Map.lookup (getId t) tree of
|
||||
Nothing -> []
|
||||
Just parent -> getPath tree parent
|
||||
|
||||
isParentOf tree parent child =
|
||||
elem (getId parent) . map getId $ getPath tree child
|
||||
|
||||
parents params = getPath (parentMap params)
|
||||
|
||||
pathTo t = do
|
||||
parents <- reader parentMap
|
||||
return $ getPath parents t
|
||||
|
||||
-- Check whether a word is entirely output from a single command
|
||||
tokenIsJustCommandOutput t = case t of
|
||||
T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds
|
||||
T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ cmds]] -> check cmds
|
||||
T_NormalWord id [T_Backticked _ cmds] -> check cmds
|
||||
T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ cmds]] -> check cmds
|
||||
_ -> False
|
||||
where
|
||||
check [x] = not $ isOnlyRedirection x
|
||||
check _ = False
|
||||
|
||||
-- TODO: Replace this with a proper Control Flow Graph
|
||||
getVariableFlow shell parents t =
|
||||
let (_, stack) = runState (doStackAnalysis startScope endScope t) []
|
||||
in reverse stack
|
||||
where
|
||||
startScope t =
|
||||
let scopeType = leadType shell parents t
|
||||
in do
|
||||
when (scopeType /= NoneScope) $ modify (StackScope scopeType:)
|
||||
when (assignFirst t) $ setWritten t
|
||||
|
||||
endScope t =
|
||||
let scopeType = leadType shell parents t
|
||||
in do
|
||||
setRead t
|
||||
unless (assignFirst t) $ setWritten t
|
||||
when (scopeType /= NoneScope) $ modify (StackScopeEnd:)
|
||||
|
||||
assignFirst (T_ForIn {}) = True
|
||||
assignFirst (T_SelectIn {}) = True
|
||||
assignFirst _ = False
|
||||
|
||||
setRead t =
|
||||
let read = getReferencedVariables parents t
|
||||
in mapM_ (\v -> modify (Reference v:)) read
|
||||
|
||||
setWritten t =
|
||||
let written = getModifiedVariables t
|
||||
in mapM_ (\v -> modify (Assignment v:)) written
|
||||
|
||||
|
||||
leadType shell parents t =
|
||||
case t of
|
||||
T_DollarExpansion _ _ -> SubshellScope "$(..) expansion"
|
||||
T_Backticked _ _ -> SubshellScope "`..` expansion"
|
||||
T_Backgrounded _ _ -> SubshellScope "backgrounding &"
|
||||
T_Subshell _ _ -> SubshellScope "(..) group"
|
||||
T_CoProcBody _ _ -> SubshellScope "coproc"
|
||||
T_Redirecting {} ->
|
||||
if fromMaybe False causesSubshell
|
||||
then SubshellScope "pipeline"
|
||||
else NoneScope
|
||||
_ -> NoneScope
|
||||
where
|
||||
parentPipeline = do
|
||||
parent <- Map.lookup (getId t) parents
|
||||
case parent of
|
||||
T_Pipeline {} -> return parent
|
||||
_ -> Nothing
|
||||
|
||||
causesSubshell = do
|
||||
(T_Pipeline _ _ list) <- parentPipeline
|
||||
if length list <= 1
|
||||
then return False
|
||||
else if lastCreatesSubshell
|
||||
then return True
|
||||
else return . not $ (getId . head $ reverse list) == getId t
|
||||
|
||||
lastCreatesSubshell =
|
||||
case shell of
|
||||
Bash -> True
|
||||
Dash -> True
|
||||
Sh -> True
|
||||
Ksh -> False
|
||||
|
||||
getModifiedVariables t =
|
||||
case t of
|
||||
T_SimpleCommand _ vars [] ->
|
||||
concatMap (\x -> case x of
|
||||
T_Assignment id _ name _ w ->
|
||||
[(x, x, name, dataTypeFrom DataString w)]
|
||||
_ -> []
|
||||
) vars
|
||||
c@(T_SimpleCommand {}) ->
|
||||
getModifiedVariableCommand c
|
||||
|
||||
TA_Unary _ "++|" var -> maybeToList $ do
|
||||
name <- getLiteralString var
|
||||
return (t, t, name, DataString $ SourceFrom [t])
|
||||
TA_Unary _ "|++" var -> maybeToList $ do
|
||||
name <- getLiteralString var
|
||||
return (t, t, name, DataString $ SourceFrom [t])
|
||||
TA_Assignment _ op lhs rhs -> maybeToList $ do
|
||||
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
||||
name <- getLiteralString lhs
|
||||
return (t, t, name, DataString $ SourceFrom [rhs])
|
||||
|
||||
T_DollarBraced _ l -> maybeToList $ do
|
||||
let string = bracedString t
|
||||
let modifier = getBracedModifier string
|
||||
guard $ ":=" `isPrefixOf` modifier
|
||||
return (t, t, getBracedReference string, DataString $ SourceFrom [l])
|
||||
|
||||
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo
|
||||
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
|
||||
|
||||
t@(T_CoProc _ name _) ->
|
||||
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
|
||||
|
||||
--Points to 'for' rather than variable
|
||||
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
|
||||
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
|
||||
_ -> []
|
||||
|
||||
isClosingFileOp op =
|
||||
case op of
|
||||
T_IoFile _ (T_GREATAND _) (T_NormalWord _ [T_Literal _ "-"]) -> True
|
||||
T_IoFile _ (T_LESSAND _) (T_NormalWord _ [T_Literal _ "-"]) -> True
|
||||
_ -> False
|
||||
|
||||
|
||||
-- Consider 'export/declare -x' a reference, since it makes the var available
|
||||
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
||||
case x of
|
||||
"export" -> if "f" `elem` flags
|
||||
then []
|
||||
else concatMap getReference rest
|
||||
"declare" -> if any (`elem` flags) ["x", "p"]
|
||||
then concatMap getReference rest
|
||||
else []
|
||||
"readonly" ->
|
||||
if any (`elem` flags) ["f", "p"]
|
||||
then []
|
||||
else concatMap getReference rest
|
||||
"trap" ->
|
||||
case rest of
|
||||
head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head
|
||||
_ -> []
|
||||
_ -> []
|
||||
where
|
||||
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
|
||||
getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
|
||||
getReference _ = []
|
||||
flags = map snd $ getAllFlags base
|
||||
|
||||
getReferencedVariableCommand _ = []
|
||||
|
||||
getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
||||
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
|
||||
case x of
|
||||
"read" ->
|
||||
let params = map getLiteral rest in
|
||||
catMaybes . takeWhile isJust . reverse $ params
|
||||
"getopts" ->
|
||||
case rest of
|
||||
opts:var:_ -> maybeToList $ getLiteral var
|
||||
_ -> []
|
||||
|
||||
"let" -> concatMap letParamToLiteral rest
|
||||
|
||||
"export" ->
|
||||
if "f" `elem` flags then [] else concatMap getModifierParamString rest
|
||||
|
||||
"declare" -> if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
|
||||
"typeset" -> declaredVars
|
||||
|
||||
"local" -> concatMap getModifierParamString rest
|
||||
"readonly" ->
|
||||
if any (`elem` flags) ["f", "p"]
|
||||
then []
|
||||
else concatMap getModifierParamString rest
|
||||
"set" -> maybeToList $ do
|
||||
params <- getSetParams rest
|
||||
return (base, base, "@", DataString $ SourceFrom params)
|
||||
|
||||
"printf" -> maybeToList $ getPrintfVariable rest
|
||||
|
||||
"mapfile" -> maybeToList $ getMapfileArray base rest
|
||||
"readarray" -> maybeToList $ getMapfileArray base rest
|
||||
|
||||
_ -> []
|
||||
where
|
||||
flags = map snd $ getAllFlags base
|
||||
stripEquals s = let rest = dropWhile (/= '=') s in
|
||||
if rest == "" then "" else tail rest
|
||||
stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) =
|
||||
T_NormalWord id1 (T_Literal id2 (stripEquals s):rs)
|
||||
stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) =
|
||||
T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
|
||||
stripEqualsFrom t = t
|
||||
|
||||
declaredVars = concatMap (getModifierParam defaultType) rest
|
||||
where
|
||||
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
|
||||
|
||||
getLiteral t = do
|
||||
s <- getLiteralString t
|
||||
when ("-" `isPrefixOf` s) $ fail "argument"
|
||||
return (base, t, s, DataString SourceExternal)
|
||||
|
||||
getModifierParamString = getModifierParam DataString
|
||||
|
||||
getModifierParam def t@(T_Assignment _ _ name _ value) =
|
||||
[(base, t, name, dataTypeFrom def value)]
|
||||
getModifierParam def t@(T_NormalWord {}) = maybeToList $ do
|
||||
name <- getLiteralString t
|
||||
guard $ isVariableName name
|
||||
return (base, t, name, def SourceDeclaration)
|
||||
getModifierParam _ _ = []
|
||||
|
||||
letParamToLiteral token =
|
||||
if var == ""
|
||||
then []
|
||||
else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])]
|
||||
where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token
|
||||
|
||||
getSetParams (t:_:rest) | getLiteralString t == Just "-o" = getSetParams rest
|
||||
getSetParams (t:rest) =
|
||||
let s = getLiteralString t in
|
||||
case s of
|
||||
Just "--" -> return rest
|
||||
Just ('-':_) -> getSetParams rest
|
||||
_ -> return (t:fromMaybe [] (getSetParams rest))
|
||||
getSetParams [] = Nothing
|
||||
|
||||
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
|
||||
where
|
||||
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, var, DataString $ SourceFrom list)
|
||||
f (_:rest) = f rest
|
||||
f [] = fail "not found"
|
||||
|
||||
-- mapfile has some curious syntax allowing flags plus 0..n variable names
|
||||
-- where only the first non-option one is used if any. Here we cheat and
|
||||
-- just get the last one, if it's a variable name.
|
||||
getMapfileArray base arguments = do
|
||||
lastArg <- listToMaybe (reverse arguments)
|
||||
name <- getLiteralString lastArg
|
||||
guard $ isVariableName name
|
||||
return (base, lastArg, name, DataArray SourceExternal)
|
||||
|
||||
getModifiedVariableCommand _ = []
|
||||
|
||||
getIndexReferences s = fromMaybe [] $ do
|
||||
match <- matchRegex re s
|
||||
index <- match !!! 0
|
||||
return $ matchAllStrings variableNameRegex index
|
||||
where
|
||||
re = mkRegex "(\\[.*\\])"
|
||||
|
||||
getOffsetReferences mods = fromMaybe [] $ do
|
||||
match <- matchRegex re mods
|
||||
offsets <- match !!! 0
|
||||
return $ matchAllStrings variableNameRegex offsets
|
||||
where
|
||||
re = mkRegex "^ *:(.*)"
|
||||
|
||||
getReferencedVariables parents t =
|
||||
case t of
|
||||
T_DollarBraced id l -> let str = bracedString t in
|
||||
(t, t, getBracedReference str) :
|
||||
map (\x -> (l, l, x)) (
|
||||
getIndexReferences str
|
||||
++ getOffsetReferences (getBracedModifier str))
|
||||
TA_Expansion id _ ->
|
||||
if isArithmeticAssignment t
|
||||
then []
|
||||
else getIfReference t t
|
||||
T_Assignment id mode str _ word ->
|
||||
[(t, t, str) | mode == Append] ++ specialReferences str t word
|
||||
|
||||
TC_Unary id _ "-v" token -> getIfReference t token
|
||||
TC_Unary id _ "-R" token -> getIfReference t token
|
||||
TC_Binary id DoubleBracket op lhs rhs ->
|
||||
if isDereferencing op
|
||||
then concatMap (getIfReference t) [lhs, rhs]
|
||||
else []
|
||||
|
||||
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
|
||||
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
|
||||
x -> getReferencedVariableCommand x
|
||||
where
|
||||
-- Try to reduce false positives for unused vars only referenced from evaluated vars
|
||||
specialReferences name base word =
|
||||
if name `elem` [
|
||||
"PS1", "PS2", "PS3", "PS4",
|
||||
"PROMPT_COMMAND"
|
||||
]
|
||||
then
|
||||
map (\x -> (base, base, x)) $
|
||||
getVariablesFromLiteralToken word
|
||||
else []
|
||||
|
||||
literalizer (TA_Index {}) = return "" -- x[0] becomes a reference of x
|
||||
literalizer _ = Nothing
|
||||
|
||||
getIfReference context token = maybeToList $ do
|
||||
str <- getLiteralStringExt literalizer token
|
||||
guard . not $ null str
|
||||
when (isDigit $ head str) $ fail "is a number"
|
||||
return (context, token, getBracedReference str)
|
||||
|
||||
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
|
||||
|
||||
isArithmeticAssignment t = case getPath parents t of
|
||||
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
|
||||
_ -> False
|
||||
|
||||
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
|
||||
|
||||
|
||||
--- Command specific checks
|
||||
|
||||
isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `isSuffixOf` cmd)
|
||||
isUnqualifiedCommand token str = isCommandMatch token (== str)
|
||||
|
||||
isCommandMatch token matcher = fromMaybe False $ do
|
||||
cmd <- getCommandName token
|
||||
return $ matcher cmd
|
||||
|
||||
isConfusedGlobRegex ('*':_) = True
|
||||
isConfusedGlobRegex [x,'*'] | x /= '\\' = True
|
||||
isConfusedGlobRegex _ = False
|
||||
|
||||
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
|
||||
isVariableChar x = isVariableStartChar x || isDigit x
|
||||
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
|
||||
|
||||
prop_isVariableName1 = isVariableName "_fo123"
|
||||
prop_isVariableName2 = not $ isVariableName "4"
|
||||
prop_isVariableName3 = not $ isVariableName "test: "
|
||||
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
|
||||
isVariableName _ = False
|
||||
|
||||
getVariablesFromLiteralToken token =
|
||||
getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token)
|
||||
|
||||
-- Try to get referenced variables from a literal string like "$foo"
|
||||
-- Ignores tons of cases like arithmetic evaluation and array indices.
|
||||
prop_getVariablesFromLiteral1 =
|
||||
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
|
||||
getVariablesFromLiteral string =
|
||||
map (!! 0) $ matchAllSubgroups variableRegex string
|
||||
where
|
||||
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
|
||||
|
||||
prop_getBracedReference1 = getBracedReference "foo" == "foo"
|
||||
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
|
||||
prop_getBracedReference3 = getBracedReference "#" == "#"
|
||||
prop_getBracedReference4 = getBracedReference "##" == "#"
|
||||
prop_getBracedReference5 = getBracedReference "#!" == "!"
|
||||
prop_getBracedReference6 = getBracedReference "!#" == "#"
|
||||
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
|
||||
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
|
||||
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
|
||||
prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
|
||||
prop_getBracedReference11= getBracedReference "!os*" == ""
|
||||
prop_getBracedReference12= getBracedReference "!os?bar**" == ""
|
||||
prop_getBracedReference13= getBracedReference "foo[bar]" == "foo"
|
||||
getBracedReference s = fromMaybe s $
|
||||
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
|
||||
where
|
||||
noPrefix = dropPrefix s
|
||||
dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest
|
||||
dropPrefix "" = ""
|
||||
takeName s = do
|
||||
let name = takeWhile isVariableChar s
|
||||
guard . not $ null name
|
||||
return name
|
||||
getSpecial (c:_) =
|
||||
if c `elem` "*@#?-$!" then return [c] else fail "not special"
|
||||
getSpecial _ = fail "empty"
|
||||
|
||||
nameExpansion ('!':rest) = do -- e.g. ${!foo*bar*}
|
||||
let suffix = dropWhile isVariableChar rest
|
||||
guard $ suffix /= rest -- e.g. ${!@}
|
||||
first <- suffix !!! 0
|
||||
guard $ first `elem` "*?"
|
||||
return ""
|
||||
nameExpansion _ = Nothing
|
||||
|
||||
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
|
||||
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
|
||||
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
|
||||
getBracedModifier s = fromMaybe "" . listToMaybe $ do
|
||||
let var = getBracedReference s
|
||||
a <- dropModifier s
|
||||
dropPrefix var a
|
||||
where
|
||||
dropPrefix [] t = return t
|
||||
dropPrefix (a:b) (c:d) | a == c = dropPrefix b d
|
||||
dropPrefix _ _ = []
|
||||
|
||||
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
|
||||
dropModifier x = [x]
|
||||
|
||||
-- Useful generic functions
|
||||
potentially :: Monad m => Maybe (m ()) -> m ()
|
||||
potentially = fromMaybe (return ())
|
||||
|
||||
headOrDefault _ (a:_) = a
|
||||
headOrDefault def _ = def
|
||||
|
||||
(!!!) list i =
|
||||
case drop i list of
|
||||
[] -> Nothing
|
||||
(r:_) -> Just r
|
||||
|
||||
|
||||
|
||||
filterByAnnotation token =
|
||||
filter (not . shouldIgnore)
|
||||
where
|
||||
idFor (TokenComment id _) = id
|
||||
shouldIgnore note =
|
||||
any (shouldIgnoreFor (getCode note)) $
|
||||
getPath parents (T_Bang $ idFor note)
|
||||
shouldIgnoreFor num (T_Annotation _ anns _) =
|
||||
any hasNum anns
|
||||
where
|
||||
hasNum (DisableComment ts) = num == ts
|
||||
hasNum _ = False
|
||||
shouldIgnoreFor _ (T_Include {}) = True -- Ignore included files
|
||||
shouldIgnoreFor _ _ = False
|
||||
parents = getParentTree token
|
||||
getCode (TokenComment _ (Comment _ c _)) = c
|
||||
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -1,162 +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/>.
|
||||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Analyzer
|
||||
|
||||
import Data.Either
|
||||
import Data.Functor
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import Data.Ord
|
||||
import Control.Monad.Identity
|
||||
import qualified Data.Map as Map
|
||||
import qualified System.IO
|
||||
import Prelude hiding (readFile)
|
||||
import Control.Monad
|
||||
|
||||
import Test.QuickCheck.All
|
||||
|
||||
tokenToPosition map (TokenComment id c) = fromMaybe fail $ do
|
||||
position <- Map.lookup id map
|
||||
return $ PositionedComment position position c
|
||||
where
|
||||
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
|
||||
|
||||
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
|
||||
checkScript sys spec = do
|
||||
results <- checkScript (csScript spec)
|
||||
return CheckResult {
|
||||
crFilename = csFilename spec,
|
||||
crComments = results
|
||||
}
|
||||
where
|
||||
checkScript contents = do
|
||||
result <- parseScript sys ParseSpec {
|
||||
psFilename = csFilename spec,
|
||||
psScript = contents
|
||||
}
|
||||
let parseMessages = prComments result
|
||||
let analysisMessages =
|
||||
fromMaybe [] $
|
||||
(arComments . analyzeScript . analysisSpec)
|
||||
<$> prRoot result
|
||||
let translator = tokenToPosition (prTokenPositions result)
|
||||
return . nub . sortMessages . filter shouldInclude $
|
||||
(parseMessages ++ map translator analysisMessages)
|
||||
|
||||
shouldInclude (PositionedComment _ _ (Comment _ code _)) =
|
||||
code `notElem` csExcludedWarnings spec
|
||||
|
||||
sortMessages = sortBy (comparing order)
|
||||
order (PositionedComment pos _ (Comment severity code message)) =
|
||||
(posFile pos, posLine pos, posColumn pos, severity, code, message)
|
||||
getPosition (PositionedComment pos _ _) = pos
|
||||
|
||||
analysisSpec root =
|
||||
AnalysisSpec {
|
||||
asScript = root,
|
||||
asShellType = csShellTypeOverride spec,
|
||||
asExecutionMode = Executed
|
||||
}
|
||||
|
||||
getErrors sys spec =
|
||||
sort . map getCode . crComments $
|
||||
runIdentity (checkScript sys spec)
|
||||
where
|
||||
getCode (PositionedComment _ _ (Comment _ code _)) = code
|
||||
|
||||
check = checkWithIncludes []
|
||||
|
||||
checkWithIncludes includes src =
|
||||
getErrors
|
||||
(mockedSystemInterface includes)
|
||||
emptyCheckSpec {
|
||||
csScript = src,
|
||||
csExcludedWarnings = [2148]
|
||||
}
|
||||
|
||||
prop_findsParseIssue = check "echo \"$12\"" == [1037]
|
||||
|
||||
prop_commentDisablesParseIssue1 =
|
||||
null $ check "#shellcheck disable=SC1037\necho \"$12\""
|
||||
prop_commentDisablesParseIssue2 =
|
||||
null $ check "#shellcheck disable=SC1037\n#lol\necho \"$12\""
|
||||
|
||||
prop_findsAnalysisIssue =
|
||||
check "echo $1" == [2086]
|
||||
prop_commentDisablesAnalysisIssue1 =
|
||||
null $ check "#shellcheck disable=SC2086\necho $1"
|
||||
prop_commentDisablesAnalysisIssue2 =
|
||||
null $ check "#shellcheck disable=SC2086\n#lol\necho $1"
|
||||
|
||||
prop_optionDisablesIssue1 =
|
||||
null $ getErrors
|
||||
(mockedSystemInterface [])
|
||||
emptyCheckSpec {
|
||||
csScript = "echo $1",
|
||||
csExcludedWarnings = [2148, 2086]
|
||||
}
|
||||
|
||||
prop_optionDisablesIssue2 =
|
||||
null $ getErrors
|
||||
(mockedSystemInterface [])
|
||||
emptyCheckSpec {
|
||||
csScript = "echo \"$10\"",
|
||||
csExcludedWarnings = [2148, 1037]
|
||||
}
|
||||
|
||||
prop_canParseDevNull =
|
||||
[] == check "source /dev/null"
|
||||
|
||||
prop_failsWhenNotSourcing =
|
||||
[1091, 2154] == check "source lol; echo \"$bar\""
|
||||
|
||||
prop_worksWhenSourcing =
|
||||
null $ checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\""
|
||||
|
||||
prop_worksWhenDotting =
|
||||
null $ checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\""
|
||||
|
||||
prop_noInfiniteSourcing =
|
||||
[] == checkWithIncludes [("lib", "source lib")] "source lib"
|
||||
|
||||
prop_canSourceBadSyntax =
|
||||
[1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
|
||||
|
||||
prop_cantSourceDynamic =
|
||||
[1090] == checkWithIncludes [("lib", "")] ". \"$1\""
|
||||
|
||||
prop_cantSourceDynamic2 =
|
||||
[1090] == checkWithIncludes [("lib", "")] "source ~/foo"
|
||||
|
||||
prop_canSourceDynamicWhenRedirected =
|
||||
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
|
||||
|
||||
prop_sourceDirectiveDoesntFollowFile =
|
||||
null $ checkWithIncludes
|
||||
[("foo", "source bar"), ("bar", "baz=3")]
|
||||
"#shellcheck source=foo\n. \"$1\"; echo \"$baz\""
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
@@ -1,623 +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/>.
|
||||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
|
||||
-- This module contains checks that examine specific commands by name.
|
||||
module ShellCheck.Checks.Commands (runChecks
|
||||
, ShellCheck.Checks.Commands.runTests
|
||||
) where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.AnalyzerLib
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.Reader
|
||||
import Control.Monad.Writer
|
||||
import Data.Char
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import qualified Data.Map as Map
|
||||
import Test.QuickCheck.All (forAllProperties)
|
||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||
|
||||
data CommandName = Exactly String | Basename String
|
||||
deriving (Eq, Ord)
|
||||
|
||||
data CommandCheck =
|
||||
CommandCheck CommandName (Token -> Analysis)
|
||||
|
||||
nullCheck :: Token -> Analysis
|
||||
nullCheck _ = return ()
|
||||
|
||||
|
||||
verify :: CommandCheck -> String -> Bool
|
||||
verify f s = producesComments f s == Just True
|
||||
verifyNot f s = producesComments f s == Just False
|
||||
|
||||
producesComments :: CommandCheck -> String -> Maybe Bool
|
||||
producesComments f s = do
|
||||
root <- pScript s
|
||||
return . not . null $ runList (defaultSpec root) [f]
|
||||
|
||||
composeChecks f g t = do
|
||||
f t
|
||||
g t
|
||||
|
||||
arguments (T_SimpleCommand _ _ (cmd:args)) = args
|
||||
|
||||
commandChecks :: [CommandCheck]
|
||||
commandChecks = [
|
||||
checkTr
|
||||
,checkFindNameGlob
|
||||
,checkNeedlessExpr
|
||||
,checkGrepRe
|
||||
,checkTrapQuotes
|
||||
,checkReturn
|
||||
,checkFindExecWithSingleArgument
|
||||
,checkUnusedEchoEscapes
|
||||
,checkInjectableFindSh
|
||||
,checkFindActionPrecedence
|
||||
,checkMkdirDashPM
|
||||
,checkNonportableSignals
|
||||
,checkInteractiveSu
|
||||
,checkSshCommandString
|
||||
,checkPrintfVar
|
||||
,checkUuoeCmd
|
||||
,checkSetAssignment
|
||||
,checkExportedExpansions
|
||||
,checkAliasesUsesArgs
|
||||
,checkAliasesExpandEarly
|
||||
,checkUnsetGlobs
|
||||
,checkFindWithoutPath
|
||||
]
|
||||
|
||||
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
||||
buildCommandMap = foldl' addCheck Map.empty
|
||||
where
|
||||
addCheck map (CommandCheck name function) =
|
||||
Map.insertWith' composeChecks name function map
|
||||
|
||||
|
||||
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
|
||||
checkCommand map t@(T_SimpleCommand id _ (cmd:rest)) = fromMaybe (return ()) $ do
|
||||
name <- getLiteralString cmd
|
||||
return $
|
||||
if '/' `elem` name
|
||||
then
|
||||
Map.findWithDefault nullCheck (Basename $ basename name) map t
|
||||
else do
|
||||
Map.findWithDefault nullCheck (Exactly name) map t
|
||||
Map.findWithDefault nullCheck (Basename name) map t
|
||||
|
||||
where
|
||||
basename = reverse . takeWhile (/= '/') . reverse
|
||||
checkCommand _ _ = return ()
|
||||
|
||||
runList spec list = notes
|
||||
where
|
||||
root = asScript spec
|
||||
params = makeParameters spec
|
||||
notes = execWriter $ runReaderT (doAnalysis (checkCommand map) root) params
|
||||
map = buildCommandMap list
|
||||
|
||||
runChecks spec = runList spec commandChecks
|
||||
|
||||
|
||||
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
|
||||
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
|
||||
prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'"
|
||||
prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'"
|
||||
prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'"
|
||||
prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'"
|
||||
prop_checkTr4 = verifyNot checkTr "ls [a-z]"
|
||||
prop_checkTr5 = verify checkTr "tr foo bar"
|
||||
prop_checkTr6 = verify checkTr "tr 'hello' 'world'"
|
||||
prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
|
||||
prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
|
||||
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
|
||||
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
|
||||
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
|
||||
where
|
||||
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
|
||||
warn (getId w) 2060 "Quote parameters to tr to prevent glob expansion."
|
||||
f word =
|
||||
case getLiteralString word of
|
||||
Just "a-z" -> info (getId word) 2018 "Use '[:lower:]' to support accents and foreign alphabets."
|
||||
Just "A-Z" -> info (getId word) 2019 "Use '[:upper:]' to support accents and foreign alphabets."
|
||||
Just s -> do -- Eliminate false positives by only looking for dupes in SET2?
|
||||
when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $
|
||||
info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)."
|
||||
unless ("[:" `isPrefixOf` s) $
|
||||
when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $
|
||||
info (getId word) 2021 "Don't use [] around classes in tr, it replaces literal square brackets."
|
||||
Nothing -> return ()
|
||||
|
||||
duplicated s =
|
||||
let relevant = filter isAlpha s
|
||||
in relevant /= nub relevant
|
||||
|
||||
prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php"
|
||||
prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)"
|
||||
prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"
|
||||
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
|
||||
acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
|
||||
acceptsGlob _ = False
|
||||
f [] = return ()
|
||||
f [x] = return ()
|
||||
f (a:b:r) = do
|
||||
when (acceptsGlob (getLiteralString a) && isGlob b) $ do
|
||||
let (Just s) = getLiteralString a
|
||||
warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."
|
||||
f (b:r)
|
||||
|
||||
|
||||
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
|
||||
prop_checkNeedlessExpr2 = verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``"
|
||||
prop_checkNeedlessExpr3 = verifyNot checkNeedlessExpr "foo=$(expr foo : regex)"
|
||||
prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)"
|
||||
checkNeedlessExpr = CommandCheck (Basename "expr") f where
|
||||
f t =
|
||||
when (all (`notElem` exceptions) (words $ arguments t)) $
|
||||
style (getId t) 2003
|
||||
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
|
||||
-- These operators are hard to replicate in POSIX
|
||||
exceptions = [ ":", "<", ">", "<=", ">=" ]
|
||||
words = mapMaybe getLiteralString
|
||||
|
||||
|
||||
prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3"
|
||||
prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3"
|
||||
prop_checkGrepRe3 = verify checkGrepRe "grep --regex=*.mp3 file"
|
||||
prop_checkGrepRe4 = verifyNot checkGrepRe "grep foo *.mp3"
|
||||
prop_checkGrepRe5 = verifyNot checkGrepRe "grep-v --regex=moo *"
|
||||
prop_checkGrepRe6 = verifyNot checkGrepRe "grep foo \\*.mp3"
|
||||
prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file"
|
||||
prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg"
|
||||
prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"
|
||||
prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file"
|
||||
prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo"
|
||||
|
||||
checkGrepRe = CommandCheck (Basename "grep") (f . arguments) where
|
||||
-- --regex=*(extglob) doesn't work. Fixme?
|
||||
skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s
|
||||
skippable _ = False
|
||||
f [] = return ()
|
||||
f (x:r) | skippable (getLiteralStringExt (const $ return "_") x) = f r
|
||||
f (re:_) = do
|
||||
when (isGlob re) $
|
||||
warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it."
|
||||
let string = concat $ oversimplify re
|
||||
if isConfusedGlobRegex string then
|
||||
warn (getId re) 2063 "Grep uses regex, but this looks like a glob."
|
||||
else potentially $ do
|
||||
char <- getSuspiciousRegexWildcard string
|
||||
return $ info (getId re) 2022 $
|
||||
"Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'."
|
||||
|
||||
wordStartingWith c =
|
||||
head . filter ([c] `isPrefixOf`) $ candidates
|
||||
where
|
||||
candidates =
|
||||
sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords ++ [c:"test"]
|
||||
|
||||
getSuspiciousRegexWildcard str =
|
||||
if not $ str `matches` contra
|
||||
then do
|
||||
match <- matchRegex suspicious str
|
||||
str <- match !!! 0
|
||||
str !!! 0
|
||||
else
|
||||
fail "looks good"
|
||||
where
|
||||
suspicious = mkRegex "([A-Za-z1-9])\\*"
|
||||
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
|
||||
|
||||
|
||||
prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
|
||||
prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT"
|
||||
prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT"
|
||||
prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
|
||||
checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
|
||||
f (x:_) = checkTrap x
|
||||
f _ = return ()
|
||||
checkTrap (T_NormalWord _ [T_DoubleQuoted _ rs]) = mapM_ checkExpansions rs
|
||||
checkTrap _ = return ()
|
||||
warning id = warn id 2064 "Use single quotes, otherwise this expands now rather than when signalled."
|
||||
checkExpansions (T_DollarExpansion id _) = warning id
|
||||
checkExpansions (T_Backticked id _) = warning id
|
||||
checkExpansions (T_DollarBraced id _) = warning id
|
||||
checkExpansions (T_DollarArithmetic id _) = warning id
|
||||
checkExpansions _ = return ()
|
||||
|
||||
|
||||
prop_checkReturn1 = verifyNot checkReturn "return"
|
||||
prop_checkReturn2 = verifyNot checkReturn "return 1"
|
||||
prop_checkReturn3 = verifyNot checkReturn "return $var"
|
||||
prop_checkReturn4 = verifyNot checkReturn "return $((a|b))"
|
||||
prop_checkReturn5 = verify checkReturn "return -1"
|
||||
prop_checkReturn6 = verify checkReturn "return 1000"
|
||||
prop_checkReturn7 = verify checkReturn "return 'hello world'"
|
||||
checkReturn = CommandCheck (Exactly "return") (f . arguments)
|
||||
where
|
||||
f (first:second:_) =
|
||||
err (getId second) 2151
|
||||
"Only one integer 0-255 can be returned. Use stdout for other data."
|
||||
f [value] =
|
||||
when (isInvalid $ literal value) $
|
||||
err (getId value) 2152
|
||||
"Can only return 0-255. Other data should be written to stdout."
|
||||
f _ = return ()
|
||||
|
||||
isInvalid s = s == "" || any (not . isDigit) s || length s > 5
|
||||
|| let value = (read s :: Integer) in value > 255
|
||||
|
||||
literal token = fromJust $ getLiteralStringExt lit token
|
||||
lit (T_DollarBraced {}) = return "0"
|
||||
lit (T_DollarArithmetic {}) = return "0"
|
||||
lit (T_DollarExpansion {}) = return "0"
|
||||
lit (T_Backticked {}) = return "0"
|
||||
lit _ = return "WTF"
|
||||
|
||||
|
||||
prop_checkFindExecWithSingleArgument1 = verify checkFindExecWithSingleArgument "find . -exec 'cat {} | wc -l' \\;"
|
||||
prop_checkFindExecWithSingleArgument2 = verify checkFindExecWithSingleArgument "find . -execdir 'cat {} | wc -l' +"
|
||||
prop_checkFindExecWithSingleArgument3 = verifyNot checkFindExecWithSingleArgument "find . -exec wc -l {} \\;"
|
||||
checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments)
|
||||
where
|
||||
f = void . sequence . mapMaybe check . tails
|
||||
check (exec:arg:term:_) = do
|
||||
execS <- getLiteralString exec
|
||||
termS <- getLiteralString term
|
||||
cmdS <- getLiteralStringExt (const $ return " ") arg
|
||||
|
||||
guard $ execS `elem` ["-exec", "-execdir"] && termS `elem` [";", "+"]
|
||||
guard $ cmdS `matches` commandRegex
|
||||
return $ warn (getId exec) 2150 "-exec does not invoke a shell. Rewrite or use -exec sh -c .. ."
|
||||
check _ = Nothing
|
||||
commandRegex = mkRegex "[ |;]"
|
||||
|
||||
|
||||
prop_checkUnusedEchoEscapes1 = verify checkUnusedEchoEscapes "echo 'foo\\nbar\\n'"
|
||||
prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'"
|
||||
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
|
||||
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
|
||||
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
|
||||
checkUnusedEchoEscapes = CommandCheck (Basename "echo") (f . arguments)
|
||||
where
|
||||
isDashE = mkRegex "^-.*e"
|
||||
hasEscapes = mkRegex "\\\\[rnt]"
|
||||
f args | concat (concatMap oversimplify allButLast) `matches` isDashE =
|
||||
return ()
|
||||
where allButLast = reverse . drop 1 . reverse $ args
|
||||
f args = mapM_ checkEscapes args
|
||||
|
||||
checkEscapes (T_NormalWord _ args) =
|
||||
mapM_ checkEscapes args
|
||||
checkEscapes (T_DoubleQuoted id args) =
|
||||
mapM_ checkEscapes args
|
||||
checkEscapes (T_Literal id str) = examine id str
|
||||
checkEscapes (T_SingleQuoted id str) = examine id str
|
||||
checkEscapes _ = return ()
|
||||
|
||||
examine id str =
|
||||
when (str `matches` hasEscapes) $
|
||||
info id 2028 "echo won't expand escape sequences. Consider printf."
|
||||
|
||||
|
||||
prop_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
|
||||
prop_checkInjectableFindSh2 = verify checkInjectableFindSh "find . -execdir bash -c 'rm \"{}\"' ';'"
|
||||
prop_checkInjectableFindSh3 = verifyNot checkInjectableFindSh "find . -exec sh -c 'rm \"$@\"' _ {} \\;"
|
||||
checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments)
|
||||
where
|
||||
check args = do
|
||||
let idStrings = map (\x -> (getId x, onlyLiteralString x)) args
|
||||
match pattern idStrings
|
||||
|
||||
match _ [] = return ()
|
||||
match [] (next:_) = action next
|
||||
match (p:tests) ((id, arg):args) = do
|
||||
when (p arg) $ match tests args
|
||||
match (p:tests) args
|
||||
|
||||
pattern = [
|
||||
(`elem` ["-exec", "-execdir"]),
|
||||
(`elem` ["sh", "bash", "ksh"]),
|
||||
(== "-c")
|
||||
]
|
||||
action (id, arg) =
|
||||
when ("{}" `isInfixOf` arg) $
|
||||
warn id 2156 "Injecting filenames is fragile and insecure. Use parameters."
|
||||
|
||||
|
||||
prop_checkFindActionPrecedence1 = verify checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au' -exec rm {} +"
|
||||
prop_checkFindActionPrecedence2 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o \\( -name '*.au' -exec rm {} + \\)"
|
||||
prop_checkFindActionPrecedence3 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au'"
|
||||
checkFindActionPrecedence = CommandCheck (Basename "find") (f . arguments)
|
||||
where
|
||||
pattern = [isMatch, const True, isParam ["-o", "-or"], isMatch, const True, isAction]
|
||||
f list | length list < length pattern = return ()
|
||||
f list@(_:rest) =
|
||||
if and (zipWith ($) pattern list)
|
||||
then warnFor (list !! (length pattern - 1))
|
||||
else f rest
|
||||
isMatch = isParam [ "-name", "-regex", "-iname", "-iregex", "-wholename", "-iwholename" ]
|
||||
isAction = isParam [ "-exec", "-execdir", "-delete", "-print", "-print0" ]
|
||||
isParam strs t = fromMaybe False $ do
|
||||
param <- getLiteralString t
|
||||
return $ param `elem` strs
|
||||
warnFor t = warn (getId t) 2146 "This action ignores everything before the -o. Use \\( \\) to group."
|
||||
|
||||
|
||||
prop_checkMkdirDashPM0 = verify checkMkdirDashPM "mkdir -p -m 0755 a/b"
|
||||
prop_checkMkdirDashPM1 = verify checkMkdirDashPM "mkdir -pm 0755 $dir"
|
||||
prop_checkMkdirDashPM2 = verify checkMkdirDashPM "mkdir -vpm 0755 a/b"
|
||||
prop_checkMkdirDashPM3 = verify checkMkdirDashPM "mkdir -pm 0755 -v a/b"
|
||||
prop_checkMkdirDashPM4 = verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b"
|
||||
prop_checkMkdirDashPM5 = verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b"
|
||||
prop_checkMkdirDashPM6 = verify checkMkdirDashPM "mkdir -p --mode=0755 a/b"
|
||||
prop_checkMkdirDashPM7 = verify checkMkdirDashPM "mkdir --parents -m 0755 a/b"
|
||||
prop_checkMkdirDashPM8 = verifyNot checkMkdirDashPM "mkdir -p a/b"
|
||||
prop_checkMkdirDashPM9 = verifyNot checkMkdirDashPM "mkdir -m 0755 a/b"
|
||||
prop_checkMkdirDashPM10 = verifyNot checkMkdirDashPM "mkdir a/b"
|
||||
prop_checkMkdirDashPM11 = verifyNot checkMkdirDashPM "mkdir --parents a/b"
|
||||
prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b"
|
||||
prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b"
|
||||
prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"
|
||||
checkMkdirDashPM = CommandCheck (Basename "mkdir") check
|
||||
where
|
||||
check t = potentially $ do
|
||||
let flags = getAllFlags t
|
||||
dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
|
||||
dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
|
||||
guard $ any couldHaveSubdirs (drop 1 $ arguments t) -- mkdir -pm 0700 dir is fine, but dir/subdir is not.
|
||||
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
|
||||
couldHaveSubdirs t = fromMaybe True $ do
|
||||
name <- getLiteralString t
|
||||
return $ '/' `elem` name
|
||||
|
||||
|
||||
prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8"
|
||||
prop_checkNonportableSignals2 = verifyNot checkNonportableSignals "trap f 0"
|
||||
prop_checkNonportableSignals3 = verifyNot checkNonportableSignals "trap f 14"
|
||||
prop_checkNonportableSignals4 = verify checkNonportableSignals "trap f SIGKILL"
|
||||
prop_checkNonportableSignals5 = verify checkNonportableSignals "trap f 9"
|
||||
prop_checkNonportableSignals6 = verify checkNonportableSignals "trap f stop"
|
||||
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
|
||||
where
|
||||
f = mapM_ check
|
||||
check param = potentially $ do
|
||||
str <- getLiteralString param
|
||||
let id = getId param
|
||||
return $ sequence_ $ mapMaybe (\f -> f id str) [
|
||||
checkNumeric,
|
||||
checkUntrappable
|
||||
]
|
||||
|
||||
checkNumeric id str = do
|
||||
guard $ not (null str)
|
||||
guard $ all isDigit str
|
||||
guard $ str /= "0" -- POSIX exit trap
|
||||
guard $ str `notElem` ["1", "2", "3", "6", "9", "14", "15" ] -- XSI
|
||||
return $ warn id 2172
|
||||
"Trapping signals by number is not well defined. Prefer signal names."
|
||||
|
||||
checkUntrappable id str = do
|
||||
guard $ map toLower str `elem` ["kill", "9", "sigkill", "stop", "sigstop"]
|
||||
return $ err id 2173
|
||||
"SIGKILL/SIGSTOP can not be trapped."
|
||||
|
||||
|
||||
prop_checkInteractiveSu1 = verify checkInteractiveSu "su; rm file; su $USER"
|
||||
prop_checkInteractiveSu2 = verify checkInteractiveSu "su foo; something; exit"
|
||||
prop_checkInteractiveSu3 = verifyNot checkInteractiveSu "echo rm | su foo"
|
||||
prop_checkInteractiveSu4 = verifyNot checkInteractiveSu "su root < script"
|
||||
checkInteractiveSu = CommandCheck (Basename "su") f
|
||||
where
|
||||
f cmd = when (length (arguments cmd) <= 1) $ do
|
||||
path <- pathTo cmd
|
||||
when (all undirected path) $
|
||||
info (getId cmd) 2117
|
||||
"To run commands as another user, use su -c or sudo."
|
||||
|
||||
undirected (T_Pipeline _ _ l) = length l <= 1
|
||||
-- This should really just be modifications to stdin, but meh
|
||||
undirected (T_Redirecting _ list _) = null list
|
||||
undirected _ = True
|
||||
|
||||
|
||||
-- This is hard to get right without properly parsing ssh args
|
||||
prop_checkSshCmdStr1 = verify checkSshCommandString "ssh host \"echo $PS1\""
|
||||
prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\""
|
||||
prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""
|
||||
checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
|
||||
where
|
||||
nonOptions =
|
||||
filter (\x -> not $ "-" `isPrefixOf` concat (oversimplify x))
|
||||
f args =
|
||||
case nonOptions args of
|
||||
(hostport:r@(_:_)) -> checkArg $ last r
|
||||
_ -> return ()
|
||||
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
|
||||
case filter (not . isConstant) parts of
|
||||
[] -> return ()
|
||||
(x:_) -> info (getId x) 2029
|
||||
"Note that, unescaped, this expands on the client side."
|
||||
checkArg _ = return ()
|
||||
|
||||
|
||||
prop_checkPrintfVar1 = verify checkPrintfVar "printf \"Lol: $s\""
|
||||
prop_checkPrintfVar2 = verifyNot checkPrintfVar "printf 'Lol: $s'"
|
||||
prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)"
|
||||
prop_checkPrintfVar4 = verifyNot checkPrintfVar "printf \"%${count}s\" var"
|
||||
prop_checkPrintfVar5 = verify checkPrintfVar "printf '%s %s %s' foo bar"
|
||||
prop_checkPrintfVar6 = verify checkPrintfVar "printf foo bar baz"
|
||||
prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz"
|
||||
prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\""
|
||||
prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
|
||||
prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
|
||||
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
|
||||
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
|
||||
f (format:params) = check format params
|
||||
f _ = return ()
|
||||
|
||||
countFormats string =
|
||||
case string of
|
||||
'%':'%':rest -> countFormats rest
|
||||
'%':rest -> 1 + countFormats rest
|
||||
_:rest -> countFormats rest
|
||||
[] -> 0
|
||||
|
||||
check format more = do
|
||||
fromMaybe (return ()) $ do
|
||||
string <- getLiteralString format
|
||||
let vars = countFormats string
|
||||
|
||||
return $ do
|
||||
when (vars == 0 && more /= []) $
|
||||
err (getId format) 2182
|
||||
"This printf format string has no variables. Other arguments are ignored."
|
||||
|
||||
when (vars > 0
|
||||
&& length more < vars
|
||||
&& all (not . mayBecomeMultipleArgs) more) $
|
||||
warn (getId format) 2183 $
|
||||
"This format string has " ++ show vars ++ " variables, but is passed " ++ show (length more) ++ " arguments."
|
||||
|
||||
|
||||
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
|
||||
info (getId format) 2059
|
||||
"Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"."
|
||||
|
||||
|
||||
|
||||
|
||||
prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)"
|
||||
prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`"
|
||||
prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\""
|
||||
prop_checkUuoeCmd4 = verify checkUuoeCmd "echo \"`date`\""
|
||||
prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\""
|
||||
prop_checkUuoeCmd6 = verifyNot checkUuoeCmd "echo \"$(<file)\""
|
||||
checkUuoeCmd = CommandCheck (Exactly "echo") (f . arguments) where
|
||||
msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'."
|
||||
f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token)
|
||||
f _ = return ()
|
||||
|
||||
|
||||
prop_checkSetAssignment1 = verify checkSetAssignment "set foo 42"
|
||||
prop_checkSetAssignment2 = verify checkSetAssignment "set foo = 42"
|
||||
prop_checkSetAssignment3 = verify checkSetAssignment "set foo=42"
|
||||
prop_checkSetAssignment4 = verifyNot checkSetAssignment "set -- if=/dev/null"
|
||||
prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'"
|
||||
prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"
|
||||
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
|
||||
where
|
||||
f (var:value:rest) =
|
||||
let str = literal var in
|
||||
when (isVariableName str || isAssignment str) $
|
||||
msg (getId var)
|
||||
f (var:_) =
|
||||
when (isAssignment $ literal var) $
|
||||
msg (getId var)
|
||||
f _ = return ()
|
||||
|
||||
msg id = warn id 2121 "To assign a variable, use just 'var=value', no 'set ..'."
|
||||
|
||||
isAssignment str = '=' `elem` str
|
||||
literal (T_NormalWord _ l) = concatMap literal l
|
||||
literal (T_Literal _ str) = str
|
||||
literal _ = "*"
|
||||
|
||||
|
||||
prop_checkExportedExpansions1 = verify checkExportedExpansions "export $foo"
|
||||
prop_checkExportedExpansions2 = verify checkExportedExpansions "export \"$foo\""
|
||||
prop_checkExportedExpansions3 = verifyNot checkExportedExpansions "export foo"
|
||||
checkExportedExpansions = CommandCheck (Exactly "export") (check . arguments)
|
||||
where
|
||||
check = mapM_ checkForVariables
|
||||
checkForVariables f =
|
||||
case getWordParts f of
|
||||
[t@(T_DollarBraced {})] ->
|
||||
warn (getId t) 2163 "Exporting an expansion rather than a variable."
|
||||
_ -> return ()
|
||||
|
||||
|
||||
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
|
||||
prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'"
|
||||
prop_checkAliasesUsesArgs3 = verify checkAliasesUsesArgs "alias a=\"echo \\${@}\""
|
||||
checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments)
|
||||
where
|
||||
re = mkRegex "\\$\\{?[0-9*@]"
|
||||
f = mapM_ checkArg
|
||||
checkArg arg =
|
||||
let string = fromJust $ getLiteralStringExt (const $ return "_") arg in
|
||||
when ('=' `elem` string && string `matches` re) $
|
||||
err (getId arg) 2142
|
||||
"Aliases can't use positional parameters. Use a function."
|
||||
|
||||
|
||||
prop_checkAliasesExpandEarly1 = verify checkAliasesExpandEarly "alias foo=\"echo $PWD\""
|
||||
prop_checkAliasesExpandEarly2 = verifyNot checkAliasesExpandEarly "alias -p"
|
||||
prop_checkAliasesExpandEarly3 = verifyNot checkAliasesExpandEarly "alias foo='echo {1..10}'"
|
||||
checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
|
||||
where
|
||||
f = mapM_ checkArg
|
||||
checkArg arg | '=' `elem` concat (oversimplify arg) =
|
||||
forM_ (take 1 $ filter (not . isLiteral) $ getWordParts arg) $
|
||||
\x -> warn (getId x) 2139 "This expands when defined, not when used. Consider escaping."
|
||||
checkArg _ = return ()
|
||||
|
||||
|
||||
prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]"
|
||||
prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo"
|
||||
checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)
|
||||
where
|
||||
check arg =
|
||||
when (isGlob arg) $
|
||||
warn (getId arg) 2184 "Quote arguments to unset so they're not glob expanded."
|
||||
|
||||
|
||||
prop_checkFindWithoutPath1 = verify checkFindWithoutPath "find -type f"
|
||||
prop_checkFindWithoutPath2 = verify checkFindWithoutPath "find"
|
||||
prop_checkFindWithoutPath3 = verifyNot checkFindWithoutPath "find . -type f"
|
||||
prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"
|
||||
checkFindWithoutPath = CommandCheck (Basename "find") f
|
||||
where
|
||||
f (T_SimpleCommand _ _ (cmd:args)) =
|
||||
unless (hasPath args) $
|
||||
info (getId cmd) 2185 "Some finds don't have a default path. Specify '.' explicitly."
|
||||
|
||||
-- This is a bit of a kludge. find supports flag arguments both before and after the path,
|
||||
-- as well as multiple non-flag arguments that are not the path. We assume that all the
|
||||
-- pre-path flags are single characters, which is generally the case.
|
||||
hasPath (first:rest) =
|
||||
let flag = fromJust $ getLiteralStringExt (const $ return "___") first in
|
||||
not ("-" `isPrefixOf` flag) || length flag <= 2 && hasPath rest
|
||||
hasPath [] = False
|
||||
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -1,60 +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.Formatter.JSON (format) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Data.IORef
|
||||
import GHC.Exts
|
||||
import System.IO
|
||||
import Text.JSON
|
||||
|
||||
format = do
|
||||
ref <- newIORef []
|
||||
return Formatter {
|
||||
header = return (),
|
||||
onResult = collectResult ref,
|
||||
onFailure = outputError,
|
||||
footer = finish ref
|
||||
}
|
||||
|
||||
instance JSON (PositionedComment) where
|
||||
showJSON comment@(PositionedComment start end (Comment level code string)) = makeObj [
|
||||
("file", showJSON $ posFile start),
|
||||
("line", showJSON $ posLine start),
|
||||
("endLine", showJSON $ posLine end),
|
||||
("column", showJSON $ posColumn start),
|
||||
("endColumn", showJSON $ posColumn end),
|
||||
("level", showJSON $ severityText comment),
|
||||
("code", showJSON code),
|
||||
("message", showJSON string)
|
||||
]
|
||||
|
||||
readJSON = undefined
|
||||
|
||||
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
|
||||
collectResult ref result _ =
|
||||
modifyIORef ref (\x -> crComments result ++ x)
|
||||
|
||||
finish ref = do
|
||||
list <- readIORef ref
|
||||
putStrLn $ encodeStrict list
|
||||
|
@@ -1,91 +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.Formatter.TTY (format) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Data.List
|
||||
import GHC.Exts
|
||||
import System.Info
|
||||
import System.IO
|
||||
|
||||
format :: FormatterOptions -> IO Formatter
|
||||
format options = return Formatter {
|
||||
header = return (),
|
||||
footer = return (),
|
||||
onFailure = outputError options,
|
||||
onResult = outputResult options
|
||||
}
|
||||
|
||||
colorForLevel level =
|
||||
case level of
|
||||
"error" -> 31 -- red
|
||||
"warning" -> 33 -- yellow
|
||||
"info" -> 32 -- green
|
||||
"style" -> 32 -- green
|
||||
"message" -> 1 -- bold
|
||||
"source" -> 0 -- none
|
||||
otherwise -> 0 -- none
|
||||
|
||||
outputError options file error = do
|
||||
color <- getColorFunc $ foColorOption options
|
||||
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
|
||||
|
||||
outputResult options result contents = do
|
||||
color <- getColorFunc $ foColorOption options
|
||||
let comments = crComments result
|
||||
let fileLines = lines contents
|
||||
let lineCount = fromIntegral $ length fileLines
|
||||
let groups = groupWith lineNo comments
|
||||
mapM_ (\x -> do
|
||||
let lineNum = lineNo (head x)
|
||||
let line = if lineNum < 1 || lineNum > lineCount
|
||||
then ""
|
||||
else fileLines !! fromIntegral (lineNum - 1)
|
||||
putStrLn ""
|
||||
putStrLn $ color "message" $
|
||||
"In " ++ crFilename result ++" line " ++ show lineNum ++ ":"
|
||||
putStrLn (color "source" line)
|
||||
mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) x
|
||||
putStrLn ""
|
||||
) groups
|
||||
|
||||
cuteIndent :: PositionedComment -> String
|
||||
cuteIndent comment =
|
||||
replicate (fromIntegral $ colNo comment - 1) ' ' ++
|
||||
"^-- " ++ code (codeNo comment) ++ ": " ++ messageText comment
|
||||
|
||||
code code = "SC" ++ show code
|
||||
|
||||
getColorFunc colorOption = do
|
||||
term <- hIsTerminalDevice stdout
|
||||
let windows = "mingw" `isPrefixOf` os
|
||||
let isUsableTty = term && not windows
|
||||
let useColor = case colorOption of
|
||||
ColorAlways -> True
|
||||
ColorNever -> False
|
||||
ColorAuto -> isUsableTty
|
||||
return $ if useColor then colorComment else const id
|
||||
where
|
||||
colorComment level comment =
|
||||
ansi (colorForLevel level) ++ comment ++ clear
|
||||
clear = ansi 0
|
||||
ansi n = "\x1B[" ++ show n ++ "m"
|
@@ -1,116 +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
|
||||
|
||||
|
||||
data 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,
|
||||
csExcludedWarnings :: [Integer],
|
||||
csShellTypeOverride :: Maybe Shell
|
||||
} deriving (Show, Eq)
|
||||
|
||||
data CheckResult = CheckResult {
|
||||
crFilename :: String,
|
||||
crComments :: [PositionedComment]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
emptyCheckSpec = CheckSpec {
|
||||
csFilename = "",
|
||||
csScript = "",
|
||||
csExcludedWarnings = [],
|
||||
csShellTypeOverride = Nothing
|
||||
}
|
||||
|
||||
-- Parser input and output
|
||||
data ParseSpec = ParseSpec {
|
||||
psFilename :: String,
|
||||
psScript :: String
|
||||
} 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
|
||||
}
|
||||
|
||||
data AnalysisResult = AnalysisResult {
|
||||
arComments :: [TokenComment]
|
||||
}
|
||||
|
||||
|
||||
-- Formatter options
|
||||
data 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
|
||||
|
13
nextnumber
Executable file
13
nextnumber
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# TODO: Find a less trashy way to get the next available error code
|
||||
if ! shopt -s globstar
|
||||
then
|
||||
echo "Error: This script depends on Bash 4." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for i in 1 2
|
||||
do
|
||||
last=$(grep -hv "^prop" ./**/*.hs | grep -Ewo "$i[0-9]{3}" | sort -n | tail -n 1)
|
||||
echo "Next ${i}xxx: $((last+1))"
|
||||
done
|
11
quickrun
11
quickrun
@@ -1,5 +1,12 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
# quickrun runs ShellCheck in an interpreted mode.
|
||||
# This allows testing changes without recompiling.
|
||||
|
||||
runghc -idist/build/autogen shellcheck.hs "$@"
|
||||
runghc -isrc -idist/build/autogen shellcheck.hs "$@"
|
||||
|
||||
# Note: with new-build you can
|
||||
#
|
||||
# % cabal new-run --disable-optimization -- shellcheck "$@"
|
||||
#
|
||||
# This does build the executable, but as the optimisation is disabled,
|
||||
# the build is quite fast.
|
||||
|
36
quicktest
36
quicktest
@@ -1,21 +1,21 @@
|
||||
#!/bin/bash
|
||||
# quicktest runs the ShellCheck unit tests in an interpreted mode.
|
||||
# This allows running tests without compiling, which can be faster.
|
||||
# shellcheck disable=SC2091
|
||||
|
||||
# quicktest runs the ShellCheck unit tests.
|
||||
# Once `doctests` test executable is build, we can just run it
|
||||
# This allows running tests without compiling library, which is faster.
|
||||
# 'cabal test' remains the source of truth.
|
||||
|
||||
(
|
||||
var=$(echo 'liftM and $ sequence [
|
||||
ShellCheck.Analytics.runTests
|
||||
,ShellCheck.Parser.runTests
|
||||
,ShellCheck.Checker.runTests
|
||||
,ShellCheck.Checks.Commands.runTests
|
||||
,ShellCheck.AnalyzerLib.runTests
|
||||
]' | tr -d '\n' | cabal repl 2>&1 | tee /dev/stderr)
|
||||
if [[ $var == *$'\nTrue'* ]]
|
||||
then
|
||||
exit 0
|
||||
else
|
||||
grep -C 3 -e "Fail" -e "Tracing" <<< "$var"
|
||||
exit 1
|
||||
fi
|
||||
) 2>&1
|
||||
$(find dist -type f -name doctests)
|
||||
|
||||
# Note: if you have build the project with new-build
|
||||
#
|
||||
# % cabal new-build -w ghc-8.4.3 --enable-tests
|
||||
#
|
||||
# and have cabal-plan installed (e.g. with cabal new-install cabal-plan),
|
||||
# then you can quicktest with
|
||||
#
|
||||
# % $(cabal-plan list-bin doctests)
|
||||
#
|
||||
# Once the test executable exists, we can simply run it to perform doctests
|
||||
# which use GHCi under the hood.
|
||||
|
@@ -32,6 +32,12 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
||||
|
||||
# OPTIONS
|
||||
|
||||
**-a**,\ **--check-sourced**
|
||||
|
||||
: Emit warnings in sourced files. Normally, `shellcheck` will only warn
|
||||
about issues in the specified files. With this option, any issues in
|
||||
sourced files files will also be reported.
|
||||
|
||||
**-C**[*WHEN*],\ **--color**[=*WHEN*]
|
||||
|
||||
: For TTY output, enable colors *always*, *never* or *auto*. The default
|
||||
@@ -50,6 +56,11 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
||||
standard output. Subsequent **-f** options are ignored, see **FORMATS**
|
||||
below for more information.
|
||||
|
||||
**-S**\ *SEVERITY*,\ **--severity=***severity*
|
||||
|
||||
: Specify minimum severity of errors to consider. Valid values are *error*,
|
||||
*warning*, *info* and *style*. The default is *style*.
|
||||
|
||||
**-s**\ *shell*,\ **--shell=***shell*
|
||||
|
||||
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
|
||||
@@ -60,6 +71,11 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
||||
|
||||
: Print version information and exit.
|
||||
|
||||
**-W** *NUM*,\ **--wiki-link-count=NUM**
|
||||
|
||||
: For TTY output, show *NUM* wiki links to more information about mentioned
|
||||
warnings. Set to 0 to disable them entirely.
|
||||
|
||||
**-x**,\ **--external-sources**
|
||||
|
||||
: Follow 'source' statements even when the file is not specified as input.
|
||||
@@ -67,6 +83,7 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
||||
line (plus `/dev/null`). This option allows following any file the script
|
||||
may `source`.
|
||||
|
||||
|
||||
# FORMATS
|
||||
|
||||
**tty**
|
||||
@@ -157,6 +174,11 @@ Valid keys are:
|
||||
used to tell shellcheck where to look for a file whose name is determined
|
||||
at runtime, or to skip a source by telling it to use `/dev/null`.
|
||||
|
||||
**shell**
|
||||
: Overrides the shell detected from the shebang. This is useful for
|
||||
files meant to be included (and thus lacking a shebang), or possibly
|
||||
as a more targeted alternative to 'disable=2039'.
|
||||
|
||||
# ENVIRONMENT VARIABLES
|
||||
The environment variable `SHELLCHECK_OPTS` can be set with default flags:
|
||||
|
||||
@@ -175,6 +197,15 @@ ShellCheck uses the follow exit codes:
|
||||
+ 3: ShellCheck was invoked with bad syntax (e.g. unknown flag).
|
||||
+ 4: ShellCheck was invoked with bad options (e.g. unknown formatter).
|
||||
|
||||
# LOCALE
|
||||
This version of ShellCheck is only available in English. All files are
|
||||
leniently decoded as UTF-8, with a fallback of ISO-8859-1 for invalid
|
||||
sequences. `LC_CTYPE` is respected for output, and defaults to UTF-8 for
|
||||
locales where encoding is unspecified (such as the `C` locale).
|
||||
|
||||
Windows users seeing `commitBuffer: invalid argument (invalid character)`
|
||||
should set their terminal to use UTF-8 with `chcp 65001`.
|
||||
|
||||
# AUTHOR
|
||||
ShellCheck is written and maintained by Vidar Holen.
|
||||
|
||||
@@ -186,7 +217,7 @@ https://github.com/koalaman/shellcheck/issues
|
||||
# COPYRIGHT
|
||||
Copyright 2012-2015, Vidar Holen.
|
||||
Licensed under the GNU General Public License version 3 or later,
|
||||
see http://gnu.org/licenses/gpl.html
|
||||
see https://gnu.org/licenses/gpl.html
|
||||
|
||||
|
||||
# SEE ALSO
|
||||
|
271
shellcheck.hs
271
shellcheck.hs
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,34 +15,38 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Checker
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Regex
|
||||
import ShellCheck.Checker
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Regex
|
||||
|
||||
import ShellCheck.Formatter.Format
|
||||
import qualified ShellCheck.Formatter.CheckStyle
|
||||
import ShellCheck.Formatter.Format
|
||||
import qualified ShellCheck.Formatter.GCC
|
||||
import qualified ShellCheck.Formatter.JSON
|
||||
import qualified ShellCheck.Formatter.TTY
|
||||
|
||||
import Control.Exception
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Data.Char
|
||||
import Data.Functor
|
||||
import Data.Either
|
||||
import qualified Data.Map as Map
|
||||
import Data.Maybe
|
||||
import Data.Monoid
|
||||
import Prelude hiding (catch)
|
||||
import System.Console.GetOpt
|
||||
import System.Directory
|
||||
import System.Environment
|
||||
import System.Exit
|
||||
import System.IO
|
||||
import Control.Exception
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Data.Bits
|
||||
import Data.Char
|
||||
import Data.Either
|
||||
import Data.Functor
|
||||
import Data.IORef
|
||||
import Data.List
|
||||
import qualified Data.Map as Map
|
||||
import Data.Maybe
|
||||
import Data.Monoid
|
||||
import Data.Semigroup (Semigroup (..))
|
||||
import Prelude hiding (catch)
|
||||
import System.Console.GetOpt
|
||||
import System.Directory
|
||||
import System.Environment
|
||||
import System.Exit
|
||||
import System.IO
|
||||
|
||||
data Flag = Flag String String
|
||||
data Status =
|
||||
@@ -53,39 +57,54 @@ data Status =
|
||||
| RuntimeException
|
||||
deriving (Ord, Eq, Show)
|
||||
|
||||
instance Semigroup Status where
|
||||
(<>) = max
|
||||
|
||||
instance Monoid Status where
|
||||
mempty = NoProblems
|
||||
mappend = max
|
||||
mappend = (Data.Semigroup.<>)
|
||||
|
||||
data Options = Options {
|
||||
checkSpec :: CheckSpec,
|
||||
externalSources :: Bool,
|
||||
formatterOptions :: FormatterOptions
|
||||
checkSpec :: CheckSpec,
|
||||
externalSources :: Bool,
|
||||
formatterOptions :: FormatterOptions,
|
||||
minSeverity :: Severity
|
||||
}
|
||||
|
||||
defaultOptions = Options {
|
||||
checkSpec = emptyCheckSpec,
|
||||
externalSources = False,
|
||||
formatterOptions = FormatterOptions {
|
||||
formatterOptions = newFormatterOptions {
|
||||
foColorOption = ColorAuto
|
||||
}
|
||||
},
|
||||
minSeverity = StyleC
|
||||
}
|
||||
|
||||
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
|
||||
options = [
|
||||
Option "e" ["exclude"]
|
||||
(ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings",
|
||||
Option "f" ["format"]
|
||||
(ReqArg (Flag "format") "FORMAT") "output format",
|
||||
Option "a" ["check-sourced"]
|
||||
(NoArg $ Flag "sourced" "false") "Include warnings from sourced files",
|
||||
Option "C" ["color"]
|
||||
(OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN")
|
||||
"Use color (auto, always, never)",
|
||||
Option "e" ["exclude"]
|
||||
(ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings",
|
||||
Option "f" ["format"]
|
||||
(ReqArg (Flag "format") "FORMAT") $
|
||||
"Output format (" ++ formatList ++ ")",
|
||||
Option "s" ["shell"]
|
||||
(ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (sh,bash,dash,ksh)",
|
||||
Option "x" ["external-sources"]
|
||||
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES.",
|
||||
(ReqArg (Flag "shell") "SHELLNAME")
|
||||
"Specify dialect (sh, bash, dash, ksh)",
|
||||
Option "S" ["severity"]
|
||||
(ReqArg (Flag "severity") "SEVERITY")
|
||||
"Minimum severity of errors to consider (error, warning, info, style)",
|
||||
Option "V" ["version"]
|
||||
(NoArg $ Flag "version" "true") "Print version information"
|
||||
(NoArg $ Flag "version" "true") "Print version information",
|
||||
Option "W" ["wiki-link-count"]
|
||||
(ReqArg (Flag "wiki-link-count") "NUM")
|
||||
"The number of wiki links to show, when applicable.",
|
||||
Option "x" ["external-sources"]
|
||||
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES"
|
||||
]
|
||||
|
||||
printErr = lift . hPutStrLn stderr
|
||||
@@ -106,9 +125,13 @@ formats options = Map.fromList [
|
||||
("tty", ShellCheck.Formatter.TTY.format options)
|
||||
]
|
||||
|
||||
getOption [] _ = Nothing
|
||||
formatList = intercalate ", " names
|
||||
where
|
||||
names = Map.keys $ formats (formatterOptions defaultOptions)
|
||||
|
||||
getOption [] _ = Nothing
|
||||
getOption (Flag var val:_) name | name == var = return val
|
||||
getOption (_:rest) flag = getOption rest flag
|
||||
getOption (_:rest) flag = getOption rest flag
|
||||
|
||||
getOptions options name =
|
||||
map (\(Flag _ val) -> val) . filter (\(Flag var _) -> var == name) $ options
|
||||
@@ -122,13 +145,7 @@ split char str =
|
||||
else split' rest (a:element)
|
||||
split' [] element = [reverse element]
|
||||
|
||||
getExclusions options =
|
||||
let elements = concatMap (split ',') $ getOptions options "exclude"
|
||||
clean = dropWhile (not . isDigit)
|
||||
in
|
||||
map (Prelude.read . clean) elements :: [Int]
|
||||
|
||||
toStatus = liftM (either id id) . runExceptT
|
||||
toStatus = fmap (either id id) . runExceptT
|
||||
|
||||
getEnvArgs = do
|
||||
opts <- getEnv "SHELLCHECK_OPTS" `catch` cantWaitForLookupEnv
|
||||
@@ -148,10 +165,10 @@ main = do
|
||||
|
||||
statusToCode status =
|
||||
case status of
|
||||
NoProblems -> ExitSuccess
|
||||
SomeProblems -> ExitFailure 1
|
||||
SyntaxFailure -> ExitFailure 3
|
||||
SupportFailure -> ExitFailure 4
|
||||
NoProblems -> ExitSuccess
|
||||
SomeProblems -> ExitFailure 1
|
||||
SyntaxFailure -> ExitFailure 3
|
||||
SupportFailure -> ExitFailure 4
|
||||
RuntimeException -> ExitFailure 2
|
||||
|
||||
process :: [Flag] -> [FilePath] -> ExceptT Status IO Status
|
||||
@@ -185,30 +202,50 @@ runFormatter sys format options files = do
|
||||
newStatus <- process file `catch` handler file
|
||||
return $ status `mappend` newStatus
|
||||
handler :: FilePath -> IOException -> IO Status
|
||||
handler file e = do
|
||||
onFailure format file (show e)
|
||||
handler file e = reportFailure file (show e)
|
||||
reportFailure file str = do
|
||||
onFailure format file str
|
||||
return RuntimeException
|
||||
|
||||
process :: FilePath -> IO Status
|
||||
process filename = do
|
||||
contents <- inputFile filename
|
||||
let checkspec = (checkSpec options) {
|
||||
csFilename = filename,
|
||||
csScript = contents
|
||||
}
|
||||
result <- checkScript sys checkspec
|
||||
onResult format result contents
|
||||
return $
|
||||
if null (crComments result)
|
||||
then NoProblems
|
||||
else SomeProblems
|
||||
input <- siReadFile sys filename
|
||||
either (reportFailure filename) check input
|
||||
where
|
||||
check contents = do
|
||||
let checkspec = (checkSpec options) {
|
||||
csFilename = filename,
|
||||
csScript = contents
|
||||
}
|
||||
result <- checkScript sys checkspec
|
||||
onResult format result sys
|
||||
return $
|
||||
if null (crComments result)
|
||||
then NoProblems
|
||||
else SomeProblems
|
||||
|
||||
parseColorOption colorOption =
|
||||
case colorOption of
|
||||
"auto" -> ColorAuto
|
||||
"always" -> ColorAlways
|
||||
"never" -> ColorNever
|
||||
_ -> error $ "Bad value for --color `" ++ colorOption ++ "'"
|
||||
parseEnum name value list =
|
||||
case filter ((== value) . fst) list of
|
||||
[(name, value)] -> return value
|
||||
[] -> do
|
||||
printErr $ "Unknown value for --" ++ name ++ ". " ++
|
||||
"Valid options are: " ++ (intercalate ", " $ map fst list)
|
||||
throwError SupportFailure
|
||||
|
||||
parseColorOption value =
|
||||
parseEnum "color" value [
|
||||
("auto", ColorAuto),
|
||||
("always", ColorAlways),
|
||||
("never", ColorNever)
|
||||
]
|
||||
|
||||
parseSeverityOption value =
|
||||
parseEnum "severity" value [
|
||||
("error", ErrorC),
|
||||
("warning", WarningC),
|
||||
("info", InfoC),
|
||||
("style", StyleC)
|
||||
]
|
||||
|
||||
parseOption flag options =
|
||||
case flag of
|
||||
@@ -222,7 +259,7 @@ parseOption flag options =
|
||||
}
|
||||
|
||||
Flag "exclude" str -> do
|
||||
new <- mapM parseNum $ split ',' str
|
||||
new <- mapM parseNum $ filter (not . null) $ split ',' str
|
||||
let old = csExcludedWarnings . checkSpec $ options
|
||||
return options {
|
||||
checkSpec = (checkSpec options) {
|
||||
@@ -239,10 +276,34 @@ parseOption flag options =
|
||||
externalSources = True
|
||||
}
|
||||
|
||||
Flag "color" color ->
|
||||
Flag "color" color -> do
|
||||
option <- parseColorOption color
|
||||
return options {
|
||||
formatterOptions = (formatterOptions options) {
|
||||
foColorOption = parseColorOption color
|
||||
foColorOption = option
|
||||
}
|
||||
}
|
||||
|
||||
Flag "sourced" _ ->
|
||||
return options {
|
||||
checkSpec = (checkSpec options) {
|
||||
csCheckSourced = True
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,20 +315,34 @@ parseOption flag options =
|
||||
parseNum ('S':'C':str) = parseNum str
|
||||
parseNum num = do
|
||||
unless (all isDigit num) $ do
|
||||
printErr $ "Bad exclusion: " ++ num
|
||||
printErr $ "Invalid number: " ++ num
|
||||
throwError SyntaxFailure
|
||||
return (Prelude.read num :: Integer)
|
||||
|
||||
ioInterface options files = do
|
||||
inputs <- mapM normalize files
|
||||
cache <- newIORef emptyCache
|
||||
return SystemInterface {
|
||||
siReadFile = get inputs
|
||||
siReadFile = get cache inputs
|
||||
}
|
||||
where
|
||||
get inputs file = do
|
||||
emptyCache :: Map.Map FilePath String
|
||||
emptyCache = Map.empty
|
||||
get cache inputs file = do
|
||||
map <- readIORef cache
|
||||
case Map.lookup file map of
|
||||
Just x -> return $ Right x
|
||||
Nothing -> fetch cache inputs file
|
||||
|
||||
fetch cache inputs file = do
|
||||
ok <- allowable inputs file
|
||||
if ok
|
||||
then (Right <$> inputFile file) `catch` handler
|
||||
then (do
|
||||
(contents, shouldCache) <- inputFile file
|
||||
when shouldCache $
|
||||
modifyIORef cache $ Map.insert file contents
|
||||
return $ Right contents
|
||||
) `catch` handler
|
||||
else return $ Left (file ++ " was not specified as input (see shellcheck -x).")
|
||||
|
||||
where
|
||||
@@ -288,13 +363,49 @@ ioInterface options files = do
|
||||
fallback path _ = return path
|
||||
|
||||
inputFile file = do
|
||||
contents <-
|
||||
(handle, shouldCache) <-
|
||||
if file == "-"
|
||||
then getContents
|
||||
else readFile file
|
||||
then return (stdin, True)
|
||||
else do
|
||||
h <- openBinaryFile file ReadMode
|
||||
reopenable <- hIsSeekable h
|
||||
return (h, not reopenable)
|
||||
|
||||
hSetBinaryMode handle True
|
||||
contents <- decodeString <$> hGetContents handle -- closes handle
|
||||
|
||||
seq (length contents) $
|
||||
return contents
|
||||
return (contents, shouldCache)
|
||||
|
||||
-- Decode a char8 string into a utf8 string, with fallback on
|
||||
-- ISO-8859-1. This avoids depending on additional libraries.
|
||||
decodeString = decode
|
||||
where
|
||||
decode [] = []
|
||||
decode (c:rest) | isAscii c = c : decode rest
|
||||
decode (c:rest) =
|
||||
let num = (fromIntegral $ ord c) :: Int
|
||||
next = case num of
|
||||
_ | num >= 0xF8 -> Nothing
|
||||
| num >= 0xF0 -> construct (num .&. 0x07) 3 rest
|
||||
| num >= 0xE0 -> construct (num .&. 0x0F) 2 rest
|
||||
| num >= 0xC0 -> construct (num .&. 0x1F) 1 rest
|
||||
| True -> Nothing
|
||||
in
|
||||
case next of
|
||||
Just (n, remainder) -> chr n : decode remainder
|
||||
Nothing -> c : decode rest
|
||||
|
||||
construct x 0 rest = do
|
||||
guard $ x <= 0x10FFFF
|
||||
return (x, rest)
|
||||
construct x n (c:rest) =
|
||||
let num = (fromIntegral $ ord c) :: Int in
|
||||
if num >= 0x80 && num <= 0xBF
|
||||
then construct ((x `shiftL` 6) .|. (num .&. 0x3f)) (n-1) rest
|
||||
else Nothing
|
||||
construct _ _ _ = Nothing
|
||||
|
||||
|
||||
verifyFiles files =
|
||||
when (null files) $ do
|
||||
@@ -306,4 +417,4 @@ printVersion = do
|
||||
putStrLn "ShellCheck - shell script analysis tool"
|
||||
putStrLn $ "version: " ++ shellcheckVersion
|
||||
putStrLn "license: GNU General Public License, version 3"
|
||||
putStrLn "website: http://www.shellcheck.net"
|
||||
putStrLn "website: https://www.shellcheck.net"
|
||||
|
53
snap/snapcraft.yaml
Normal file
53
snap/snapcraft.yaml
Normal file
@@ -0,0 +1,53 @@
|
||||
name: shellcheck
|
||||
summary: A shell script static analysis tool
|
||||
description: |
|
||||
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh
|
||||
shell scripts.
|
||||
|
||||
The goals of ShellCheck are
|
||||
|
||||
- To point out and clarify typical beginner's syntax issues that cause a
|
||||
shell to give cryptic error messages.
|
||||
|
||||
- To point out and clarify typical intermediate level semantic problems that
|
||||
cause a shell to behave strangely and counter-intuitively.
|
||||
|
||||
- To point out subtle caveats, corner cases and pitfalls that may cause an
|
||||
advanced user's otherwise working script to fail under future
|
||||
circumstances.
|
||||
|
||||
By default ShellCheck can only check non-hidden files under /home, to make
|
||||
ShellCheck be able to check files under /media and /run/media you must
|
||||
connect it to the `removable-media` interface manually:
|
||||
|
||||
# snap connect shellcheck:removable-media
|
||||
|
||||
version: git
|
||||
grade: devel
|
||||
confinement: strict
|
||||
|
||||
apps:
|
||||
shellcheck:
|
||||
command: usr/bin/shellcheck
|
||||
plugs: [home, removable-media]
|
||||
|
||||
parts:
|
||||
shellcheck:
|
||||
plugin: dump
|
||||
source: ./
|
||||
build-packages:
|
||||
- cabal-install
|
||||
- squid3
|
||||
build: |
|
||||
# See comments in .snapsquid.conf
|
||||
[ "$http_proxy" ] && {
|
||||
squid3 -f .snapsquid.conf
|
||||
export http_proxy="http://localhost:8888"
|
||||
sleep 3
|
||||
}
|
||||
cabal sandbox init
|
||||
cabal update || cat /var/log/squid/*
|
||||
cabal install -j
|
||||
install: |
|
||||
install -d $SNAPCRAFT_PART_INSTALL/usr/bin
|
||||
install .cabal-sandbox/bin/shellcheck $SNAPCRAFT_PART_INSTALL/usr/bin
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,40 +15,45 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
|
||||
module ShellCheck.AST where
|
||||
|
||||
import Control.Monad
|
||||
import GHC.Generics (Generic)
|
||||
import Control.Monad.Identity
|
||||
import Control.DeepSeq
|
||||
import Text.Parsec
|
||||
import qualified ShellCheck.Regex as Re
|
||||
import Prelude hiding (id)
|
||||
|
||||
data Id = Id Int deriving (Show, Eq, Ord)
|
||||
newtype Id = Id Int deriving (Show, Eq, Ord, Generic, NFData)
|
||||
|
||||
data Quoted = Quoted | Unquoted deriving (Show, Eq)
|
||||
data Dashed = Dashed | Undashed deriving (Show, Eq)
|
||||
data AssignmentMode = Assign | Append deriving (Show, Eq)
|
||||
data FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
|
||||
data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
|
||||
newtype FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
|
||||
newtype FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
|
||||
data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
|
||||
|
||||
newtype Root = Root Token
|
||||
data Token =
|
||||
TA_Binary Id String Token Token
|
||||
| TA_Assignment Id String Token Token
|
||||
| TA_Variable Id String [Token]
|
||||
| TA_Expansion Id [Token]
|
||||
| TA_Index Id Token
|
||||
| TA_Sequence Id [Token]
|
||||
| TA_Trinary Id Token Token Token
|
||||
| TA_Unary Id String Token
|
||||
| TC_And Id ConditionType String Token Token
|
||||
| TC_Binary Id ConditionType String Token Token
|
||||
| TC_Group Id ConditionType Token
|
||||
| TC_Noary Id ConditionType Token
|
||||
| TC_Nullary Id ConditionType Token
|
||||
| TC_Or Id ConditionType String Token Token
|
||||
| TC_Unary Id ConditionType String Token
|
||||
| TC_Empty Id ConditionType
|
||||
| T_AND_IF Id
|
||||
| T_AndIf Id (Token) (Token)
|
||||
| T_AndIf Id Token Token
|
||||
| T_Arithmetic Id Token
|
||||
| T_Array Id [Token]
|
||||
| T_IndexedElement Id [Token] Token
|
||||
@@ -109,7 +114,8 @@ data Token =
|
||||
| T_NEWLINE Id
|
||||
| T_NormalWord Id [Token]
|
||||
| T_OR_IF Id
|
||||
| T_OrIf Id (Token) (Token)
|
||||
| T_OrIf Id Token Token
|
||||
| T_ParamSubSpecialChar Id String -- e.g. '%' in ${foo%bar} or '/' in ${foo/bar/baz}
|
||||
| T_Pipeline Id [Token] [Token] -- [Pipe separators] [Commands]
|
||||
| T_ProcSub Id String [Token]
|
||||
| T_Rbrace Id
|
||||
@@ -131,7 +137,8 @@ data Token =
|
||||
| T_Pipe Id String
|
||||
| T_CoProc Id (Maybe String) Token
|
||||
| T_CoProcBody Id Token
|
||||
| T_Include Id Token Token -- . & source: SimpleCommand T_Script
|
||||
| T_Include Id Token
|
||||
| T_SourceCommand Id Token Token
|
||||
deriving (Show)
|
||||
|
||||
data Annotation =
|
||||
@@ -160,11 +167,6 @@ analyze f g i =
|
||||
i newT
|
||||
roundAll = mapM round
|
||||
|
||||
roundMaybe Nothing = return Nothing
|
||||
roundMaybe (Just v) = do
|
||||
s <- round v
|
||||
return (Just s)
|
||||
|
||||
dl l v = do
|
||||
x <- roundAll l
|
||||
return $ v x
|
||||
@@ -256,7 +258,7 @@ analyze f g i =
|
||||
delve (TC_Group id typ token) = d1 token $ TC_Group id typ
|
||||
delve (TC_Binary id typ op lhs rhs) = d2 lhs rhs $ TC_Binary id typ op
|
||||
delve (TC_Unary id typ op token) = d1 token $ TC_Unary id typ op
|
||||
delve (TC_Noary id typ token) = d1 token $ TC_Noary id typ
|
||||
delve (TC_Nullary id typ token) = d1 token $ TC_Nullary id typ
|
||||
|
||||
delve (TA_Binary id op t1 t2) = d2 t1 t2 $ TA_Binary id op
|
||||
delve (TA_Assignment id op t1 t2) = d2 t1 t2 $ TA_Assignment id op
|
||||
@@ -268,13 +270,15 @@ analyze f g i =
|
||||
c <- round t3
|
||||
return $ TA_Trinary id a b c
|
||||
delve (TA_Expansion id t) = dl t $ TA_Expansion id
|
||||
delve (TA_Index id t) = d1 t $ TA_Index id
|
||||
delve (TA_Variable id str t) = dl t $ TA_Variable id str
|
||||
delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns
|
||||
delve (T_CoProc id var body) = d1 body $ T_CoProc id var
|
||||
delve (T_CoProcBody id t) = d1 t $ T_CoProcBody id
|
||||
delve (T_Include id includer script) = d2 includer script $ T_Include id
|
||||
delve (T_Include id script) = d1 script $ T_Include id
|
||||
delve (T_SourceCommand id includer t_include) = d2 includer t_include $ T_SourceCommand id
|
||||
delve t = return t
|
||||
|
||||
getId :: Token -> Id
|
||||
getId t = case t of
|
||||
T_AND_IF id -> id
|
||||
T_OR_IF id -> id
|
||||
@@ -318,6 +322,7 @@ getId t = case t of
|
||||
T_DollarBraced id _ -> id
|
||||
T_DollarArithmetic id _ -> id
|
||||
T_BraceExpansion id _ -> id
|
||||
T_ParamSubSpecialChar id _ -> id
|
||||
T_DollarBraceCommandExpansion id _ -> id
|
||||
T_IoFile id _ _ -> id
|
||||
T_IoDuplicate id _ _ -> id
|
||||
@@ -353,14 +358,13 @@ getId t = case t of
|
||||
TC_Group id _ _ -> id
|
||||
TC_Binary id _ _ _ _ -> id
|
||||
TC_Unary id _ _ _ -> id
|
||||
TC_Noary id _ _ -> id
|
||||
TC_Nullary id _ _ -> id
|
||||
TA_Binary id _ _ _ -> id
|
||||
TA_Assignment id _ _ _ -> id
|
||||
TA_Unary id _ _ -> id
|
||||
TA_Sequence id _ -> id
|
||||
TA_Trinary id _ _ _ -> id
|
||||
TA_Expansion id _ -> id
|
||||
TA_Index id _ -> id
|
||||
T_ProcSub id _ _ -> id
|
||||
T_Glob id _ -> id
|
||||
T_ForArithmetic id _ _ _ _ -> id
|
||||
@@ -371,12 +375,18 @@ getId t = case t of
|
||||
T_Pipe id _ -> id
|
||||
T_CoProc id _ _ -> id
|
||||
T_CoProcBody id _ -> id
|
||||
T_Include id _ _ -> id
|
||||
T_Include id _ -> id
|
||||
T_SourceCommand id _ _ -> id
|
||||
T_UnparsedIndex id _ _ -> id
|
||||
TC_Empty id _ -> id
|
||||
TA_Variable id _ _ -> id
|
||||
|
||||
blank :: Monad m => Token -> m ()
|
||||
blank = const $ return ()
|
||||
doAnalysis f = analyze f blank (return . id)
|
||||
doStackAnalysis startToken endToken = analyze startToken endToken (return . id)
|
||||
doAnalysis :: Monad m => (Token -> m ()) -> Token -> m Token
|
||||
doAnalysis f = analyze f blank return
|
||||
doStackAnalysis :: Monad m => (Token -> m ()) -> (Token -> m ()) -> Token -> m Token
|
||||
doStackAnalysis startToken endToken = analyze startToken endToken return
|
||||
doTransform :: (Token -> Token) -> Token -> Token
|
||||
doTransform i = runIdentity . analyze blank blank (return . i)
|
||||
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.ASTLib where
|
||||
|
||||
@@ -23,6 +23,8 @@ import ShellCheck.AST
|
||||
|
||||
import Control.Monad.Writer
|
||||
import Control.Monad
|
||||
import Data.Char
|
||||
import Data.Functor
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
|
||||
@@ -47,8 +49,8 @@ willSplit x =
|
||||
T_NormalWord _ l -> any willSplit l
|
||||
_ -> False
|
||||
|
||||
isGlob (T_Extglob {}) = True
|
||||
isGlob (T_Glob {}) = True
|
||||
isGlob T_Extglob {} = True
|
||||
isGlob T_Glob {} = True
|
||||
isGlob (T_NormalWord _ l) = any isGlob l
|
||||
isGlob _ = False
|
||||
|
||||
@@ -86,13 +88,14 @@ oversimplify token =
|
||||
(T_Glob _ s) -> [s]
|
||||
(T_Pipeline _ _ [x]) -> oversimplify x
|
||||
(T_Literal _ x) -> [x]
|
||||
(T_ParamSubSpecialChar _ x) -> [x]
|
||||
(T_SimpleCommand _ vars words) -> concatMap oversimplify words
|
||||
(T_Redirecting _ _ foo) -> oversimplify foo
|
||||
(T_DollarSingleQuoted _ s) -> [s]
|
||||
(T_Annotation _ _ s) -> oversimplify s
|
||||
-- Workaround for let "foo = bar" parsing
|
||||
(TA_Sequence _ [TA_Expansion _ v]) -> concatMap oversimplify v
|
||||
otherwise -> []
|
||||
_ -> []
|
||||
|
||||
|
||||
-- Turn a SimpleCommand foo -avz --bar=baz into args "a", "v", "z", "bar",
|
||||
@@ -110,10 +113,24 @@ getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
|
||||
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
|
||||
|
||||
-- Get all flags in a GNU way, up until --
|
||||
getAllFlags :: Token -> [(Token, String)]
|
||||
getAllFlags = getFlagsUntil (== "--")
|
||||
-- Get all flags in a BSD way, up until first non-flag argument
|
||||
getLeadingFlags = getFlagsUntil (not . ("-" `isPrefixOf`))
|
||||
-- Get all flags in a BSD way, up until first non-flag argument or --
|
||||
getLeadingFlags = getFlagsUntil (\x -> x == "--" || (not $ "-" `isPrefixOf` x))
|
||||
|
||||
-- Check if a command has a flag.
|
||||
hasFlag cmd str = str `elem` (map snd $ getAllFlags cmd)
|
||||
|
||||
-- Is this token a word that starts with a dash?
|
||||
isFlag token =
|
||||
case getWordParts token of
|
||||
T_Literal _ ('-':_) : _ -> True
|
||||
_ -> False
|
||||
|
||||
-- Is this token a flag where the - is unquoted?
|
||||
isUnquotedFlag token = fromMaybe False $ do
|
||||
str <- getLeadingUnquotedString token
|
||||
return $ "-" `isPrefixOf` str
|
||||
|
||||
-- Given a T_DollarBraced, return a simplified version of the string contents.
|
||||
bracedString (T_DollarBraced _ l) = concat $ oversimplify l
|
||||
@@ -139,9 +156,9 @@ mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
|
||||
-- Is it certain that this word will becomes multiple words?
|
||||
willBecomeMultipleArgs t = willConcatInAssignment t || f t
|
||||
where
|
||||
f (T_Extglob {}) = True
|
||||
f (T_Glob {}) = True
|
||||
f (T_BraceExpansion {}) = True
|
||||
f T_Extglob {} = True
|
||||
f T_Glob {} = True
|
||||
f T_BraceExpansion {} = True
|
||||
f (T_DoubleQuoted _ parts) = any f parts
|
||||
f (T_NormalWord _ parts) = any f parts
|
||||
f _ = False
|
||||
@@ -149,7 +166,7 @@ willBecomeMultipleArgs t = willConcatInAssignment t || f t
|
||||
-- This does token cause implicit concatenation in assignments?
|
||||
willConcatInAssignment token =
|
||||
case token of
|
||||
t@(T_DollarBraced {}) -> isArrayExpansion t
|
||||
t@T_DollarBraced {} -> isArrayExpansion t
|
||||
(T_DoubleQuoted _ parts) -> any willConcatInAssignment parts
|
||||
(T_NormalWord _ parts) -> any willConcatInAssignment parts
|
||||
_ -> False
|
||||
@@ -164,12 +181,33 @@ onlyLiteralString = fromJust . getLiteralStringExt (const $ return "")
|
||||
|
||||
-- Maybe get a literal string, but only if it's an unquoted argument.
|
||||
getUnquotedLiteral (T_NormalWord _ list) =
|
||||
liftM concat $ mapM str list
|
||||
concat <$> mapM str list
|
||||
where
|
||||
str (T_Literal _ s) = return s
|
||||
str _ = Nothing
|
||||
getUnquotedLiteral _ = Nothing
|
||||
|
||||
-- Get the last unquoted T_Literal in a word like "${var}foo"THIS
|
||||
-- or nothing if the word does not end in an unquoted literal.
|
||||
getTrailingUnquotedLiteral :: Token -> Maybe Token
|
||||
getTrailingUnquotedLiteral t =
|
||||
case t of
|
||||
(T_NormalWord _ list@(_:_)) ->
|
||||
from (last list)
|
||||
_ -> Nothing
|
||||
where
|
||||
from t =
|
||||
case t of
|
||||
T_Literal {} -> return t
|
||||
_ -> Nothing
|
||||
|
||||
-- Get the leading, unquoted, literal string of a token (if any).
|
||||
getLeadingUnquotedString :: Token -> Maybe String
|
||||
getLeadingUnquotedString t =
|
||||
case t of
|
||||
T_NormalWord _ ((T_Literal _ s) : _) -> return s
|
||||
_ -> Nothing
|
||||
|
||||
-- Maybe get the literal string of this token and any globs in it.
|
||||
getGlobOrLiteralString = getLiteralStringExt f
|
||||
where
|
||||
@@ -181,15 +219,51 @@ getGlobOrLiteralString = getLiteralStringExt f
|
||||
getLiteralStringExt :: (Token -> Maybe String) -> Token -> Maybe String
|
||||
getLiteralStringExt more = g
|
||||
where
|
||||
allInList = liftM concat . mapM g
|
||||
allInList = fmap concat . mapM g
|
||||
g (T_DoubleQuoted _ l) = allInList l
|
||||
g (T_DollarDoubleQuoted _ l) = allInList l
|
||||
g (T_NormalWord _ l) = allInList l
|
||||
g (TA_Expansion _ l) = allInList l
|
||||
g (T_SingleQuoted _ s) = return s
|
||||
g (T_Literal _ s) = return s
|
||||
g (T_ParamSubSpecialChar _ s) = return s
|
||||
g (T_DollarSingleQuoted _ s) = return $ decodeEscapes s
|
||||
g x = more x
|
||||
|
||||
-- Bash style $'..' decoding
|
||||
decodeEscapes ('\\':c:cs) =
|
||||
case c of
|
||||
'a' -> '\a' : rest
|
||||
'b' -> '\b' : rest
|
||||
'e' -> '\x1B' : rest
|
||||
'f' -> '\f' : rest
|
||||
'n' -> '\n' : rest
|
||||
'r' -> '\r' : rest
|
||||
't' -> '\t' : rest
|
||||
'v' -> '\v' : rest
|
||||
'\'' -> '\'' : rest
|
||||
'"' -> '"' : rest
|
||||
'\\' -> '\\' : rest
|
||||
'x' ->
|
||||
case cs of
|
||||
(x:y:more) ->
|
||||
if isHexDigit x && isHexDigit y
|
||||
then chr (16*(digitToInt x) + (digitToInt y)) : rest
|
||||
else '\\':c:rest
|
||||
_ | isOctDigit c ->
|
||||
let digits = take 3 $ takeWhile isOctDigit (c:cs)
|
||||
num = parseOct digits
|
||||
in (if num < 256 then chr num else '?') : rest
|
||||
_ -> '\\' : c : rest
|
||||
where
|
||||
rest = decodeEscapes cs
|
||||
parseOct = f 0
|
||||
where
|
||||
f n "" = n
|
||||
f n (c:rest) = f (n * 8 + digitToInt c) rest
|
||||
decodeEscapes (c:cs) = c : decodeEscapes cs
|
||||
decodeEscapes [] = []
|
||||
|
||||
-- Is this token a string literal?
|
||||
isLiteral t = isJust $ getLiteralString t
|
||||
|
||||
@@ -217,12 +291,29 @@ getCommand t =
|
||||
T_Redirecting _ _ w -> getCommand w
|
||||
T_SimpleCommand _ _ (w:_) -> return t
|
||||
T_Annotation _ _ t -> getCommand t
|
||||
otherwise -> Nothing
|
||||
_ -> Nothing
|
||||
|
||||
-- Maybe get the command name string of a token representing a command
|
||||
getCommandName :: Token -> Maybe String
|
||||
getCommandName = fst . getCommandNameAndToken
|
||||
|
||||
-- Get the command name token from a command, i.e.
|
||||
-- the token representing 'ls' in 'ls -la 2> foo'.
|
||||
-- If it can't be determined, return the original token.
|
||||
getCommandTokenOrThis = snd . getCommandNameAndToken
|
||||
|
||||
getCommandNameAndToken :: Token -> (Maybe String, Token)
|
||||
getCommandNameAndToken t = fromMaybe (Nothing, t) $ do
|
||||
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
|
||||
s <- getLiteralString w
|
||||
if "busybox" `isSuffixOf` s || "builtin" == s
|
||||
then
|
||||
case rest of
|
||||
(applet:_) -> return (getLiteralString applet, applet)
|
||||
_ -> return (Just s, w)
|
||||
else
|
||||
return (Just s, w)
|
||||
|
||||
-- Maybe get the command name of a token representing a command
|
||||
getCommandName t = do
|
||||
(T_SimpleCommand _ _ (w:_)) <- getCommand t
|
||||
getLiteralString w
|
||||
|
||||
-- If a command substitution is a single command, get its name.
|
||||
-- $(date +%s) = Just "date"
|
||||
@@ -232,13 +323,13 @@ getCommandNameFromExpansion t =
|
||||
T_DollarExpansion _ [c] -> extract c
|
||||
T_Backticked _ [c] -> extract c
|
||||
T_DollarBraceCommandExpansion _ [c] -> extract c
|
||||
otherwise -> Nothing
|
||||
_ -> Nothing
|
||||
where
|
||||
extract (T_Pipeline _ _ [cmd]) = getCommandName cmd
|
||||
extract _ = Nothing
|
||||
|
||||
-- Get the basename of a token representing a command
|
||||
getCommandBasename = liftM basename . getCommandName
|
||||
getCommandBasename = fmap basename . getCommandName
|
||||
where
|
||||
basename = reverse . takeWhile (/= '/') . reverse
|
||||
|
||||
@@ -248,7 +339,7 @@ isAssignment t =
|
||||
T_SimpleCommand _ (w:_) [] -> True
|
||||
T_Assignment {} -> True
|
||||
T_Annotation _ _ w -> isAssignment w
|
||||
otherwise -> False
|
||||
_ -> False
|
||||
|
||||
isOnlyRedirection t =
|
||||
case t of
|
||||
@@ -256,12 +347,15 @@ isOnlyRedirection t =
|
||||
T_Annotation _ _ w -> isOnlyRedirection w
|
||||
T_Redirecting _ (_:_) c -> isOnlyRedirection c
|
||||
T_SimpleCommand _ [] [] -> True
|
||||
otherwise -> False
|
||||
_ -> False
|
||||
|
||||
isFunction t = case t of T_Function {} -> True; _ -> False
|
||||
|
||||
isBraceExpansion t = case t of T_BraceExpansion {} -> True; _ -> False
|
||||
|
||||
-- Get the lists of commands from tokens that contain them, such as
|
||||
-- the body of while loops or branches of if statements.
|
||||
getCommandSequences :: Token -> [[Token]]
|
||||
getCommandSequences t =
|
||||
case t of
|
||||
T_Script _ _ cmds -> [cmds]
|
||||
@@ -272,16 +366,18 @@ getCommandSequences t =
|
||||
T_ForIn _ _ _ cmds -> [cmds]
|
||||
T_ForArithmetic _ _ _ _ cmds -> [cmds]
|
||||
T_IfExpression _ thens elses -> map snd thens ++ [elses]
|
||||
otherwise -> []
|
||||
T_Annotation _ _ t -> getCommandSequences t
|
||||
_ -> []
|
||||
|
||||
-- Get a list of names of associative arrays
|
||||
getAssociativeArrays t =
|
||||
nub . execWriter $ doAnalysis f t
|
||||
where
|
||||
f :: Token -> Writer [String] ()
|
||||
f t@(T_SimpleCommand {}) = fromMaybe (return ()) $ do
|
||||
f t@T_SimpleCommand {} = fromMaybe (return ()) $ do
|
||||
name <- getCommandName t
|
||||
guard $ name == "declare"
|
||||
let assocNames = ["declare","local","typeset"]
|
||||
guard $ elem name assocNames
|
||||
let flags = getAllFlags t
|
||||
guard $ elem "A" $ map snd flags
|
||||
let args = map fst . filter ((==) "" . snd) $ flags
|
||||
@@ -292,4 +388,105 @@ getAssociativeArrays t =
|
||||
nameAssignments t =
|
||||
case t of
|
||||
T_Assignment _ _ name _ _ -> return name
|
||||
otherwise -> Nothing
|
||||
_ -> Nothing
|
||||
|
||||
-- A Pseudoglob is a wildcard pattern used for checking if a match can succeed.
|
||||
-- For example, [[ $(cmd).jpg == [a-z] ]] will give the patterns *.jpg and ?, which
|
||||
-- can be proven never to match.
|
||||
data PseudoGlob = PGAny | PGMany | PGChar Char
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- Turn a word into a PG pattern, replacing all unknown/runtime values with
|
||||
-- PGMany.
|
||||
wordToPseudoGlob :: Token -> Maybe [PseudoGlob]
|
||||
wordToPseudoGlob word =
|
||||
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
|
||||
where
|
||||
f x = case x of
|
||||
T_Literal _ s -> return $ map PGChar s
|
||||
T_SingleQuoted _ s -> return $ map PGChar s
|
||||
|
||||
T_DollarBraced {} -> return [PGMany]
|
||||
T_DollarExpansion {} -> return [PGMany]
|
||||
T_Backticked {} -> return [PGMany]
|
||||
|
||||
T_Glob _ "?" -> return [PGAny]
|
||||
T_Glob _ ('[':_) -> return [PGAny]
|
||||
T_Glob {} -> return [PGMany]
|
||||
|
||||
T_Extglob {} -> return [PGMany]
|
||||
|
||||
_ -> return [PGMany]
|
||||
|
||||
-- Turn a word into a PG pattern, but only if we can preserve
|
||||
-- exact semantics.
|
||||
wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob]
|
||||
wordToExactPseudoGlob word =
|
||||
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
|
||||
where
|
||||
f x = case x of
|
||||
T_Literal _ s -> return $ map PGChar s
|
||||
T_SingleQuoted _ s -> return $ map PGChar s
|
||||
T_Glob _ "?" -> return [PGAny]
|
||||
T_Glob _ "*" -> return [PGMany]
|
||||
_ -> fail "Unknown token type"
|
||||
|
||||
-- Reorder a PseudoGlob for more efficient matching, e.g.
|
||||
-- f?*?**g -> f??*g
|
||||
simplifyPseudoGlob :: [PseudoGlob] -> [PseudoGlob]
|
||||
simplifyPseudoGlob = f
|
||||
where
|
||||
f [] = []
|
||||
f (x@(PGChar _) : rest ) = x : f rest
|
||||
f list =
|
||||
let (anys, rest) = span (\x -> x == PGMany || x == PGAny) list in
|
||||
order anys ++ f rest
|
||||
|
||||
order s = let (any, many) = partition (== PGAny) s in
|
||||
any ++ take 1 many
|
||||
|
||||
-- Check whether the two patterns can ever overlap.
|
||||
pseudoGlobsCanOverlap :: [PseudoGlob] -> [PseudoGlob] -> Bool
|
||||
pseudoGlobsCanOverlap = matchable
|
||||
where
|
||||
matchable x@(xf:xs) y@(yf:ys) =
|
||||
case (xf, yf) of
|
||||
(PGMany, _) -> matchable x ys || matchable xs y
|
||||
(_, PGMany) -> matchable x ys || matchable xs y
|
||||
(PGAny, _) -> matchable xs ys
|
||||
(_, PGAny) -> matchable xs ys
|
||||
(_, _) -> xf == yf && matchable xs ys
|
||||
|
||||
matchable [] [] = True
|
||||
matchable (PGMany : rest) [] = matchable rest []
|
||||
matchable (_:_) [] = False
|
||||
matchable [] r = matchable r []
|
||||
|
||||
-- Check whether the first pattern always overlaps the second.
|
||||
pseudoGlobIsSuperSetof :: [PseudoGlob] -> [PseudoGlob] -> Bool
|
||||
pseudoGlobIsSuperSetof = matchable
|
||||
where
|
||||
matchable x@(xf:xs) y@(yf:ys) =
|
||||
case (xf, yf) of
|
||||
(PGMany, PGMany) -> matchable x ys
|
||||
(PGMany, _) -> matchable x ys || matchable xs y
|
||||
(_, PGMany) -> False
|
||||
(PGAny, _) -> matchable xs ys
|
||||
(_, PGAny) -> False
|
||||
(_, _) -> xf == yf && matchable xs ys
|
||||
|
||||
matchable [] [] = True
|
||||
matchable (PGMany : rest) [] = matchable rest []
|
||||
matchable _ _ = False
|
||||
|
||||
wordsCanBeEqual x y = fromMaybe True $
|
||||
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
|
||||
|
||||
-- Is this an expansion that can be quoted,
|
||||
-- e.g. $(foo) `foo` $foo (but not {foo,})?
|
||||
isQuoteableExpansion t = case t of
|
||||
T_DollarExpansion {} -> True
|
||||
T_DollarBraceCommandExpansion {} -> True
|
||||
T_Backticked {} -> True
|
||||
T_DollarBraced {} -> True
|
||||
_ -> False
|
3185
src/ShellCheck/Analytics.hs
Normal file
3185
src/ShellCheck/Analytics.hs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Analyzer (analyzeScript) where
|
||||
|
||||
@@ -23,14 +23,23 @@ import ShellCheck.Analytics
|
||||
import ShellCheck.AnalyzerLib
|
||||
import ShellCheck.Interface
|
||||
import Data.List
|
||||
import Data.Monoid
|
||||
import qualified ShellCheck.Checks.Commands
|
||||
import qualified ShellCheck.Checks.ShellSupport
|
||||
|
||||
|
||||
-- TODO: Clean up the cruft this is layered on
|
||||
analyzeScript :: AnalysisSpec -> AnalysisResult
|
||||
analyzeScript spec = AnalysisResult {
|
||||
analyzeScript spec = newAnalysisResult {
|
||||
arComments =
|
||||
filterByAnnotation (asScript spec) . nub $
|
||||
filterByAnnotation spec params . nub $
|
||||
runAnalytics spec
|
||||
++ ShellCheck.Checks.Commands.runChecks spec
|
||||
++ runChecker params (checkers params)
|
||||
}
|
||||
where
|
||||
params = makeParameters spec
|
||||
|
||||
checkers params = mconcat $ map ($ params) [
|
||||
ShellCheck.Checks.Commands.checker,
|
||||
ShellCheck.Checks.ShellSupport.checker
|
||||
]
|
932
src/ShellCheck/AnalyzerLib.hs
Normal file
932
src/ShellCheck/AnalyzerLib.hs
Normal file
@@ -0,0 +1,932 @@
|
||||
{-
|
||||
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/>.
|
||||
-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
module ShellCheck.AnalyzerLib where
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Arrow (first)
|
||||
import Control.DeepSeq
|
||||
import Control.Monad.Identity
|
||||
import Control.Monad.RWS
|
||||
import Control.Monad.State
|
||||
import Control.Monad.Writer
|
||||
import Data.Char
|
||||
import Data.List
|
||||
import qualified Data.Map as Map
|
||||
import Data.Maybe
|
||||
import Data.Semigroup
|
||||
|
||||
prop :: Bool -> IO ()
|
||||
prop False = putStrLn "FAIL"
|
||||
prop True = return ()
|
||||
|
||||
type Analysis = AnalyzerM ()
|
||||
type AnalyzerM a = RWS Parameters [TokenComment] Cache a
|
||||
nullCheck = const $ return ()
|
||||
|
||||
|
||||
data Checker = Checker {
|
||||
perScript :: Root -> Analysis,
|
||||
perToken :: Token -> Analysis
|
||||
}
|
||||
|
||||
runChecker :: Parameters -> Checker -> [TokenComment]
|
||||
runChecker params checker = notes
|
||||
where
|
||||
root = rootNode params
|
||||
check = perScript checker `composeAnalyzers` (\(Root x) -> void $ doAnalysis (perToken checker) x)
|
||||
notes = snd $ evalRWS (check $ Root root) params Cache
|
||||
|
||||
instance Semigroup Checker where
|
||||
(<>) x y = Checker {
|
||||
perScript = perScript x `composeAnalyzers` perScript y,
|
||||
perToken = perToken x `composeAnalyzers` perToken y
|
||||
}
|
||||
|
||||
instance Monoid Checker where
|
||||
mempty = Checker {
|
||||
perScript = nullCheck,
|
||||
perToken = nullCheck
|
||||
}
|
||||
mappend = (Data.Semigroup.<>)
|
||||
|
||||
composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis
|
||||
composeAnalyzers f g x = f x >> g x
|
||||
|
||||
data Parameters = Parameters {
|
||||
hasLastpipe :: Bool, -- Whether this script has the 'lastpipe' option set/default.
|
||||
hasSetE :: Bool, -- Whether this script has 'set -e' anywhere.
|
||||
variableFlow :: [StackData], -- A linear (bad) analysis of data flow
|
||||
parentMap :: Map.Map Id Token, -- A map from Id to parent Token
|
||||
shellType :: Shell, -- The shell type, such as Bash or Ksh
|
||||
shellTypeSpecified :: Bool, -- True if shell type was forced via flags
|
||||
rootNode :: Token, -- The root node of the AST
|
||||
tokenPositions :: Map.Map Id (Position, Position) -- map from token id to start and end position
|
||||
} deriving (Show)
|
||||
|
||||
-- TODO: Cache results of common AST ops here
|
||||
data Cache = Cache {}
|
||||
|
||||
data Scope = SubshellScope String | NoneScope deriving (Show, Eq)
|
||||
data StackData =
|
||||
StackScope Scope
|
||||
| StackScopeEnd
|
||||
-- (Base expression, specific position, var name, assigned values)
|
||||
| Assignment (Token, Token, String, DataType)
|
||||
| Reference (Token, Token, String)
|
||||
deriving (Show)
|
||||
|
||||
data DataType = DataString DataSource | DataArray DataSource
|
||||
deriving (Show)
|
||||
|
||||
data DataSource =
|
||||
SourceFrom [Token]
|
||||
| SourceExternal
|
||||
| SourceDeclaration
|
||||
| SourceInteger
|
||||
| SourceChecked
|
||||
deriving (Show)
|
||||
|
||||
data VariableState = Dead Token String | Alive deriving (Show)
|
||||
|
||||
defaultSpec pr = spec {
|
||||
asShellType = Nothing,
|
||||
asCheckSourced = False,
|
||||
asExecutionMode = Executed,
|
||||
asTokenPositions = prTokenPositions pr
|
||||
} where spec = newAnalysisSpec (fromJust $ prRoot pr)
|
||||
|
||||
pScript s =
|
||||
let
|
||||
pSpec = newParseSpec {
|
||||
psFilename = "script",
|
||||
psScript = s
|
||||
}
|
||||
in runIdentity $ parseScript (mockedSystemInterface []) pSpec
|
||||
|
||||
-- For testing. If parsed, returns whether there are any comments
|
||||
producesComments :: Checker -> String -> Maybe Bool
|
||||
producesComments c s = do
|
||||
let pr = pScript s
|
||||
prRoot pr
|
||||
let spec = defaultSpec pr
|
||||
let params = makeParameters spec
|
||||
return . not . null $ runChecker params c
|
||||
|
||||
makeComment :: Severity -> Id -> Code -> String -> TokenComment
|
||||
makeComment severity id code note =
|
||||
newTokenComment {
|
||||
tcId = id,
|
||||
tcComment = newComment {
|
||||
cSeverity = severity,
|
||||
cCode = code,
|
||||
cMessage = note
|
||||
}
|
||||
}
|
||||
|
||||
addComment note = note `deepseq` tell [note]
|
||||
|
||||
warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m ()
|
||||
warn id code str = addComment $ makeComment WarningC id code str
|
||||
err id code str = addComment $ makeComment ErrorC id code str
|
||||
info id code str = addComment $ makeComment InfoC id code str
|
||||
style id code str = addComment $ makeComment StyleC id code str
|
||||
|
||||
warnWithFix id code str fix = addComment $
|
||||
let comment = makeComment WarningC id code str in
|
||||
comment {
|
||||
tcFix = Just fix
|
||||
}
|
||||
|
||||
makeCommentWithFix :: Severity -> Id -> Code -> String -> Fix -> TokenComment
|
||||
makeCommentWithFix severity id code str fix =
|
||||
let comment = makeComment severity id code str
|
||||
withFix = comment {
|
||||
tcFix = Just fix
|
||||
}
|
||||
in withFix `deepseq` withFix
|
||||
|
||||
makeParameters spec =
|
||||
let params = Parameters {
|
||||
rootNode = root,
|
||||
shellType = fromMaybe (determineShell root) $ asShellType spec,
|
||||
hasSetE = containsSetE root,
|
||||
hasLastpipe =
|
||||
case shellType params of
|
||||
Bash -> containsLastpipe root
|
||||
Dash -> False
|
||||
Sh -> False
|
||||
Ksh -> True,
|
||||
|
||||
shellTypeSpecified = isJust $ asShellType spec,
|
||||
parentMap = getParentTree root,
|
||||
variableFlow = getVariableFlow params root,
|
||||
tokenPositions = asTokenPositions spec
|
||||
} in params
|
||||
where root = asScript spec
|
||||
|
||||
|
||||
-- Does this script mention 'set -e' anywhere?
|
||||
-- Used as a hack to disable certain warnings.
|
||||
containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
|
||||
where
|
||||
isSetE t =
|
||||
case t of
|
||||
T_Script _ str _ -> str `matches` re
|
||||
T_SimpleCommand {} ->
|
||||
t `isUnqualifiedCommand` "set" &&
|
||||
("errexit" `elem` oversimplify t ||
|
||||
"e" `elem` map snd (getAllFlags t))
|
||||
_ -> False
|
||||
re = mkRegex "[[:space:]]-[^-]*e"
|
||||
|
||||
-- Does this script mention 'shopt -s lastpipe' anywhere?
|
||||
-- Also used as a hack.
|
||||
containsLastpipe root =
|
||||
isNothing $ doAnalysis (guard . not . isShoptLastPipe) root
|
||||
where
|
||||
isShoptLastPipe t =
|
||||
case t of
|
||||
T_SimpleCommand {} ->
|
||||
t `isUnqualifiedCommand` "shopt" &&
|
||||
("lastpipe" `elem` oversimplify t)
|
||||
_ -> False
|
||||
|
||||
|
||||
-- |
|
||||
-- >>> prop $ determineShellTest "#!/bin/sh" == Sh
|
||||
-- >>> prop $ determineShellTest "#!/usr/bin/env ksh" == Ksh
|
||||
-- >>> prop $ determineShellTest "" == Bash
|
||||
-- >>> prop $ determineShellTest "#!/bin/sh -e" == Sh
|
||||
-- >>> prop $ determineShellTest "#!/bin/ksh\n#shellcheck shell=sh\nfoo" == Sh
|
||||
-- >>> prop $ determineShellTest "#shellcheck shell=sh\nfoo" == Sh
|
||||
-- >>> prop $ determineShellTest "#! /bin/sh" == Sh
|
||||
-- >>> prop $ determineShellTest "#! /bin/ash" == Dash
|
||||
determineShellTest = determineShell . fromJust . prRoot . pScript
|
||||
determineShell t = fromMaybe Bash $ do
|
||||
shellString <- foldl mplus Nothing $ getCandidates t
|
||||
shellForExecutable shellString
|
||||
where
|
||||
forAnnotation t =
|
||||
case t of
|
||||
(ShellOverride s) -> return s
|
||||
_ -> fail ""
|
||||
getCandidates :: Token -> [Maybe String]
|
||||
getCandidates t@T_Script {} = [Just $ fromShebang t]
|
||||
getCandidates (T_Annotation _ annotations s) =
|
||||
map forAnnotation annotations ++
|
||||
[Just $ fromShebang s]
|
||||
fromShebang (T_Script _ s t) = executableFromShebang s
|
||||
|
||||
-- Given a string like "/bin/bash" or "/usr/bin/env dash",
|
||||
-- return the shell basename like "bash" or "dash"
|
||||
executableFromShebang :: String -> String
|
||||
executableFromShebang = shellFor
|
||||
where
|
||||
shellFor s | "/env " `isInfixOf` s = head (drop 1 (words s)++[""])
|
||||
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
|
||||
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
|
||||
|
||||
|
||||
|
||||
-- Given a root node, make a map from Id to parent Token.
|
||||
-- This is used to populate parentMap in Parameters
|
||||
getParentTree :: Token -> Map.Map Id Token
|
||||
getParentTree t =
|
||||
snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
|
||||
where
|
||||
pre t = modify (first ((:) t))
|
||||
post t = do
|
||||
(x, map) <- get
|
||||
case x of
|
||||
_:rest -> case rest of [] -> put (rest, map)
|
||||
(x:_) -> put (rest, Map.insert (getId t) x map)
|
||||
|
||||
-- Given a root node, make a map from Id to Token
|
||||
getTokenMap :: Token -> Map.Map Id Token
|
||||
getTokenMap t =
|
||||
execState (doAnalysis f t) Map.empty
|
||||
where
|
||||
f t = modify (Map.insert (getId t) t)
|
||||
|
||||
|
||||
-- Is this token in a quoting free context? (i.e. would variable expansion split)
|
||||
-- True: Assignments, [[ .. ]], here docs, already in double quotes
|
||||
-- False: Regular words
|
||||
isStrictlyQuoteFree = isQuoteFreeNode True
|
||||
|
||||
-- Like above, but also allow some cases where splitting may be desired.
|
||||
-- True: Like above + for loops
|
||||
-- False: Like above
|
||||
isQuoteFree = isQuoteFreeNode False
|
||||
|
||||
|
||||
isQuoteFreeNode strict tree t =
|
||||
(isQuoteFreeElement t == Just True) ||
|
||||
head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False])
|
||||
where
|
||||
-- Is this node self-quoting in itself?
|
||||
isQuoteFreeElement t =
|
||||
case t of
|
||||
T_Assignment {} -> return True
|
||||
T_FdRedirect {} -> return True
|
||||
_ -> Nothing
|
||||
|
||||
-- Are any subnodes inherently self-quoting?
|
||||
isQuoteFreeContext t =
|
||||
case t of
|
||||
TC_Nullary _ DoubleBracket _ -> return True
|
||||
TC_Unary _ DoubleBracket _ _ -> return True
|
||||
TC_Binary _ DoubleBracket _ _ _ -> return True
|
||||
TA_Sequence {} -> return True
|
||||
T_Arithmetic {} -> return True
|
||||
T_Assignment {} -> return True
|
||||
T_Redirecting {} -> return False
|
||||
T_DoubleQuoted _ _ -> return True
|
||||
T_DollarDoubleQuoted _ _ -> return True
|
||||
T_CaseExpression {} -> return True
|
||||
T_HereDoc {} -> return True
|
||||
T_DollarBraced {} -> return True
|
||||
-- When non-strict, pragmatically assume it's desirable to split here
|
||||
T_ForIn {} -> return (not strict)
|
||||
T_SelectIn {} -> return (not strict)
|
||||
_ -> Nothing
|
||||
|
||||
-- Check if a token is a parameter to a certain command by name:
|
||||
-- Example: isParamTo (parentMap params) "sed" t
|
||||
isParamTo :: Map.Map Id Token -> String -> Token -> Bool
|
||||
isParamTo tree cmd =
|
||||
go
|
||||
where
|
||||
go x = case Map.lookup (getId x) tree of
|
||||
Nothing -> False
|
||||
Just parent -> check parent
|
||||
check t =
|
||||
case t of
|
||||
T_SingleQuoted _ _ -> go t
|
||||
T_DoubleQuoted _ _ -> go t
|
||||
T_NormalWord _ _ -> go t
|
||||
T_SimpleCommand {} -> isCommand t cmd
|
||||
T_Redirecting {} -> isCommand t cmd
|
||||
_ -> False
|
||||
|
||||
-- Get the parent command (T_Redirecting) of a Token, if any.
|
||||
getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token
|
||||
getClosestCommand tree t =
|
||||
findFirst findCommand $ getPath tree t
|
||||
where
|
||||
findCommand t =
|
||||
case t of
|
||||
T_Redirecting {} -> return True
|
||||
T_Script {} -> return False
|
||||
_ -> Nothing
|
||||
|
||||
-- Like above, if koala_man knew Haskell when starting this project.
|
||||
getClosestCommandM t = do
|
||||
tree <- asks parentMap
|
||||
return $ getClosestCommand tree t
|
||||
|
||||
-- Is the token used as a command name (the first word in a T_SimpleCommand)?
|
||||
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
||||
where
|
||||
go currentId (T_NormalWord id [word]:rest)
|
||||
| currentId == getId word = go id rest
|
||||
go currentId (T_DoubleQuoted id [word]:rest)
|
||||
| currentId == getId word = go id rest
|
||||
go currentId (T_SimpleCommand _ _ (word:_):_)
|
||||
| currentId == getId word = True
|
||||
go _ _ = False
|
||||
|
||||
-- A list of the element and all its parents up to the root node.
|
||||
getPath tree t = t :
|
||||
case Map.lookup (getId t) tree of
|
||||
Nothing -> []
|
||||
Just parent -> getPath tree parent
|
||||
|
||||
-- Version of the above taking the map from the current context
|
||||
-- Todo: give this the name "getPath"
|
||||
getPathM t = do
|
||||
map <- asks parentMap
|
||||
return $ getPath map t
|
||||
|
||||
isParentOf tree parent child =
|
||||
elem (getId parent) . map getId $ getPath tree child
|
||||
|
||||
parents params = getPath (parentMap params)
|
||||
|
||||
pathTo t = do
|
||||
parents <- reader parentMap
|
||||
return $ getPath parents t
|
||||
|
||||
-- Find the first match in a list where the predicate is Just True.
|
||||
-- Stops if it's Just False and ignores Nothing.
|
||||
findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a
|
||||
findFirst p l =
|
||||
case l of
|
||||
[] -> Nothing
|
||||
(x:xs) ->
|
||||
case p x of
|
||||
Just True -> return x
|
||||
Just False -> Nothing
|
||||
Nothing -> findFirst p xs
|
||||
|
||||
-- Check whether a word is entirely output from a single command
|
||||
tokenIsJustCommandOutput t = case t of
|
||||
T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds
|
||||
T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ cmds]] -> check cmds
|
||||
T_NormalWord id [T_Backticked _ cmds] -> check cmds
|
||||
T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ cmds]] -> check cmds
|
||||
_ -> False
|
||||
where
|
||||
check [x] = not $ isOnlyRedirection x
|
||||
check _ = False
|
||||
|
||||
-- TODO: Replace this with a proper Control Flow Graph
|
||||
getVariableFlow params t =
|
||||
let (_, stack) = runState (doStackAnalysis startScope endScope t) []
|
||||
in reverse stack
|
||||
where
|
||||
startScope t =
|
||||
let scopeType = leadType params t
|
||||
in do
|
||||
when (scopeType /= NoneScope) $ modify (StackScope scopeType:)
|
||||
when (assignFirst t) $ setWritten t
|
||||
|
||||
endScope t =
|
||||
let scopeType = leadType params t
|
||||
in do
|
||||
setRead t
|
||||
unless (assignFirst t) $ setWritten t
|
||||
when (scopeType /= NoneScope) $ modify (StackScopeEnd:)
|
||||
|
||||
assignFirst T_ForIn {} = True
|
||||
assignFirst T_SelectIn {} = True
|
||||
assignFirst _ = False
|
||||
|
||||
setRead t =
|
||||
let read = getReferencedVariables (parentMap params) t
|
||||
in mapM_ (\v -> modify (Reference v:)) read
|
||||
|
||||
setWritten t =
|
||||
let written = getModifiedVariables t
|
||||
in mapM_ (\v -> modify (Assignment v:)) written
|
||||
|
||||
|
||||
leadType params t =
|
||||
case t of
|
||||
T_DollarExpansion _ _ -> SubshellScope "$(..) expansion"
|
||||
T_Backticked _ _ -> SubshellScope "`..` expansion"
|
||||
T_Backgrounded _ _ -> SubshellScope "backgrounding &"
|
||||
T_Subshell _ _ -> SubshellScope "(..) group"
|
||||
T_CoProcBody _ _ -> SubshellScope "coproc"
|
||||
T_Redirecting {} ->
|
||||
if fromMaybe False causesSubshell
|
||||
then SubshellScope "pipeline"
|
||||
else NoneScope
|
||||
_ -> NoneScope
|
||||
where
|
||||
parentPipeline = do
|
||||
parent <- Map.lookup (getId t) (parentMap params)
|
||||
case parent of
|
||||
T_Pipeline {} -> return parent
|
||||
_ -> Nothing
|
||||
|
||||
causesSubshell = do
|
||||
(T_Pipeline _ _ list) <- parentPipeline
|
||||
if length list <= 1
|
||||
then return False
|
||||
else if not $ hasLastpipe params
|
||||
then return True
|
||||
else return . not $ (getId . head $ reverse list) == getId t
|
||||
|
||||
getModifiedVariables t =
|
||||
case t of
|
||||
T_SimpleCommand _ vars [] ->
|
||||
concatMap (\x -> case x of
|
||||
T_Assignment id _ name _ w ->
|
||||
[(x, x, name, dataTypeFrom DataString w)]
|
||||
_ -> []
|
||||
) vars
|
||||
c@T_SimpleCommand {} ->
|
||||
getModifiedVariableCommand c
|
||||
|
||||
TA_Unary _ "++|" v@(TA_Variable _ name _) ->
|
||||
[(t, v, name, DataString $ SourceFrom [v])]
|
||||
TA_Unary _ "|++" v@(TA_Variable _ name _) ->
|
||||
[(t, v, name, DataString $ SourceFrom [v])]
|
||||
TA_Assignment _ op (TA_Variable _ name _) rhs -> maybeToList $ do
|
||||
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
||||
return (t, t, name, DataString $ SourceFrom [rhs])
|
||||
|
||||
-- Count [[ -v foo ]] as an "assignment".
|
||||
-- This is to prevent [ -v foo ] being unassigned or unused.
|
||||
TC_Unary id _ "-v" token -> maybeToList $ do
|
||||
str <- fmap (takeWhile (/= '[')) $ -- Quoted index
|
||||
flip getLiteralStringExt token $ \x ->
|
||||
case x of
|
||||
T_Glob _ s -> return s -- Unquoted index
|
||||
_ -> Nothing
|
||||
|
||||
guard . not . null $ str
|
||||
return (t, token, str, DataString SourceChecked)
|
||||
|
||||
T_DollarBraced _ l -> maybeToList $ do
|
||||
let string = bracedString t
|
||||
let modifier = getBracedModifier string
|
||||
guard $ ":=" `isPrefixOf` modifier
|
||||
return (t, t, getBracedReference string, DataString $ SourceFrom [l])
|
||||
|
||||
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo
|
||||
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
|
||||
|
||||
t@(T_CoProc _ name _) ->
|
||||
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
|
||||
|
||||
--Points to 'for' rather than variable
|
||||
T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)]
|
||||
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
|
||||
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
|
||||
_ -> []
|
||||
|
||||
isClosingFileOp op =
|
||||
case op of
|
||||
T_IoDuplicate _ (T_GREATAND _) "-" -> True
|
||||
T_IoDuplicate _ (T_LESSAND _) "-" -> True
|
||||
_ -> False
|
||||
|
||||
|
||||
-- Consider 'export/declare -x' a reference, since it makes the var available
|
||||
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
||||
case x of
|
||||
"export" -> if "f" `elem` flags
|
||||
then []
|
||||
else concatMap getReference rest
|
||||
"declare" -> if
|
||||
any (`elem` flags) ["x", "p"] &&
|
||||
(not $ any (`elem` flags) ["f", "F"])
|
||||
then concatMap getReference rest
|
||||
else []
|
||||
"readonly" ->
|
||||
if any (`elem` flags) ["f", "p"]
|
||||
then []
|
||||
else concatMap getReference rest
|
||||
"trap" ->
|
||||
case rest of
|
||||
head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head
|
||||
_ -> []
|
||||
_ -> []
|
||||
where
|
||||
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
|
||||
getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
|
||||
getReference _ = []
|
||||
flags = map snd $ getAllFlags base
|
||||
|
||||
getReferencedVariableCommand _ = []
|
||||
|
||||
-- The function returns a tuple consisting of four items describing an assignment.
|
||||
-- Given e.g. declare foo=bar
|
||||
-- (
|
||||
-- BaseCommand :: Token, -- The command/structure assigning the variable, i.e. declare foo=bar
|
||||
-- AssignmentToken :: Token, -- The specific part that assigns this variable, i.e. foo=bar
|
||||
-- VariableName :: String, -- The variable name, i.e. foo
|
||||
-- VariableValue :: DataType -- A description of the value being assigned, i.e. "Literal string with value foo"
|
||||
-- )
|
||||
getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
||||
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
|
||||
case x of
|
||||
"read" ->
|
||||
let params = map getLiteral rest
|
||||
readArrayVars = getReadArrayVariables rest
|
||||
in
|
||||
catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params
|
||||
"getopts" ->
|
||||
case rest of
|
||||
opts:var:_ -> maybeToList $ getLiteral var
|
||||
_ -> []
|
||||
|
||||
"let" -> concatMap letParamToLiteral rest
|
||||
|
||||
"export" ->
|
||||
if "f" `elem` flags then [] else concatMap getModifierParamString rest
|
||||
|
||||
"declare" -> if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
|
||||
"typeset" -> declaredVars
|
||||
|
||||
"local" -> concatMap getModifierParamString rest
|
||||
"readonly" ->
|
||||
if any (`elem` flags) ["f", "p"]
|
||||
then []
|
||||
else concatMap getModifierParamString rest
|
||||
"set" -> maybeToList $ do
|
||||
params <- getSetParams rest
|
||||
return (base, base, "@", DataString $ SourceFrom params)
|
||||
|
||||
"printf" -> maybeToList $ getPrintfVariable rest
|
||||
|
||||
"mapfile" -> maybeToList $ getMapfileArray base rest
|
||||
"readarray" -> maybeToList $ getMapfileArray base rest
|
||||
|
||||
_ -> []
|
||||
where
|
||||
flags = map snd $ getAllFlags base
|
||||
stripEquals s = let rest = dropWhile (/= '=') s in
|
||||
if rest == "" then "" else tail rest
|
||||
stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) =
|
||||
T_NormalWord id1 (T_Literal id2 (stripEquals s):rs)
|
||||
stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) =
|
||||
T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
|
||||
stripEqualsFrom t = t
|
||||
|
||||
declaredVars = concatMap (getModifierParam defaultType) rest
|
||||
where
|
||||
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
|
||||
|
||||
getLiteralOfDataType t d = do
|
||||
s <- getLiteralString t
|
||||
when ("-" `isPrefixOf` s) $ fail "argument"
|
||||
return (base, t, s, d)
|
||||
|
||||
getLiteral t = getLiteralOfDataType t (DataString SourceExternal)
|
||||
|
||||
getLiteralArray t = getLiteralOfDataType t (DataArray SourceExternal)
|
||||
|
||||
getModifierParamString = getModifierParam DataString
|
||||
|
||||
getModifierParam def t@(T_Assignment _ _ name _ value) =
|
||||
[(base, t, name, dataTypeFrom def value)]
|
||||
getModifierParam def t@T_NormalWord {} = maybeToList $ do
|
||||
name <- getLiteralString t
|
||||
guard $ isVariableName name
|
||||
return (base, t, name, def SourceDeclaration)
|
||||
getModifierParam _ _ = []
|
||||
|
||||
letParamToLiteral token =
|
||||
if var == ""
|
||||
then []
|
||||
else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])]
|
||||
where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token
|
||||
|
||||
getSetParams (t:_:rest) | getLiteralString t == Just "-o" = getSetParams rest
|
||||
getSetParams (t:rest) =
|
||||
let s = getLiteralString t in
|
||||
case s of
|
||||
Just "--" -> return rest
|
||||
Just ('-':_) -> getSetParams rest
|
||||
_ -> return (t:fromMaybe [] (getSetParams rest))
|
||||
getSetParams [] = Nothing
|
||||
|
||||
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
|
||||
where
|
||||
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, var, DataString $ SourceFrom list)
|
||||
f (_:rest) = f rest
|
||||
f [] = fail "not found"
|
||||
|
||||
-- mapfile has some curious syntax allowing flags plus 0..n variable names
|
||||
-- where only the first non-option one is used if any. Here we cheat and
|
||||
-- just get the last one, if it's a variable name.
|
||||
getMapfileArray base arguments = do
|
||||
lastArg <- listToMaybe (reverse arguments)
|
||||
name <- getLiteralString lastArg
|
||||
guard $ isVariableName name
|
||||
return (base, lastArg, name, DataArray SourceExternal)
|
||||
|
||||
-- get all the array variables used in read, e.g. read -a arr
|
||||
getReadArrayVariables args = do
|
||||
map (getLiteralArray . snd)
|
||||
(filter (\(x,_) -> getLiteralString x == Just "-a") (zip (args) (tail args)))
|
||||
|
||||
getModifiedVariableCommand _ = []
|
||||
|
||||
getIndexReferences s = fromMaybe [] $ do
|
||||
match <- matchRegex re s
|
||||
index <- match !!! 0
|
||||
return $ matchAllStrings variableNameRegex index
|
||||
where
|
||||
re = mkRegex "(\\[.*\\])"
|
||||
|
||||
-- |
|
||||
-- >>> prop $ getOffsetReferences ":bar" == ["bar"]
|
||||
-- >>> prop $ getOffsetReferences ":bar:baz" == ["bar", "baz"]
|
||||
-- >>> prop $ getOffsetReferences "[foo]:bar" == ["bar"]
|
||||
-- >>> prop $ getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
|
||||
getOffsetReferences mods = fromMaybe [] $ do
|
||||
-- if mods start with [, then drop until ]
|
||||
match <- matchRegex re mods
|
||||
offsets <- match !!! 1
|
||||
return $ matchAllStrings variableNameRegex offsets
|
||||
where
|
||||
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
|
||||
|
||||
getReferencedVariables parents t =
|
||||
case t of
|
||||
T_DollarBraced id l -> let str = bracedString t in
|
||||
(t, t, getBracedReference str) :
|
||||
map (\x -> (l, l, x)) (
|
||||
getIndexReferences str
|
||||
++ getOffsetReferences (getBracedModifier str))
|
||||
TA_Variable id name _ ->
|
||||
if isArithmeticAssignment t
|
||||
then []
|
||||
else [(t, t, name)]
|
||||
T_Assignment id mode str _ word ->
|
||||
[(t, t, str) | mode == Append] ++ specialReferences str t word
|
||||
|
||||
TC_Unary id _ "-v" token -> getIfReference t token
|
||||
TC_Unary id _ "-R" token -> getIfReference t token
|
||||
TC_Binary id DoubleBracket op lhs rhs ->
|
||||
if isDereferencing op
|
||||
then concatMap (getIfReference t) [lhs, rhs]
|
||||
else []
|
||||
|
||||
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
|
||||
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
|
||||
x -> getReferencedVariableCommand x
|
||||
where
|
||||
-- Try to reduce false positives for unused vars only referenced from evaluated vars
|
||||
specialReferences name base word =
|
||||
if name `elem` [
|
||||
"PS1", "PS2", "PS3", "PS4",
|
||||
"PROMPT_COMMAND"
|
||||
]
|
||||
then
|
||||
map (\x -> (base, base, x)) $
|
||||
getVariablesFromLiteralToken word
|
||||
else []
|
||||
|
||||
literalizer t = case t of
|
||||
T_Glob _ s -> return s -- Also when parsed as globs
|
||||
_ -> Nothing
|
||||
|
||||
getIfReference context token = maybeToList $ do
|
||||
str <- getLiteralStringExt literalizer token
|
||||
guard . not $ null str
|
||||
when (isDigit $ head str) $ fail "is a number"
|
||||
return (context, token, getBracedReference str)
|
||||
|
||||
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
|
||||
|
||||
isArithmeticAssignment t = case getPath parents t of
|
||||
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
|
||||
_ -> False
|
||||
|
||||
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
|
||||
|
||||
|
||||
--- Command specific checks
|
||||
|
||||
-- Compare a command to a string: t `isCommand` "sed" (also matches /usr/bin/sed)
|
||||
isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `isSuffixOf` cmd)
|
||||
|
||||
-- Compare a command to a literal. Like above, but checks full path.
|
||||
isUnqualifiedCommand token str = isCommandMatch token (== str)
|
||||
|
||||
isCommandMatch token matcher = fromMaybe False $
|
||||
fmap matcher (getCommandName token)
|
||||
|
||||
-- |
|
||||
-- Does this regex look like it was intended as a glob?
|
||||
--
|
||||
-- >>> isConfusedGlobRegex "*foo*"
|
||||
-- True
|
||||
--
|
||||
-- >>> isConfusedGlobRegex ".*foo.*"
|
||||
-- False
|
||||
--
|
||||
isConfusedGlobRegex :: String -> Bool
|
||||
isConfusedGlobRegex ('*':_) = True
|
||||
isConfusedGlobRegex [x,'*'] | x /= '\\' = True
|
||||
isConfusedGlobRegex _ = False
|
||||
|
||||
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
|
||||
isVariableChar x = isVariableStartChar x || isDigit x
|
||||
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
|
||||
|
||||
-- |
|
||||
-- >>> prop $ isVariableName "_fo123"
|
||||
-- >>> prop $ not $ isVariableName "4"
|
||||
-- >>> prop $ not $ isVariableName "test: "
|
||||
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
|
||||
isVariableName _ = False
|
||||
|
||||
getVariablesFromLiteralToken token =
|
||||
getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token)
|
||||
|
||||
-- Try to get referenced variables from a literal string like "$foo"
|
||||
-- Ignores tons of cases like arithmetic evaluation and array indices.
|
||||
-- >>> prop $ getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
|
||||
getVariablesFromLiteral string =
|
||||
map (!! 0) $ matchAllSubgroups variableRegex string
|
||||
where
|
||||
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
|
||||
|
||||
-- |
|
||||
-- Get the variable name from an expansion like ${var:-foo}
|
||||
--
|
||||
-- >>> prop $ getBracedReference "foo" == "foo"
|
||||
-- >>> prop $ getBracedReference "#foo" == "foo"
|
||||
-- >>> prop $ getBracedReference "#" == "#"
|
||||
-- >>> prop $ getBracedReference "##" == "#"
|
||||
-- >>> prop $ getBracedReference "#!" == "!"
|
||||
-- >>> prop $ getBracedReference "!#" == "#"
|
||||
-- >>> prop $ getBracedReference "!foo#?" == "foo"
|
||||
-- >>> prop $ getBracedReference "foo-bar" == "foo"
|
||||
-- >>> prop $ getBracedReference "foo:-bar" == "foo"
|
||||
-- >>> prop $ getBracedReference "foo: -1" == "foo"
|
||||
-- >>> prop $ getBracedReference "!os*" == ""
|
||||
-- >>> prop $ getBracedReference "!os?bar**" == ""
|
||||
-- >>> prop $ getBracedReference "foo[bar]" == "foo"
|
||||
getBracedReference s = fromMaybe s $
|
||||
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
|
||||
where
|
||||
noPrefix = dropPrefix s
|
||||
dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest
|
||||
dropPrefix "" = ""
|
||||
takeName s = do
|
||||
let name = takeWhile isVariableChar s
|
||||
guard . not $ null name
|
||||
return name
|
||||
getSpecial (c:_) =
|
||||
if c `elem` "*@#?-$!" then return [c] else fail "not special"
|
||||
getSpecial _ = fail "empty"
|
||||
|
||||
nameExpansion ('!':rest) = do -- e.g. ${!foo*bar*}
|
||||
let suffix = dropWhile isVariableChar rest
|
||||
guard $ suffix /= rest -- e.g. ${!@}
|
||||
first <- suffix !!! 0
|
||||
guard $ first `elem` "*?"
|
||||
return ""
|
||||
nameExpansion _ = Nothing
|
||||
|
||||
-- |
|
||||
-- >>> prop $ getBracedModifier "foo:bar:baz" == ":bar:baz"
|
||||
-- >>> prop $ getBracedModifier "!var:-foo" == ":-foo"
|
||||
-- >>> prop $ getBracedModifier "foo[bar]" == "[bar]"
|
||||
getBracedModifier s = fromMaybe "" . listToMaybe $ do
|
||||
let var = getBracedReference s
|
||||
a <- dropModifier s
|
||||
dropPrefix var a
|
||||
where
|
||||
dropPrefix [] t = return t
|
||||
dropPrefix (a:b) (c:d) | a == c = dropPrefix b d
|
||||
dropPrefix _ _ = []
|
||||
|
||||
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
|
||||
dropModifier x = [x]
|
||||
|
||||
-- Useful generic functions.
|
||||
|
||||
-- Run an action in a Maybe (or do nothing).
|
||||
-- Example:
|
||||
--
|
||||
-- @
|
||||
-- potentially $ do
|
||||
-- s <- getLiteralString cmd
|
||||
-- guard $ s `elem` ["--recursive", "-r"]
|
||||
-- return $ warn .. "Something something recursive"
|
||||
-- @
|
||||
potentially :: Monad m => Maybe (m ()) -> m ()
|
||||
potentially = fromMaybe (return ())
|
||||
|
||||
-- Get element 0 or a default. Like `head` but safe.
|
||||
headOrDefault _ (a:_) = a
|
||||
headOrDefault def _ = def
|
||||
|
||||
--- Get element n of a list, or Nothing. Like `!!` but safe.
|
||||
(!!!) list i =
|
||||
case drop i list of
|
||||
[] -> Nothing
|
||||
(r:_) -> Just r
|
||||
|
||||
-- Run a command if the shell is in the given list
|
||||
whenShell l c = do
|
||||
shell <- asks shellType
|
||||
when (shell `elem` l ) c
|
||||
|
||||
|
||||
filterByAnnotation asSpec params =
|
||||
filter (not . shouldIgnore)
|
||||
where
|
||||
token = asScript asSpec
|
||||
shouldIgnore note =
|
||||
any (shouldIgnoreFor (getCode note)) $
|
||||
getPath parents (T_Bang $ tcId note)
|
||||
shouldIgnoreFor num (T_Annotation _ anns _) =
|
||||
any hasNum anns
|
||||
where
|
||||
hasNum (DisableComment ts) = num == ts
|
||||
hasNum _ = False
|
||||
shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec
|
||||
shouldIgnoreFor _ _ = False
|
||||
parents = parentMap params
|
||||
getCode = cCode . tcComment
|
||||
|
||||
-- Is this a ${#anything}, to get string length or array count?
|
||||
isCountingReference (T_DollarBraced id token) =
|
||||
case concat $ oversimplify token of
|
||||
'#':_ -> True
|
||||
_ -> False
|
||||
isCountingReference _ = False
|
||||
|
||||
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
|
||||
isQuotedAlternativeReference t =
|
||||
case t of
|
||||
T_DollarBraced _ _ ->
|
||||
getBracedModifier (bracedString t) `matches` re
|
||||
_ -> False
|
||||
where
|
||||
re = mkRegex "(^|\\]):?\\+"
|
||||
|
||||
-- getGnuOpts "erd:u:" will parse a SimpleCommand like
|
||||
-- read -re -d : -u 3 bar
|
||||
-- into
|
||||
-- Just [("r", -re), ("e", -re), ("d", :), ("u", 3), ("", bar)]
|
||||
-- where flags with arguments map to arguments, while others map to themselves.
|
||||
-- Any unrecognized flag will result in Nothing.
|
||||
getGnuOpts = getOpts getAllFlags
|
||||
getBsdOpts = getOpts getLeadingFlags
|
||||
getOpts :: (Token -> [(Token, String)]) -> String -> Token -> Maybe [(String, Token)]
|
||||
getOpts flagTokenizer string cmd = process flags
|
||||
where
|
||||
flags = flagTokenizer cmd
|
||||
flagList (c:':':rest) = ([c], True) : flagList rest
|
||||
flagList (c:rest) = ([c], False) : flagList rest
|
||||
flagList [] = []
|
||||
flagMap = Map.fromList $ ("", False) : flagList string
|
||||
|
||||
process [] = return []
|
||||
process [(token, flag)] = do
|
||||
takesArg <- Map.lookup flag flagMap
|
||||
guard $ not takesArg
|
||||
return [(flag, token)]
|
||||
process ((token1, flag1):rest2@((token2, flag2):rest)) = do
|
||||
takesArg <- Map.lookup flag1 flagMap
|
||||
if takesArg
|
||||
then do
|
||||
guard $ flag2 == ""
|
||||
more <- process rest
|
||||
return $ (flag1, token2) : more
|
||||
else do
|
||||
more <- process rest2
|
||||
return $ (flag1, token1) : more
|
253
src/ShellCheck/Checker.hs
Normal file
253
src/ShellCheck/Checker.hs
Normal file
@@ -0,0 +1,253 @@
|
||||
{-
|
||||
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.Checker (checkScript) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Analyzer
|
||||
|
||||
import Data.Either
|
||||
import Data.Functor
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import Data.Ord
|
||||
import Control.Monad.Identity
|
||||
import qualified Data.Map as Map
|
||||
import qualified System.IO
|
||||
import Prelude hiding (readFile)
|
||||
import Control.Monad
|
||||
|
||||
tokenToPosition startMap t = fromMaybe fail $ do
|
||||
span <- Map.lookup (tcId t) startMap
|
||||
return $ newPositionedComment {
|
||||
pcStartPos = fst span,
|
||||
pcEndPos = snd span,
|
||||
pcComment = tcComment t,
|
||||
pcFix = tcFix t
|
||||
}
|
||||
where
|
||||
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
|
||||
|
||||
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
|
||||
checkScript sys spec = do
|
||||
results <- checkScript (csScript spec)
|
||||
return emptyCheckResult {
|
||||
crFilename = csFilename spec,
|
||||
crComments = results
|
||||
}
|
||||
where
|
||||
checkScript contents = do
|
||||
result <- parseScript sys newParseSpec {
|
||||
psFilename = csFilename spec,
|
||||
psScript = contents,
|
||||
psCheckSourced = csCheckSourced spec,
|
||||
psShellTypeOverride = csShellTypeOverride spec
|
||||
}
|
||||
let parseMessages = prComments result
|
||||
let tokenPositions = prTokenPositions result
|
||||
let analysisSpec root =
|
||||
as {
|
||||
asScript = root,
|
||||
asShellType = csShellTypeOverride spec,
|
||||
asCheckSourced = csCheckSourced spec,
|
||||
asExecutionMode = Executed,
|
||||
asTokenPositions = tokenPositions
|
||||
} where as = newAnalysisSpec root
|
||||
let analysisMessages =
|
||||
fromMaybe [] $
|
||||
(arComments . analyzeScript . analysisSpec)
|
||||
<$> prRoot result
|
||||
let translator = tokenToPosition tokenPositions
|
||||
return . nub . sortMessages . filter shouldInclude $
|
||||
(parseMessages ++ map translator analysisMessages)
|
||||
|
||||
shouldInclude pc =
|
||||
let code = cCode (pcComment pc)
|
||||
severity = cSeverity (pcComment pc)
|
||||
in
|
||||
code `notElem` csExcludedWarnings spec &&
|
||||
severity <= csMinSeverity spec
|
||||
|
||||
sortMessages = sortBy (comparing order)
|
||||
order pc =
|
||||
let pos = pcStartPos pc
|
||||
comment = pcComment pc in
|
||||
(posFile pos,
|
||||
posLine pos,
|
||||
posColumn pos,
|
||||
cSeverity comment,
|
||||
cCode comment,
|
||||
cMessage comment)
|
||||
getPosition = pcStartPos
|
||||
|
||||
|
||||
getErrors sys spec =
|
||||
sort . map getCode . crComments $
|
||||
runIdentity (checkScript sys spec)
|
||||
where
|
||||
getCode = cCode . pcComment
|
||||
|
||||
check = checkWithIncludes []
|
||||
|
||||
checkWithSpec includes =
|
||||
getErrors (mockedSystemInterface includes)
|
||||
|
||||
checkWithIncludes includes src =
|
||||
checkWithSpec includes emptyCheckSpec {
|
||||
csScript = src,
|
||||
csExcludedWarnings = [2148]
|
||||
}
|
||||
|
||||
checkRecursive includes src =
|
||||
checkWithSpec includes emptyCheckSpec {
|
||||
csScript = src,
|
||||
csExcludedWarnings = [2148],
|
||||
csCheckSourced = True
|
||||
}
|
||||
|
||||
-- | Dummy binding for doctest to run
|
||||
--
|
||||
-- >>> check "echo \"$12\""
|
||||
-- [1037]
|
||||
--
|
||||
-- >>> check "#shellcheck disable=SC1037\necho \"$12\""
|
||||
-- []
|
||||
--
|
||||
-- >>> check "#shellcheck disable=SC1037\n#lol\necho \"$12\""
|
||||
-- []
|
||||
--
|
||||
-- >>> check "echo $1"
|
||||
-- [2086]
|
||||
--
|
||||
-- >>> check "#shellcheck disable=SC2086\necho $1"
|
||||
-- []
|
||||
--
|
||||
-- >>> check "#shellcheck disable=SC2086\n#lol\necho $1"
|
||||
-- []
|
||||
--
|
||||
-- >>> :{
|
||||
-- getErrors
|
||||
-- (mockedSystemInterface [])
|
||||
-- emptyCheckSpec {
|
||||
-- csScript = "echo $1",
|
||||
-- csExcludedWarnings = [2148, 2086]
|
||||
-- }
|
||||
-- :}
|
||||
-- []
|
||||
--
|
||||
-- >>> :{
|
||||
-- getErrors
|
||||
-- (mockedSystemInterface [])
|
||||
-- emptyCheckSpec {
|
||||
-- csScript = "echo \"$10\"",
|
||||
-- csExcludedWarnings = [2148, 1037]
|
||||
-- }
|
||||
-- :}
|
||||
-- []
|
||||
--
|
||||
-- >>> check "#!/usr/bin/python\ntrue $1\n"
|
||||
-- [1071]
|
||||
--
|
||||
-- >>> :{
|
||||
-- getErrors
|
||||
-- (mockedSystemInterface [])
|
||||
-- emptyCheckSpec {
|
||||
-- csScript = "#!/usr/bin/python\ntrue\n",
|
||||
-- csShellTypeOverride = Just Sh
|
||||
-- }
|
||||
-- :}
|
||||
-- []
|
||||
--
|
||||
-- >>> check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
|
||||
-- []
|
||||
--
|
||||
-- >>> check "source /dev/null"
|
||||
-- []
|
||||
--
|
||||
-- >>> check "source lol; echo \"$bar\""
|
||||
-- [1091,2154]
|
||||
--
|
||||
-- >>> checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\""
|
||||
-- []
|
||||
--
|
||||
-- >>> checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\""
|
||||
-- []
|
||||
--
|
||||
-- >>> checkWithIncludes [("lib", "source lib")] "source lib"
|
||||
-- []
|
||||
--
|
||||
-- >>> checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
|
||||
-- [1094,2086]
|
||||
--
|
||||
-- >>> checkWithIncludes [("lib", "")] ". \"$1\""
|
||||
-- [1090]
|
||||
--
|
||||
-- >>> checkWithIncludes [("lib", "")] "source ~/foo"
|
||||
-- [1090]
|
||||
--
|
||||
-- >>> checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
|
||||
-- []
|
||||
--
|
||||
-- >>> checkRecursive [("lib", "echo $1")] "source lib"
|
||||
-- [2086]
|
||||
--
|
||||
-- >>> checkRecursive [("lib", "echo \"$10\"")] "source lib"
|
||||
-- [1037]
|
||||
--
|
||||
-- >>> checkWithIncludes [("foo", "source bar"), ("bar", "baz=3")] "#shellcheck source=foo\n. \"$1\"; echo \"$baz\""
|
||||
-- []
|
||||
--
|
||||
-- >>> check "#!/bin/sh\necho $1"
|
||||
-- [2086]
|
||||
--
|
||||
-- >>> check "#!/bin/sh\n# shellcheck disable=2086\necho $1"
|
||||
-- []
|
||||
--
|
||||
-- >>> check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
|
||||
-- []
|
||||
--
|
||||
-- >>> check "#!/bin/sh\n#unrelated\n# shellcheck disable=2086\ntrue\necho $1"
|
||||
-- []
|
||||
--
|
||||
-- >>> check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
|
||||
-- []
|
||||
--
|
||||
-- >>> check "#!/bin/sh\n\n\n\n#shellcheck disable=2086\ntrue\necho $1"
|
||||
-- []
|
||||
--
|
||||
-- >>> check "#shellcheck shell=sh\n#unrelated\n#shellcheck disable=2086\ntrue\necho $1"
|
||||
-- []
|
||||
--
|
||||
-- >>> check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
|
||||
-- []
|
||||
--
|
||||
-- check "true\n[ $? == 0 ] && echo $1"
|
||||
-- [2086, 2181]
|
||||
--
|
||||
-- check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
|
||||
-- []
|
||||
--
|
||||
-- >>> 2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
|
||||
-- True
|
||||
--
|
||||
-- >>> check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n"
|
||||
-- []
|
||||
doctests :: ()
|
||||
doctests = ()
|
1068
src/ShellCheck/Checks/Commands.hs
Normal file
1068
src/ShellCheck/Checks/Commands.hs
Normal file
File diff suppressed because it is too large
Load Diff
422
src/ShellCheck/Checks/ShellSupport.hs
Normal file
422
src/ShellCheck/Checks/ShellSupport.hs
Normal file
@@ -0,0 +1,422 @@
|
||||
{-
|
||||
Copyright 2012-2016 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/>.
|
||||
-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
module ShellCheck.Checks.ShellSupport (checker) where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.AnalyzerLib
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.RWS
|
||||
import Data.Char
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import qualified Data.Map as Map
|
||||
|
||||
data ForShell = ForShell [Shell] (Token -> Analysis)
|
||||
|
||||
getChecker params list = Checker {
|
||||
perScript = nullCheck,
|
||||
perToken = foldl composeAnalyzers nullCheck $ mapMaybe include list
|
||||
}
|
||||
where
|
||||
shell = shellType params
|
||||
include (ForShell list a) = do
|
||||
guard $ shell `elem` list
|
||||
return a
|
||||
|
||||
checker params = getChecker params checks
|
||||
|
||||
checks = [
|
||||
checkForDecimals
|
||||
,checkBashisms
|
||||
,checkEchoSed
|
||||
,checkBraceExpansionVars
|
||||
,checkMultiDimensionalArrays
|
||||
,checkPS1Assignments
|
||||
]
|
||||
|
||||
testChecker (ForShell _ t) =
|
||||
Checker {
|
||||
perScript = nullCheck,
|
||||
perToken = t
|
||||
}
|
||||
verify c s = producesComments (testChecker c) s == Just True
|
||||
verifyNot c s = producesComments (testChecker c) s == Just False
|
||||
|
||||
-- |
|
||||
-- >>> prop $ verify checkForDecimals "((3.14*c))"
|
||||
-- >>> prop $ verify checkForDecimals "foo[1.2]=bar"
|
||||
-- >>> prop $ verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
|
||||
checkForDecimals = ForShell [Sh, Dash, Bash] f
|
||||
where
|
||||
f t@(TA_Expansion id _) = potentially $ do
|
||||
str <- getLiteralString t
|
||||
first <- str !!! 0
|
||||
guard $ isDigit first && '.' `elem` str
|
||||
return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
|
||||
f _ = return ()
|
||||
|
||||
|
||||
-- |
|
||||
-- >>> prop $ verify checkBashisms "while read a; do :; done < <(a)"
|
||||
-- >>> prop $ verify checkBashisms "[ foo -nt bar ]"
|
||||
-- >>> prop $ verify checkBashisms "echo $((i++))"
|
||||
-- >>> prop $ verify checkBashisms "rm !(*.hs)"
|
||||
-- >>> prop $ verify checkBashisms "source file"
|
||||
-- >>> prop $ verify checkBashisms "[ \"$a\" == 42 ]"
|
||||
-- >>> prop $ verify checkBashisms "echo ${var[1]}"
|
||||
-- >>> prop $ verify checkBashisms "echo ${!var[@]}"
|
||||
-- >>> prop $ verify checkBashisms "echo ${!var*}"
|
||||
-- >>> prop $ verify checkBashisms "echo ${var:4:12}"
|
||||
-- >>> prop $ verifyNot checkBashisms "echo ${var:-4}"
|
||||
-- >>> prop $ verify checkBashisms "echo ${var//foo/bar}"
|
||||
-- >>> prop $ verify checkBashisms "exec -c env"
|
||||
-- >>> prop $ verify checkBashisms "echo -n \"Foo: \""
|
||||
-- >>> prop $ verify checkBashisms "let n++"
|
||||
-- >>> prop $ verify checkBashisms "echo $RANDOM"
|
||||
-- >>> prop $ verify checkBashisms "echo $((RANDOM%6+1))"
|
||||
-- >>> prop $ verify checkBashisms "foo &> /dev/null"
|
||||
-- >>> prop $ verify checkBashisms "foo > file*.txt"
|
||||
-- >>> prop $ verify checkBashisms "read -ra foo"
|
||||
-- >>> prop $ verify checkBashisms "[ -a foo ]"
|
||||
-- >>> prop $ verifyNot checkBashisms "[ foo -a bar ]"
|
||||
-- >>> prop $ verify checkBashisms "trap mything ERR INT"
|
||||
-- >>> prop $ verifyNot checkBashisms "trap mything INT TERM"
|
||||
-- >>> prop $ verify checkBashisms "cat < /dev/tcp/host/123"
|
||||
-- >>> prop $ verify checkBashisms "trap mything ERR SIGTERM"
|
||||
-- >>> prop $ verify checkBashisms "echo *[^0-9]*"
|
||||
-- >>> prop $ verify checkBashisms "exec {n}>&2"
|
||||
-- >>> prop $ verify checkBashisms "echo ${!var}"
|
||||
-- >>> prop $ verify checkBashisms "printf -v '%s' \"$1\""
|
||||
-- >>> prop $ verify checkBashisms "printf '%q' \"$1\""
|
||||
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
|
||||
-- >>> prop $ verify checkBashisms "#!/bin/sh\necho -n foo"
|
||||
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\necho -n foo"
|
||||
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\nlocal foo"
|
||||
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
|
||||
-- >>> prop $ verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
|
||||
-- >>> prop $ verify checkBashisms "RANDOM=9; echo $RANDOM"
|
||||
-- >>> prop $ verify checkBashisms "foo-bar() { true; }"
|
||||
-- >>> prop $ verify checkBashisms "echo $(<file)"
|
||||
-- >>> prop $ verify checkBashisms "echo `<file`"
|
||||
-- >>> prop $ verify checkBashisms "trap foo int"
|
||||
-- >>> prop $ verify checkBashisms "trap foo sigint"
|
||||
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
|
||||
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
|
||||
-- >>> prop $ verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
|
||||
-- >>> prop $ verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
|
||||
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
|
||||
-- >>> prop $ verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
|
||||
-- >>> prop $ verify checkBashisms "#!/bin/sh\ncmd >& file"
|
||||
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
||||
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
||||
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
||||
-- >>> prop $ verify checkBashisms "#!/bin/sh\nfoo+=bar"
|
||||
-- >>> prop $ verify checkBashisms "#!/bin/sh\necho ${@%foo}"
|
||||
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\necho ${##}"
|
||||
checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
params <- ask
|
||||
kludge params t
|
||||
where
|
||||
-- This code was copy-pasted from Analytics where params was a variable
|
||||
kludge params = bashism
|
||||
where
|
||||
isDash = shellType params == Dash
|
||||
warnMsg id s =
|
||||
if isDash
|
||||
then warn id 2169 $ "In dash, " ++ s ++ " not supported."
|
||||
else warn id 2039 $ "In POSIX sh, " ++ s ++ " undefined."
|
||||
|
||||
bashism (T_ProcSub id _ _) = warnMsg id "process substitution is"
|
||||
bashism (T_Extglob id _ _) = warnMsg id "extglob is"
|
||||
bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is"
|
||||
bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\" is"
|
||||
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id "arithmetic for loops are"
|
||||
bashism (T_Arithmetic id _) = warnMsg id "standalone ((..)) is"
|
||||
bashism (T_DollarBracket id _) = warnMsg id "$[..] in place of $((..)) is"
|
||||
bashism (T_SelectIn id _ _ _) = warnMsg id "select loops are"
|
||||
bashism (T_BraceExpansion id _) = warnMsg id "brace expansion is"
|
||||
bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is"
|
||||
bashism (T_HereString id _) = warnMsg id "here-strings are"
|
||||
bashism (TC_Binary id SingleBracket op _ _)
|
||||
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
|
||||
unless isDash $ warnMsg id $ "lexicographical " ++ op ++ " is"
|
||||
bashism (TC_Binary id SingleBracket op _ _)
|
||||
| op `elem` [ "-nt", "-ef" ] =
|
||||
unless isDash $ warnMsg id $ op ++ " is"
|
||||
bashism (TC_Binary id SingleBracket "==" _ _) =
|
||||
warnMsg id "== in place of = is"
|
||||
bashism (TC_Binary id SingleBracket "=~" _ _) =
|
||||
warnMsg id "=~ regex matching is"
|
||||
bashism (TC_Unary id _ "-a" _) =
|
||||
warnMsg id "unary -a in place of -e is"
|
||||
bashism (TA_Unary id op _)
|
||||
| op `elem` [ "|++", "|--", "++|", "--|"] =
|
||||
warnMsg id $ filter (/= '|') op ++ " is"
|
||||
bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are"
|
||||
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is"
|
||||
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id ">& is"
|
||||
bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are"
|
||||
bashism (T_FdRedirect id num _)
|
||||
| all isDigit num && length num > 1 = warnMsg id "FDs outside 0-9 are"
|
||||
bashism (T_Assignment id Append _ _ _) =
|
||||
warnMsg id "+= is"
|
||||
bashism (T_IoFile id _ word) | isNetworked =
|
||||
warnMsg id "/dev/{tcp,udp} is"
|
||||
where
|
||||
file = onlyLiteralString word
|
||||
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
|
||||
bashism (T_Glob id str) | "[^" `isInfixOf` str =
|
||||
warnMsg id "^ in place of ! in glob bracket expressions is"
|
||||
|
||||
bashism t@(TA_Variable id str _) | isBashVariable str =
|
||||
warnMsg id $ str ++ " is"
|
||||
|
||||
bashism t@(T_DollarBraced id token) = do
|
||||
mapM_ check expansion
|
||||
when (isBashVariable var) $
|
||||
warnMsg id $ var ++ " is"
|
||||
where
|
||||
str = bracedString t
|
||||
var = getBracedReference str
|
||||
check (regex, feature) =
|
||||
when (isJust $ matchRegex regex str) $ warnMsg id feature
|
||||
|
||||
bashism t@(T_Pipe id "|&") =
|
||||
warnMsg id "|& in place of 2>&1 | is"
|
||||
bashism (T_Array id _) =
|
||||
warnMsg id "arrays are"
|
||||
bashism (T_IoFile id _ t) | isGlob t =
|
||||
warnMsg id "redirecting to/from globs is"
|
||||
bashism (T_CoProc id _ _) =
|
||||
warnMsg id "coproc is"
|
||||
|
||||
bashism (T_Function id _ _ str _) | not (isVariableName str) =
|
||||
warnMsg id "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
|
||||
|
||||
bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
|
||||
warnMsg id "$(<file) to read files is"
|
||||
bashism (T_Backticked id [x]) | isOnlyRedirection x =
|
||||
warnMsg id "`<file` to read files is"
|
||||
|
||||
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
||||
| t `isCommand` "echo" && "-" `isPrefixOf` argString =
|
||||
unless ("--" `isPrefixOf` argString) $ -- echo "-----"
|
||||
if isDash
|
||||
then
|
||||
when (argString /= "-n") $
|
||||
warnMsg (getId arg) "echo flags besides -n"
|
||||
else
|
||||
warnMsg (getId arg) "echo flags are"
|
||||
where argString = concat $ oversimplify arg
|
||||
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
||||
| t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
|
||||
warnMsg (getId arg) "exec flags are"
|
||||
bashism t@(T_SimpleCommand id _ _)
|
||||
| t `isCommand` "let" = warnMsg id "'let' is"
|
||||
|
||||
bashism t@(T_SimpleCommand id _ (cmd:rest)) =
|
||||
let name = fromMaybe "" $ getCommandName t
|
||||
flags = getLeadingFlags t
|
||||
in do
|
||||
when (name `elem` unsupportedCommands) $
|
||||
warnMsg id $ "'" ++ name ++ "' is"
|
||||
potentially $ do
|
||||
allowed <- Map.lookup name allowedFlags
|
||||
(word, flag) <- listToMaybe $
|
||||
filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
|
||||
return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"
|
||||
|
||||
when (name == "source") $ warnMsg id "'source' in place of '.' is"
|
||||
when (name == "trap") $
|
||||
let
|
||||
check token = potentially $ do
|
||||
str <- getLiteralString token
|
||||
let upper = map toUpper str
|
||||
return $ do
|
||||
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
|
||||
warnMsg (getId token) $ "trapping " ++ str ++ " is"
|
||||
when ("SIG" `isPrefixOf` upper) $
|
||||
warnMsg (getId token)
|
||||
"prefixing signal names with 'SIG' is"
|
||||
when (not isDash && upper /= str) $
|
||||
warnMsg (getId token)
|
||||
"using lower/mixed case for signal names is"
|
||||
in
|
||||
mapM_ check (drop 1 rest)
|
||||
|
||||
when (name == "printf") $ potentially $ do
|
||||
format <- rest !!! 0 -- flags are covered by allowedFlags
|
||||
let literal = onlyLiteralString format
|
||||
guard $ "%q" `isInfixOf` literal
|
||||
return $ warnMsg (getId format) "printf %q is"
|
||||
where
|
||||
unsupportedCommands = [
|
||||
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
||||
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
|
||||
"typeset"
|
||||
] ++ if not isDash then ["local"] else []
|
||||
allowedFlags = Map.fromList [
|
||||
("exec", []),
|
||||
("export", ["-p"]),
|
||||
("printf", []),
|
||||
("read", if isDash then ["r", "p"] else ["r"]),
|
||||
("ulimit", ["f"])
|
||||
]
|
||||
bashism t@(T_SourceCommand id src _) =
|
||||
let name = fromMaybe "" $ getCommandName src
|
||||
in do
|
||||
when (name == "source") $ warnMsg id "'source' in place of '.' is"
|
||||
bashism _ = return ()
|
||||
|
||||
varChars="_0-9a-zA-Z"
|
||||
expansion = let re = mkRegex in [
|
||||
(re $ "^![" ++ varChars ++ "]", "indirect expansion is"),
|
||||
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
|
||||
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
|
||||
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
|
||||
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", "string indexing is"),
|
||||
(re $ "^([*@][%#]|#[@*])", "string operations on $@/$* are"),
|
||||
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")
|
||||
]
|
||||
bashVars = [
|
||||
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
||||
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
|
||||
]
|
||||
bashDynamicVars = [ "RANDOM", "SECONDS" ]
|
||||
dashVars = [ ]
|
||||
isBashVariable var =
|
||||
(var `elem` bashDynamicVars
|
||||
|| var `elem` bashVars && not (isAssigned var))
|
||||
&& not (isDash && var `elem` dashVars)
|
||||
isAssigned var = any f (variableFlow params)
|
||||
where
|
||||
f x = case x of
|
||||
Assignment (_, _, name, _) -> name == var
|
||||
_ -> False
|
||||
|
||||
-- |
|
||||
-- >>> prop $ verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
|
||||
-- >>> prop $ verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
|
||||
checkEchoSed = ForShell [Bash, Ksh] f
|
||||
where
|
||||
f (T_Pipeline id _ [a, b]) =
|
||||
when (acmd == ["echo", "${VAR}"]) $
|
||||
case bcmd of
|
||||
["sed", v] -> checkIn v
|
||||
["sed", "-e", v] -> checkIn v
|
||||
_ -> return ()
|
||||
where
|
||||
-- This should have used backreferences, but TDFA doesn't support them
|
||||
sedRe = mkRegex "^s(.)([^\n]*)g?$"
|
||||
isSimpleSed s = fromMaybe False $ do
|
||||
[first,rest] <- matchRegex sedRe s
|
||||
let delimiters = filter (== head first) rest
|
||||
guard $ length delimiters == 2
|
||||
return True
|
||||
|
||||
acmd = oversimplify a
|
||||
bcmd = oversimplify b
|
||||
checkIn s =
|
||||
when (isSimpleSed s) $
|
||||
style id 2001 "See if you can use ${variable//search/replace} instead."
|
||||
f _ = return ()
|
||||
|
||||
|
||||
-- |
|
||||
-- >>> prop $ verify checkBraceExpansionVars "echo {1..$n}"
|
||||
-- >>> prop $ verifyNot checkBraceExpansionVars "echo {1,3,$n}"
|
||||
-- >>> prop $ verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg"
|
||||
-- >>> prop $ verify checkBraceExpansionVars "echo {$i..100}"
|
||||
checkBraceExpansionVars = ForShell [Bash] f
|
||||
where
|
||||
f t@(T_BraceExpansion id list) = mapM_ check list
|
||||
where
|
||||
check element =
|
||||
when (any (`isInfixOf` toString element) ["$..", "..$"]) $ do
|
||||
c <- isEvaled element
|
||||
if c
|
||||
then style id 2175 "Quote this invalid brace expansion since it should be passed literally to eval."
|
||||
else warn id 2051 "Bash doesn't support variables in brace range expansions."
|
||||
f _ = return ()
|
||||
|
||||
literalExt t =
|
||||
case t of
|
||||
T_DollarBraced {} -> return "$"
|
||||
T_DollarExpansion {} -> return "$"
|
||||
T_DollarArithmetic {} -> return "$"
|
||||
otherwise -> return "-"
|
||||
toString t = fromJust $ getLiteralStringExt literalExt t
|
||||
isEvaled t = do
|
||||
cmd <- getClosestCommandM t
|
||||
return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval"
|
||||
|
||||
|
||||
-- |
|
||||
-- >>> prop $ verify checkMultiDimensionalArrays "foo[a][b]=3"
|
||||
-- >>> prop $ verifyNot checkMultiDimensionalArrays "foo[a]=3"
|
||||
-- >>> prop $ verify checkMultiDimensionalArrays "foo=( [a][b]=c )"
|
||||
-- >>> prop $ verifyNot checkMultiDimensionalArrays "foo=( [a]=c )"
|
||||
-- >>> prop $ verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}"
|
||||
-- >>> prop $ verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}"
|
||||
checkMultiDimensionalArrays = ForShell [Bash] f
|
||||
where
|
||||
f token =
|
||||
case token of
|
||||
T_Assignment _ _ name (first:second:_) _ -> about second
|
||||
T_IndexedElement _ (first:second:_) _ -> about second
|
||||
T_DollarBraced {} ->
|
||||
when (isMultiDim token) $ about token
|
||||
_ -> return ()
|
||||
about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays."
|
||||
|
||||
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
|
||||
isMultiDim t = getBracedModifier (bracedString t) `matches` re
|
||||
|
||||
-- |
|
||||
-- >>> prop $ verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
|
||||
-- >>> prop $ verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
|
||||
-- >>> prop $ verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
|
||||
-- >>> prop $ verify checkPS1Assignments "PS1=$'\\x1b[c '"
|
||||
-- >>> prop $ verify checkPS1Assignments "PS1=$'\\e[3m; '"
|
||||
-- >>> prop $ verify checkPS1Assignments "export PS1=$'\\e[3m; '"
|
||||
-- >>> prop $ verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"
|
||||
-- >>> prop $ verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
|
||||
-- >>> prop $ verifyNot checkPS1Assignments "PS1='e033x1B'"
|
||||
-- >>> prop $ verifyNot checkPS1Assignments "PS1='\\[\\e\\]'"
|
||||
checkPS1Assignments = ForShell [Bash] f
|
||||
where
|
||||
f token = case token of
|
||||
(T_Assignment _ _ "PS1" _ word) -> warnFor word
|
||||
_ -> return ()
|
||||
|
||||
warnFor word =
|
||||
let contents = concat $ oversimplify word in
|
||||
when (containsUnescaped contents) $
|
||||
info (getId word) 2025 "Make sure all escape sequences are enclosed in \\[..\\] to prevent line wrapping issues"
|
||||
containsUnescaped s =
|
||||
let unenclosed = subRegex enclosedRegex s "" in
|
||||
isJust $ matchRegex escapeRegex unenclosed
|
||||
enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager
|
||||
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
|
@@ -33,10 +33,13 @@ internalVariables = [
|
||||
-- Other
|
||||
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
|
||||
"HOSTNAME", "KRB5CCNAME", "XAUTHORITY"
|
||||
|
||||
-- Ksh
|
||||
, ".sh.version"
|
||||
]
|
||||
|
||||
variablesWithoutSpaces = [
|
||||
"$", "-", "?", "!",
|
||||
"$", "-", "?", "!", "#",
|
||||
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
|
||||
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
|
||||
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
|
||||
@@ -74,6 +77,14 @@ commonCommands = [
|
||||
"zcat"
|
||||
]
|
||||
|
||||
nonReadingCommands = [
|
||||
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
|
||||
"cp", "du", "echo", "export", "false", "fg", "fuser", "getconf",
|
||||
"getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
|
||||
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
|
||||
"set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname"
|
||||
]
|
||||
|
||||
sampleWords = [
|
||||
"alpha", "bravo", "charlie", "delta", "echo", "foxtrot",
|
||||
"golf", "hotel", "india", "juliett", "kilo", "lima", "mike",
|
||||
@@ -82,12 +93,24 @@ sampleWords = [
|
||||
"zulu"
|
||||
]
|
||||
|
||||
binaryTestOps = [
|
||||
"-nt", "-ot", "-ef", "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le",
|
||||
"-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="
|
||||
]
|
||||
|
||||
unaryTestOps = [
|
||||
"!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p",
|
||||
"-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n",
|
||||
"-o", "-v", "-R"
|
||||
]
|
||||
|
||||
shellForExecutable :: String -> Maybe Shell
|
||||
shellForExecutable name =
|
||||
case name of
|
||||
"sh" -> return Sh
|
||||
"bash" -> return Bash
|
||||
"dash" -> return Dash
|
||||
"ash" -> return Dash -- There's also a warning for this.
|
||||
"ksh" -> return Ksh
|
||||
"ksh88" -> return Ksh
|
||||
"ksh93" -> return Ksh
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.CheckStyle (format) where
|
||||
|
||||
@@ -34,14 +34,27 @@ format = return Formatter {
|
||||
putStrLn "<checkstyle version='4.3'>",
|
||||
|
||||
onFailure = outputError,
|
||||
onResult = outputResult,
|
||||
onResult = outputResults,
|
||||
|
||||
footer = putStrLn "</checkstyle>"
|
||||
}
|
||||
|
||||
outputResult result contents = do
|
||||
let comments = makeNonVirtual (crComments result) contents
|
||||
putStrLn . formatFile (crFilename result) $ comments
|
||||
outputResults cr sys =
|
||||
if null comments
|
||||
then outputFile (crFilename cr) "" []
|
||||
else mapM_ outputGroup fileGroups
|
||||
where
|
||||
comments = crComments cr
|
||||
fileGroups = groupWith sourceFile comments
|
||||
outputGroup group = do
|
||||
let filename = sourceFile (head group)
|
||||
result <- (siReadFile sys) filename
|
||||
let contents = either (const "") id result
|
||||
outputFile filename contents group
|
||||
|
||||
outputFile filename contents warnings = do
|
||||
let comments = makeNonVirtual warnings contents
|
||||
putStrLn . formatFile filename $ comments
|
||||
|
||||
formatFile name comments = concat [
|
||||
"<file ", attr "name" name, ">\n",
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.Format where
|
||||
|
||||
@@ -25,21 +25,22 @@ import ShellCheck.Interface
|
||||
-- A formatter that carries along an arbitrary piece of data
|
||||
data Formatter = Formatter {
|
||||
header :: IO (),
|
||||
onResult :: CheckResult -> String -> IO (),
|
||||
onResult :: CheckResult -> SystemInterface IO -> IO (),
|
||||
onFailure :: FilePath -> ErrorMessage -> IO (),
|
||||
footer :: IO ()
|
||||
}
|
||||
|
||||
lineNo (PositionedComment pos _ _) = posLine pos
|
||||
endLineNo (PositionedComment _ end _) = posLine end
|
||||
colNo (PositionedComment pos _ _) = posColumn pos
|
||||
endColNo (PositionedComment _ end _) = posColumn end
|
||||
codeNo (PositionedComment _ _ (Comment _ code _)) = code
|
||||
messageText (PositionedComment _ _ (Comment _ _ t)) = t
|
||||
sourceFile = posFile . pcStartPos
|
||||
lineNo = posLine . pcStartPos
|
||||
endLineNo = posLine . pcEndPos
|
||||
colNo = posColumn . pcStartPos
|
||||
endColNo = posColumn . pcEndPos
|
||||
codeNo = cCode . pcComment
|
||||
messageText = cMessage . pcComment
|
||||
|
||||
severityText :: PositionedComment -> String
|
||||
severityText (PositionedComment _ _ (Comment c _ _)) =
|
||||
case c of
|
||||
severityText pc =
|
||||
case cSeverity (pcComment pc) of
|
||||
ErrorC -> "error"
|
||||
WarningC -> "warning"
|
||||
InfoC -> "info"
|
||||
@@ -50,11 +51,14 @@ makeNonVirtual comments contents =
|
||||
map fix comments
|
||||
where
|
||||
ls = lines contents
|
||||
fix c@(PositionedComment start end comment) = PositionedComment start {
|
||||
posColumn = realignColumn lineNo colNo c
|
||||
} end {
|
||||
posColumn = realignColumn endLineNo endColNo c
|
||||
} comment
|
||||
fix c = c {
|
||||
pcStartPos = (pcStartPos c) {
|
||||
posColumn = realignColumn lineNo colNo c
|
||||
}
|
||||
, pcEndPos = (pcEndPos c) {
|
||||
posColumn = realignColumn endLineNo endColNo c
|
||||
}
|
||||
}
|
||||
realignColumn lineNo colNo c =
|
||||
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
|
||||
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.GCC (format) where
|
||||
|
||||
@@ -31,14 +31,25 @@ format = return Formatter {
|
||||
header = return (),
|
||||
footer = return (),
|
||||
onFailure = outputError,
|
||||
onResult = outputResult
|
||||
onResult = outputAll
|
||||
}
|
||||
|
||||
outputError file error = hPutStrLn stderr $ file ++ ": " ++ error
|
||||
|
||||
outputResult result contents = do
|
||||
let comments = makeNonVirtual (crComments result) contents
|
||||
mapM_ (putStrLn . formatComment (crFilename result)) comments
|
||||
outputAll cr sys = mapM_ f groups
|
||||
where
|
||||
comments = crComments cr
|
||||
groups = groupWith sourceFile comments
|
||||
f :: [PositionedComment] -> IO ()
|
||||
f group = do
|
||||
let filename = sourceFile (head group)
|
||||
result <- (siReadFile sys) filename
|
||||
let contents = either (const "") id result
|
||||
outputResult filename contents group
|
||||
|
||||
outputResult filename contents warnings = do
|
||||
let comments = makeNonVirtual warnings contents
|
||||
mapM_ (putStrLn . formatComment filename) comments
|
||||
|
||||
formatComment filename c = concat [
|
||||
filename, ":",
|
99
src/ShellCheck/Formatter/JSON.hs
Normal file
99
src/ShellCheck/Formatter/JSON.hs
Normal file
@@ -0,0 +1,99 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-
|
||||
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.Formatter.JSON (format) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Data.Aeson
|
||||
import Data.IORef
|
||||
import Data.Monoid
|
||||
import GHC.Exts
|
||||
import System.IO
|
||||
import qualified Data.ByteString.Lazy.Char8 as BL
|
||||
|
||||
format = do
|
||||
ref <- newIORef []
|
||||
return Formatter {
|
||||
header = return (),
|
||||
onResult = collectResult ref,
|
||||
onFailure = outputError,
|
||||
footer = finish ref
|
||||
}
|
||||
|
||||
instance ToJSON Replacement where
|
||||
toJSON replacement =
|
||||
let start = repStartPos replacement
|
||||
end = repEndPos replacement
|
||||
str = repString replacement in
|
||||
object [
|
||||
"line" .= posLine start,
|
||||
"endLine" .= posLine end,
|
||||
"column" .= posColumn start,
|
||||
"endColumn" .= posColumn end,
|
||||
"replaceWith" .= str
|
||||
]
|
||||
|
||||
instance ToJSON PositionedComment where
|
||||
toJSON comment =
|
||||
let start = pcStartPos comment
|
||||
end = pcEndPos comment
|
||||
c = pcComment comment in
|
||||
object [
|
||||
"file" .= posFile start,
|
||||
"line" .= posLine start,
|
||||
"endLine" .= posLine end,
|
||||
"column" .= posColumn start,
|
||||
"endColumn" .= posColumn end,
|
||||
"level" .= severityText comment,
|
||||
"code" .= cCode c,
|
||||
"message" .= cMessage c,
|
||||
"fix" .= pcFix comment
|
||||
]
|
||||
|
||||
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
|
||||
<> "fix" .= pcFix comment
|
||||
)
|
||||
|
||||
instance ToJSON Fix where
|
||||
toJSON fix = object [
|
||||
"replacements" .= fixReplacements fix
|
||||
]
|
||||
|
||||
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
|
||||
collectResult ref result _ =
|
||||
modifyIORef ref (\x -> crComments result ++ x)
|
||||
|
||||
finish ref = do
|
||||
list <- readIORef ref
|
||||
BL.putStrLn $ encode list
|
213
src/ShellCheck/Formatter/TTY.hs
Normal file
213
src/ShellCheck/Formatter/TTY.hs
Normal file
@@ -0,0 +1,213 @@
|
||||
{-
|
||||
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.Formatter.TTY (format) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Control.Monad
|
||||
import Data.IORef
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import GHC.Exts
|
||||
import System.IO
|
||||
import System.Info
|
||||
|
||||
wikiLink = "https://www.shellcheck.net/wiki/"
|
||||
|
||||
-- An arbitrary Ord thing to order warnings
|
||||
type Ranking = (Char, Severity, Integer)
|
||||
|
||||
format :: FormatterOptions -> IO Formatter
|
||||
format options = do
|
||||
topErrorRef <- newIORef []
|
||||
return Formatter {
|
||||
header = return (),
|
||||
footer = outputWiki topErrorRef,
|
||||
onFailure = outputError options,
|
||||
onResult = outputResult options topErrorRef
|
||||
}
|
||||
|
||||
colorForLevel level =
|
||||
case level of
|
||||
"error" -> 31 -- red
|
||||
"warning" -> 33 -- yellow
|
||||
"info" -> 32 -- green
|
||||
"style" -> 32 -- green
|
||||
"message" -> 1 -- bold
|
||||
"source" -> 0 -- none
|
||||
_ -> 0 -- none
|
||||
|
||||
rankError :: PositionedComment -> Ranking
|
||||
rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err)
|
||||
where
|
||||
ranking =
|
||||
if cCode (pcComment err) `elem` uninteresting
|
||||
then 'Z'
|
||||
else 'A'
|
||||
|
||||
-- A list of the most generic, least directly helpful
|
||||
-- error codes to downrank.
|
||||
uninteresting = [
|
||||
1009, -- Mentioned parser error was..
|
||||
1019, -- Expected this to be an argument
|
||||
1036, -- ( is invalid here
|
||||
1047, -- Expected 'fi'
|
||||
1062, -- Expected 'done'
|
||||
1070, -- Parsing stopped here (generic)
|
||||
1072, -- Missing/unexpected ..
|
||||
1073, -- Couldn't parse this ..
|
||||
1088, -- Parsing stopped here (paren)
|
||||
1089 -- Parsing stopped here (keyword)
|
||||
]
|
||||
|
||||
appendComments errRef comments max = do
|
||||
previous <- readIORef errRef
|
||||
let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments
|
||||
writeIORef errRef . take max . nubBy equal . sort $ previous ++ current
|
||||
where
|
||||
fst3 (x,_,_) = x
|
||||
equal x y = fst3 x == fst3 y
|
||||
|
||||
outputWiki :: IORef [(Ranking, Integer, String)] -> IO ()
|
||||
outputWiki errRef = do
|
||||
issues <- readIORef errRef
|
||||
unless (null issues) $ do
|
||||
putStrLn "For more information:"
|
||||
mapM_ showErr issues
|
||||
where
|
||||
showErr (_, code, msg) =
|
||||
putStrLn $ " " ++ wikiLink ++ "SC" ++ show code ++ " -- " ++ shorten msg
|
||||
limit = 36
|
||||
shorten msg =
|
||||
if length msg < limit
|
||||
then msg
|
||||
else (take (limit-3) msg) ++ "..."
|
||||
|
||||
outputError options file error = do
|
||||
color <- getColorFunc $ foColorOption options
|
||||
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
|
||||
|
||||
outputResult options ref result sys = do
|
||||
color <- getColorFunc $ foColorOption options
|
||||
let comments = crComments result
|
||||
appendComments ref comments (fromIntegral $ foWikiLinkCount options)
|
||||
let fileGroups = groupWith sourceFile comments
|
||||
mapM_ (outputForFile color sys) fileGroups
|
||||
|
||||
outputForFile color sys comments = do
|
||||
let fileName = sourceFile (head comments)
|
||||
result <- (siReadFile sys) fileName
|
||||
let contents = either (const "") id result
|
||||
let fileLines = lines contents
|
||||
let lineCount = fromIntegral $ length fileLines
|
||||
let groups = groupWith lineNo comments
|
||||
mapM_ (\commentsForLine -> do
|
||||
let lineNum = lineNo (head commentsForLine)
|
||||
let line = if lineNum < 1 || lineNum > lineCount
|
||||
then ""
|
||||
else fileLines !! fromIntegral (lineNum - 1)
|
||||
putStrLn ""
|
||||
putStrLn $ color "message" $
|
||||
"In " ++ fileName ++" line " ++ show lineNum ++ ":"
|
||||
putStrLn (color "source" line)
|
||||
mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) commentsForLine
|
||||
putStrLn ""
|
||||
showFixedString color comments lineNum line
|
||||
) groups
|
||||
|
||||
hasApplicableFix lineNum comment = fromMaybe False $ do
|
||||
replacements <- fixReplacements <$> pcFix comment
|
||||
guard $ all (\c -> onSameLine (repStartPos c) && onSameLine (repEndPos c)) replacements
|
||||
return True
|
||||
where
|
||||
onSameLine pos = posLine pos == lineNum
|
||||
|
||||
-- FIXME: Work correctly with multiple replacements
|
||||
showFixedString color comments lineNum line =
|
||||
case filter (hasApplicableFix lineNum) comments of
|
||||
(first:_) -> do
|
||||
-- in the spirit of error prone
|
||||
putStrLn $ color "message" "Did you mean: "
|
||||
putStrLn $ fixedString first line
|
||||
putStrLn ""
|
||||
_ -> return ()
|
||||
|
||||
-- need to do something smart about sorting by end index
|
||||
fixedString :: PositionedComment -> String -> String
|
||||
fixedString comment line =
|
||||
case (pcFix comment) of
|
||||
Nothing -> ""
|
||||
Just rs ->
|
||||
applyReplacement (fixReplacements rs) line 0
|
||||
where
|
||||
applyReplacement [] s _ = s
|
||||
applyReplacement (rep:xs) s offset =
|
||||
let replacementString = repString rep
|
||||
start = (posColumn . repStartPos) rep
|
||||
end = (posColumn . repEndPos) rep
|
||||
z = doReplace start end s replacementString
|
||||
len_r = (fromIntegral . length) replacementString in
|
||||
applyReplacement xs z (offset + (end - start) + len_r)
|
||||
|
||||
-- FIXME: Work correctly with tabs
|
||||
-- start and end comes from pos, which is 1 based
|
||||
-- doReplace 0 0 "1234" "A" -> "A1234" -- technically not valid
|
||||
-- doReplace 1 1 "1234" "A" -> "A1234"
|
||||
-- doReplace 1 2 "1234" "A" -> "A234"
|
||||
-- doReplace 3 3 "1234" "A" -> "12A34"
|
||||
-- doReplace 4 4 "1234" "A" -> "123A4"
|
||||
-- doReplace 5 5 "1234" "A" -> "1234A"
|
||||
doReplace start end o r =
|
||||
let si = fromIntegral (start-1)
|
||||
ei = fromIntegral (end-1)
|
||||
(x, xs) = splitAt si o
|
||||
(y, z) = splitAt (ei - si) xs
|
||||
in
|
||||
x ++ r ++ z
|
||||
|
||||
cuteIndent :: PositionedComment -> String
|
||||
cuteIndent comment =
|
||||
replicate (fromIntegral $ colNo comment - 1) ' ' ++
|
||||
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 num = "SC" ++ show num
|
||||
|
||||
getColorFunc colorOption = do
|
||||
term <- hIsTerminalDevice stdout
|
||||
let windows = "mingw" `isPrefixOf` os
|
||||
let isUsableTty = term && not windows
|
||||
let useColor = case colorOption of
|
||||
ColorAlways -> True
|
||||
ColorNever -> False
|
||||
ColorAuto -> isUsableTty
|
||||
return $ if useColor then colorComment else const id
|
||||
where
|
||||
colorComment level comment =
|
||||
ansi (colorForLevel level) ++ comment ++ clear
|
||||
clear = ansi 0
|
||||
ansi n = "\x1B[" ++ show n ++ "m"
|
269
src/ShellCheck/Interface.hs
Normal file
269
src/ShellCheck/Interface.hs
Normal file
@@ -0,0 +1,269 @@
|
||||
{-
|
||||
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/>.
|
||||
-}
|
||||
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
|
||||
module ShellCheck.Interface
|
||||
(
|
||||
SystemInterface(..)
|
||||
, CheckSpec(csFilename, csScript, csCheckSourced, csExcludedWarnings, csShellTypeOverride, csMinSeverity)
|
||||
, CheckResult(crFilename, crComments)
|
||||
, ParseSpec(psFilename, psScript, psCheckSourced, psShellTypeOverride)
|
||||
, ParseResult(prComments, prTokenPositions, prRoot)
|
||||
, AnalysisSpec(asScript, asShellType, asExecutionMode, asCheckSourced, asTokenPositions)
|
||||
, AnalysisResult(arComments)
|
||||
, FormatterOptions(foColorOption, foWikiLinkCount)
|
||||
, Shell(Ksh, Sh, Bash, Dash)
|
||||
, ExecutionMode(Executed, Sourced)
|
||||
, ErrorMessage
|
||||
, Code
|
||||
, Severity(ErrorC, WarningC, InfoC, StyleC)
|
||||
, Position(posFile, posLine, posColumn)
|
||||
, Comment(cSeverity, cCode, cMessage)
|
||||
, PositionedComment(pcStartPos , pcEndPos , pcComment, pcFix)
|
||||
, ColorOption(ColorAuto, ColorAlways, ColorNever)
|
||||
, TokenComment(tcId, tcComment, tcFix)
|
||||
, emptyCheckResult
|
||||
, newParseResult
|
||||
, newAnalysisSpec
|
||||
, newAnalysisResult
|
||||
, newFormatterOptions
|
||||
, newPosition
|
||||
, newTokenComment
|
||||
, mockedSystemInterface
|
||||
, newParseSpec
|
||||
, emptyCheckSpec
|
||||
, newPositionedComment
|
||||
, newComment
|
||||
, Fix(fixReplacements)
|
||||
, newFix
|
||||
, Replacement(repStartPos, repEndPos, repString)
|
||||
, newReplacement
|
||||
) where
|
||||
|
||||
import ShellCheck.AST
|
||||
|
||||
import Control.DeepSeq
|
||||
import Control.Monad.Identity
|
||||
import Data.Monoid
|
||||
import GHC.Generics (Generic)
|
||||
import qualified Data.Map as Map
|
||||
|
||||
|
||||
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,
|
||||
asTokenPositions :: Map.Map Id (Position, Position)
|
||||
}
|
||||
|
||||
newAnalysisSpec token = AnalysisSpec {
|
||||
asScript = token,
|
||||
asShellType = Nothing,
|
||||
asExecutionMode = Executed,
|
||||
asCheckSourced = False,
|
||||
asTokenPositions = Map.empty
|
||||
}
|
||||
|
||||
newtype AnalysisResult = AnalysisResult {
|
||||
arComments :: [TokenComment]
|
||||
}
|
||||
|
||||
newAnalysisResult = AnalysisResult {
|
||||
arComments = []
|
||||
}
|
||||
|
||||
-- Formatter options
|
||||
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, Generic, NFData)
|
||||
data Position = Position {
|
||||
posFile :: String, -- Filename
|
||||
posLine :: Integer, -- 1 based source line
|
||||
posColumn :: Integer -- 1 based source column, where tabs are 8
|
||||
} deriving (Show, Eq, Generic, NFData)
|
||||
|
||||
newPosition :: Position
|
||||
newPosition = Position {
|
||||
posFile = "",
|
||||
posLine = 1,
|
||||
posColumn = 1
|
||||
}
|
||||
|
||||
data Comment = Comment {
|
||||
cSeverity :: Severity,
|
||||
cCode :: Code,
|
||||
cMessage :: String
|
||||
} deriving (Show, Eq, Generic, NFData)
|
||||
|
||||
newComment :: Comment
|
||||
newComment = Comment {
|
||||
cSeverity = StyleC,
|
||||
cCode = 0,
|
||||
cMessage = ""
|
||||
}
|
||||
|
||||
-- only support single line for now
|
||||
data Replacement = Replacement {
|
||||
repStartPos :: Position,
|
||||
repEndPos :: Position,
|
||||
repString :: String
|
||||
} deriving (Show, Eq, Generic, NFData)
|
||||
|
||||
newReplacement = Replacement {
|
||||
repStartPos = newPosition,
|
||||
repEndPos = newPosition,
|
||||
repString = ""
|
||||
}
|
||||
|
||||
data Fix = Fix {
|
||||
fixReplacements :: [Replacement]
|
||||
} deriving (Show, Eq, Generic, NFData)
|
||||
|
||||
newFix = Fix {
|
||||
fixReplacements = []
|
||||
}
|
||||
|
||||
data PositionedComment = PositionedComment {
|
||||
pcStartPos :: Position,
|
||||
pcEndPos :: Position,
|
||||
pcComment :: Comment,
|
||||
pcFix :: Maybe Fix
|
||||
} deriving (Show, Eq, Generic, NFData)
|
||||
|
||||
newPositionedComment :: PositionedComment
|
||||
newPositionedComment = PositionedComment {
|
||||
pcStartPos = newPosition,
|
||||
pcEndPos = newPosition,
|
||||
pcComment = newComment,
|
||||
pcFix = Nothing
|
||||
}
|
||||
|
||||
data TokenComment = TokenComment {
|
||||
tcId :: Id,
|
||||
tcComment :: Comment,
|
||||
tcFix :: Maybe Fix
|
||||
} deriving (Show, Eq, Generic, NFData)
|
||||
|
||||
newTokenComment = TokenComment {
|
||||
tcId = Id 0,
|
||||
tcComment = newComment,
|
||||
tcFix = Nothing
|
||||
}
|
||||
|
||||
data ColorOption =
|
||||
ColorAuto
|
||||
| ColorAlways
|
||||
| ColorNever
|
||||
deriving (Ord, Eq, Show)
|
||||
|
||||
-- For testing
|
||||
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
|
||||
mockedSystemInterface files = SystemInterface {
|
||||
siReadFile = rf
|
||||
}
|
||||
where
|
||||
rf file =
|
||||
case filter ((== file) . fst) files of
|
||||
[] -> return $ Left "File not included in mock."
|
||||
[(_, contents)] -> return $ Right contents
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
|
34
stack.yaml
34
stack.yaml
@@ -1,35 +1,3 @@
|
||||
# This file was automatically generated by stack init
|
||||
# For more information, see: http://docs.haskellstack.org/en/stable/yaml_configuration/
|
||||
|
||||
# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
|
||||
resolver: lts-5.5
|
||||
|
||||
# Local packages, usually specified by relative directory name
|
||||
resolver: lts-12.9
|
||||
packages:
|
||||
- '.'
|
||||
# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3)
|
||||
extra-deps: []
|
||||
|
||||
# Override default flag values for local packages and extra-deps
|
||||
flags: {}
|
||||
|
||||
# Extra package databases containing global packages
|
||||
extra-package-dbs: []
|
||||
|
||||
# Control whether we use the GHC we find on the path
|
||||
# system-ghc: true
|
||||
|
||||
# Require a specific version of stack, using version ranges
|
||||
# require-stack-version: -any # Default
|
||||
# require-stack-version: >= 1.0.0
|
||||
|
||||
# Override the architecture used by stack, especially useful on Windows
|
||||
# arch: i386
|
||||
# arch: x86_64
|
||||
|
||||
# Extra directories used by stack for building
|
||||
# extra-include-dirs: [/path/to/dir]
|
||||
# extra-lib-dirs: [/path/to/dir]
|
||||
|
||||
# Allow a newer minor version of GHC than the snapshot specifies
|
||||
# compiler-check: newer-minor
|
||||
|
2
striptests
Executable file
2
striptests
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
# This file was deprecated by the doctest build.
|
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"
|
12
test/doctests.hs
Normal file
12
test/doctests.hs
Normal file
@@ -0,0 +1,12 @@
|
||||
module Main where
|
||||
|
||||
import Build_doctests (flags, pkgs, module_sources)
|
||||
import Data.Foldable (traverse_)
|
||||
import Test.DocTest (doctest)
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
traverse_ putStrLn args
|
||||
doctest args
|
||||
where
|
||||
args = flags ++ pkgs ++ module_sources
|
@@ -1,22 +0,0 @@
|
||||
module Main where
|
||||
|
||||
import Control.Monad
|
||||
import System.Exit
|
||||
import qualified ShellCheck.Checker
|
||||
import qualified ShellCheck.Analytics
|
||||
import qualified ShellCheck.AnalyzerLib
|
||||
import qualified ShellCheck.Parser
|
||||
import qualified ShellCheck.Checks.Commands
|
||||
|
||||
main = do
|
||||
putStrLn "Running ShellCheck tests..."
|
||||
results <- sequence [
|
||||
ShellCheck.Checker.runTests,
|
||||
ShellCheck.Checks.Commands.runTests,
|
||||
ShellCheck.Analytics.runTests,
|
||||
ShellCheck.AnalyzerLib.runTests,
|
||||
ShellCheck.Parser.runTests
|
||||
]
|
||||
if and results
|
||||
then exitSuccess
|
||||
else exitFailure
|
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