mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 00:39:19 +08:00
Compare commits
535 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
5efb724a3e | ||
|
619b6c42f3 | ||
|
88c56ecd53 | ||
|
6b62b5bf7e | ||
|
8672af29ef | ||
|
1a8e34bfea | ||
|
b0dae063bf | ||
|
4fc4899803 | ||
|
cd4896192c | ||
|
868d53af95 | ||
|
6a4b86cbea | ||
|
fe2398edc9 | ||
|
3a7dc86de1 | ||
|
1c0ec9c6f6 | ||
|
84110dbef4 | ||
|
1a7e98beaf | ||
|
a5d5831a2e | ||
|
47201822f9 | ||
|
32689ef5eb | ||
|
87481dce25 | ||
|
a90b6d14b3 | ||
|
5a46eeb09a | ||
|
47a7065a7a | ||
|
dbafbb3b3b | ||
|
13a2070a32 | ||
|
fa4874c044 | ||
|
6a71ff6f46 | ||
|
36263fb3f5 | ||
|
6dc419bbf5 | ||
|
7af3470a91 | ||
|
42f7479fb8 | ||
|
50084c06c5 | ||
|
e3bef9dc97 | ||
|
6c1abb2dee | ||
|
43c26061b9 | ||
|
07fd5724b8 | ||
|
eb2472ada8 | ||
|
3e5ecaa262 | ||
|
e1cec6c5d3 | ||
|
eaa319ec57 | ||
|
717b5e91f5 | ||
|
7f5f5b7fb5 | ||
|
856d57f7d8 | ||
|
c45e9d4878 | ||
|
89c6f6c800 | ||
|
85e69f86eb | ||
|
47fd16b8e8 | ||
|
1d04754b37 | ||
|
13ff0a7432 | ||
|
40136fe249 | ||
|
86999ded1f | ||
|
7551a241ad | ||
|
2f0ae44de4 | ||
|
51d8caf2c9 | ||
|
f835c2d4c1 | ||
|
db0c8c2dc9 | ||
|
9911470d67 | ||
|
a5821c3a4d | ||
|
c91083354f | ||
|
2957fb64c9 | ||
|
459e30804f | ||
|
49569e10e6 | ||
|
ba0221a1da | ||
|
944313c6ba | ||
|
6af1aeb259 | ||
|
b7c9d23452 | ||
|
e792d69293 | ||
|
4d8f2eb707 | ||
|
8a3bd25f7c | ||
|
825c1b5d22 | ||
|
92473b512a | ||
|
7e75d12ce1 | ||
|
7d278c3ca1 | ||
|
5f1175fb58 | ||
|
257b794322 | ||
|
89572d3a96 | ||
|
15edcbd4d5 | ||
|
736febaa3c | ||
|
a21df2d88f | ||
|
d473fb8867 | ||
|
f754363733 | ||
|
ef1f8f535e | ||
|
f9909504dd | ||
|
fa4cefda9d | ||
|
f2f6c66902 | ||
|
1f4dd85548 | ||
|
528381796e | ||
|
ad7ad28246 | ||
|
33ab998b02 | ||
|
9c28237d52 | ||
|
e0e5ba3a90 | ||
|
b4390414ef | ||
|
8acd5b13cd | ||
|
d00ca0c283 | ||
|
8bc98d89a7 | ||
|
c7964a7a78 | ||
|
8ec87d6655 | ||
|
c3df2bf761 | ||
|
d1df3713ca | ||
|
23496e93b0 | ||
|
437e69fbba | ||
|
63ad3f99ad | ||
|
0044c3dd6e | ||
|
a3d4101d6c | ||
|
bd359c5c0f | ||
|
498de63337 | ||
|
52ab7dee2d | ||
|
1a5296659b | ||
|
a66ee2967c | ||
|
d985380f48 | ||
|
6739c4a729 | ||
|
7415c9dcb7 | ||
|
d3fc1f355d | ||
|
48fd793581 | ||
|
e5842e2e2b | ||
|
cf445c7d20 | ||
|
ffb9578a98 | ||
|
630f20e888 | ||
|
8f5f91f041 | ||
|
8d9d4533c3 | ||
|
a4b4954a23 | ||
|
38cea9201d | ||
|
4ce916ec1d | ||
|
b9cb040128 | ||
|
2488be7298 | ||
|
d01b59a827 | ||
|
f77821625c | ||
|
1eece5b2ee | ||
|
58d45e3fa4 | ||
|
5aaa1a7d9a | ||
|
3b36c2c820 | ||
|
55692926b9 | ||
|
4172722167 | ||
|
485593da2c | ||
|
1181c6b3af | ||
|
ee181cfc43 | ||
|
c72667407b | ||
|
5467a0f1d9 | ||
|
3fc77d94ec | ||
|
23e0420cb1 | ||
|
a898165ac7 | ||
|
ba5e3db31a | ||
|
56145217fe | ||
|
94d265ce41 | ||
|
0f00de80fd | ||
|
c808c9b6fe | ||
|
bf9297e2a5 | ||
|
7f547cc0ec | ||
|
01c27dc96a | ||
|
856a204ec3 | ||
|
f054e2e2cc | ||
|
090e09e4ca | ||
|
10276c878d | ||
|
ae4aea4530 | ||
|
d0029ae1d4 | ||
|
eea7bc326e | ||
|
73cd2cdd6f | ||
|
a01862bc12 | ||
|
ccb6bf1ed5 | ||
|
136b654867 | ||
|
f31c8bd3a3 | ||
|
0dd61b65d8 | ||
|
07747b30fb | ||
|
26d16eb8ad | ||
|
54b2d14847 | ||
|
f653362b18 | ||
|
f85441add9 | ||
|
67cfcfd206 | ||
|
72eeafe002 | ||
|
6d9e8472e6 | ||
|
47d68019e5 | ||
|
cbda90eeb5 | ||
|
722b0606e8 | ||
|
95cfd87589 | ||
|
0a1beb883f | ||
|
83adcba88e | ||
|
35fb5073f4 | ||
|
de59c3586b | ||
|
8894333556 | ||
|
b1843c520f | ||
|
d406ba9950 | ||
|
d5dfb4a7c1 | ||
|
7929a9dbba | ||
|
7e84ad031f | ||
|
7eef12102b | ||
|
0522a5f0bd | ||
|
6c21e4671b | ||
|
3d83b87c9a | ||
|
f86d68bcc0 | ||
|
1e65d36874 | ||
|
1ff67a61b4 | ||
|
349dfdab35 | ||
|
1ab29ddb39 | ||
|
09b7788412 | ||
|
ef2135f3aa | ||
|
d10c3b2709 | ||
|
ca37794b7c | ||
|
8b8b48ef55 | ||
|
aea0310a07 | ||
|
7fff088ce9 | ||
|
65ab8c8ecb | ||
|
3a041954d1 | ||
|
828378cdff | ||
|
509cda4dcf | ||
|
6076f0b1da | ||
|
1d26c280d6 | ||
|
c785d43e34 | ||
|
4c3e731445 | ||
|
3940462da3 | ||
|
bb7ef5834b | ||
|
2f7bd556e8 | ||
|
081751c1b5 | ||
|
cc86aab3f1 | ||
|
9f1f00cdd1 | ||
|
93debd3556 | ||
|
47b971c582 | ||
|
f25ae90746 | ||
|
3daa47c0f2 | ||
|
ed56a837c3 | ||
|
80cf5d9852 | ||
|
8e554ae3d4 | ||
|
0a80188363 | ||
|
0e1a64b6ba | ||
|
0a2cf208c8 | ||
|
dcc10bbdf6 | ||
|
2c2e41952f | ||
|
0d74140650 | ||
|
955ad60823 | ||
|
2573332d77 | ||
|
00c470f323 | ||
|
63188282e9 | ||
|
61b4b65184 | ||
|
39b2bf4378 | ||
|
2fe117728d | ||
|
cde3ba8769 | ||
|
33c78b7c95 | ||
|
a485482979 | ||
|
895d83afc5 | ||
|
39bc011757 | ||
|
fe0a398239 | ||
|
1be0f1ea75 | ||
|
c9aa133282 | ||
|
7b70500d41 | ||
|
8bed447411 | ||
|
22710bf4d8 | ||
|
a354685ab1 | ||
|
a8ff7a02fd | ||
|
c5479b8ca3 | ||
|
d9dd58bec8 | ||
|
af1bb93aba | ||
|
e909c8ac42 | ||
|
93140e31a0 | ||
|
97f3834852 | ||
|
0369f43bac | ||
|
eb2eae2888 | ||
|
30c0c1f27d | ||
|
bff5d11566 | ||
|
eccb9f3f71 | ||
|
2814572116 | ||
|
90bafb9aba | ||
|
39b88bbaac | ||
|
39805ab200 | ||
|
9dadce96c0 | ||
|
1a0e208cc3 | ||
|
a69e27b774 | ||
|
b05c12223f | ||
|
38ead0385b | ||
|
9e8a11e57c | ||
|
6b84b35ec0 | ||
|
669fdf8e5e | ||
|
dccfb3c4a1 | ||
|
40ce949a56 | ||
|
9f3802138f | ||
|
2f3533fff6 | ||
|
f9c346cfd7 | ||
|
5f7419ca37 | ||
|
8494509150 | ||
|
8ba1f2fdf2 | ||
|
dbadca9f61 | ||
|
0347ce1b7a | ||
|
7fbe66e1c6 | ||
|
b000b05507 | ||
|
39423ddf81 | ||
|
875c2d2aad | ||
|
64cc7c691a | ||
|
b9784cbcc0 | ||
|
1a3f6aadaf | ||
|
35756c2cd6 | ||
|
0fd351404f | ||
|
4caa7e7900 | ||
|
c11c0196d5 | ||
|
b035331d4a | ||
|
d13253973b | ||
|
d9c622ae33 | ||
|
aac7d76047 | ||
|
fc421adb45 | ||
|
e0d3c6923a | ||
|
9772ba9de4 | ||
|
3a944de606 | ||
|
3dd592a02a | ||
|
61531cbb10 | ||
|
d53087f056 | ||
|
39756b420e | ||
|
52d4efc951 | ||
|
5dac723593 | ||
|
2364fd58b6 | ||
|
cde364c97b | ||
|
98b790f87a | ||
|
726a4e5848 | ||
|
0a9ed917e7 | ||
|
b10d31c8b7 | ||
|
133c779701 | ||
|
b18ee3fdef | ||
|
3fcc6c44d8 | ||
|
d830a36bc8 | ||
|
1af23fd131 | ||
|
d21b3362b2 | ||
|
6cd454e88b | ||
|
0b5f6b9762 | ||
|
3824e9cfc2 | ||
|
fdce0116da | ||
|
b069f7ed27 | ||
|
c4181d45d2 | ||
|
680f838c63 | ||
|
e6d81ca7b7 | ||
|
fd909eeca0 | ||
|
deab146fab | ||
|
f9aeabc245 | ||
|
558d8ffc6c | ||
|
e96c4c3ffa | ||
|
c566efd442 | ||
|
47c220d59c | ||
|
4bd902c5c4 | ||
|
033ce6d941 | ||
|
6ad3f557fe | ||
|
d0bad6c057 | ||
|
58c362f97c | ||
|
5f568dd207 | ||
|
2c1e414ac5 | ||
|
6699109ab8 | ||
|
423ca82296 | ||
|
0a263579e0 | ||
|
d63406abe4 | ||
|
81956d324d | ||
|
f549aad809 | ||
|
f9f965693d | ||
|
727d940e10 | ||
|
c26c2b8536 | ||
|
d8878ed852 | ||
|
c3cc5f649f | ||
|
8bd4365cdb | ||
|
a00a6fb53b | ||
|
3332eba9a0 | ||
|
ad08bb64aa | ||
|
f01e6e1a99 | ||
|
de0145fb29 | ||
|
0d4ae95e1d | ||
|
50db49e2fb | ||
|
60aafae21d | ||
|
902cb9c303 | ||
|
4f1fd43360 | ||
|
ca5af5c55a | ||
|
503cac3bb3 | ||
|
2a9c9ae0ad | ||
|
def4551991 | ||
|
67f4a0d6eb | ||
|
f92f934688 | ||
|
d4059c30b7 | ||
|
b68de7f42b | ||
|
7dacb62d36 | ||
|
3423cde931 | ||
|
b2d1aa01f7 | ||
|
19e1bdf11f | ||
|
75d51087c8 | ||
|
ed524fb77f | ||
|
97045c4af1 | ||
|
1b806f6c9f | ||
|
632c1614a1 | ||
|
00d9ef12e7 | ||
|
d07294810b | ||
|
948b750754 | ||
|
41ae95116d | ||
|
bf3c942294 | ||
|
055b40462d | ||
|
b087b7efb1 | ||
|
5d8d57cf07 | ||
|
661091a9da | ||
|
2ec60c2627 | ||
|
8b4909b238 | ||
|
95a3be6546 | ||
|
968e34e002 | ||
|
197b3e3f20 | ||
|
0e464ea476 | ||
|
811df6f0da | ||
|
4e5d32b05a | ||
|
c5141b77bf | ||
|
9dfeb6b42a | ||
|
77916d2645 | ||
|
4968e7d9ff | ||
|
075d58ee90 | ||
|
6a4a5a815e | ||
|
76a39f254b | ||
|
8ec9fa43fd | ||
|
e8634a3c27 | ||
|
9ae776530b | ||
|
0ec62390d5 | ||
|
82328cd86e | ||
|
5b58da7249 | ||
|
8676517270 | ||
|
4262c4b1bf | ||
|
7ad0110443 | ||
|
e9bba2f75a | ||
|
74ea5eaeec | ||
|
b7ee5f4410 | ||
|
e294db171e | ||
|
8c3d8d7cfa | ||
|
380d6c3317 | ||
|
16bd52333a | ||
|
cfb44b3fe2 | ||
|
43ed5e748d | ||
|
4dca88aade | ||
|
1d2c7a8551 | ||
|
ba080e7e34 | ||
|
fc716738eb | ||
|
659709d529 | ||
|
5b4729d940 | ||
|
b936f28763 | ||
|
78d9a7ad97 | ||
|
d540a98d33 | ||
|
8c00850134 | ||
|
d1990e3396 | ||
|
91fc4a046c | ||
|
95ebe1cd07 | ||
|
27822a1f56 | ||
|
eb06b06475 | ||
|
5d72432046 | ||
|
da51b14789 | ||
|
7be8485b8b | ||
|
a4d36ba0d2 | ||
|
d4bc0f6e10 | ||
|
1011ae7b3c | ||
|
d603ee1e89 | ||
|
4fc518c877 | ||
|
7fda86d6e2 | ||
|
6905373b6c | ||
|
1d8401d583 | ||
|
a89aee1a34 | ||
|
4853dce3fe | ||
|
a793e09bab | ||
|
fbd85e93ee | ||
|
77f754fa32 | ||
|
01d557abe6 | ||
|
68cc00b6e8 | ||
|
8b7c0be06f | ||
|
473bb666d8 | ||
|
376d407ea1 | ||
|
2e13cedc4b | ||
|
17515ad706 | ||
|
d8b5d6393a | ||
|
d404bc703d | ||
|
e5e08df1d9 | ||
|
1988cba147 | ||
|
4cee7fd27f | ||
|
b75fe02aac | ||
|
83c3dd3418 | ||
|
020850dbbb | ||
|
8d265aa25e | ||
|
c343217fd2 | ||
|
71bc26aefa | ||
|
8a3d259ae6 | ||
|
3a9ae0ebf1 | ||
|
d6b903e6cc |
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"):
|
||||||
|
- [ ] I tried on shellcheck.net and verified that this is still a problem on the latest commit
|
||||||
|
- [ ] It's not reproducible on shellcheck.net, but I think that's because it's an OS, configuration or encoding issue
|
||||||
|
|
||||||
|
#### For new checks and feature suggestions
|
||||||
|
- [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this
|
||||||
|
- [ ] 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:
|
||||||
|
|
||||||
|
|
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Created by http://www.gitignore.io
|
||||||
|
|
||||||
|
### Haskell ###
|
||||||
|
dist
|
||||||
|
cabal-dev
|
||||||
|
*.o
|
||||||
|
*.hi
|
||||||
|
*.chi
|
||||||
|
*.chs.h
|
||||||
|
.virtualenv
|
||||||
|
.hsenv
|
||||||
|
.cabal-sandbox/
|
||||||
|
cabal.sandbox.config
|
||||||
|
cabal.config
|
||||||
|
.stack-work
|
21
.travis.yml
Normal file
21
.travis.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
sudo: required
|
||||||
|
|
||||||
|
language: sh
|
||||||
|
|
||||||
|
services:
|
||||||
|
- docker
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- export DOCKER_REPO=koalaman/shellcheck
|
||||||
|
- |-
|
||||||
|
export TAG=$([ "$TRAVIS_BRANCH" == "master" ] && echo "latest" || ([ -n "$TRAVIS_TAG" ] && echo "$TRAVIS_TAG") || echo "$TRAVIS_BRANCH")
|
||||||
|
|
||||||
|
script:
|
||||||
|
- docker build -t builder -f Dockerfile_builder .
|
||||||
|
- docker run --rm -it -v $(pwd):/mnt builder
|
||||||
|
- docker build -t $DOCKER_REPO:$TAG .
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
||||||
|
- |-
|
||||||
|
([ "$TRAVIS_BRANCH" == "master" ] || [ -n "$TRAVIS_TAG" ]) && docker push "$DOCKER_REPO:$TAG"
|
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
MAINTAINER Nikyle Nguyen <NLKNguyen@MSN.com>
|
||||||
|
|
||||||
|
COPY package/bin/shellcheck /usr/local/bin/
|
||||||
|
COPY package/lib/ /usr/local/lib/
|
||||||
|
|
||||||
|
RUN ldconfig /usr/local/lib
|
||||||
|
|
||||||
|
WORKDIR /mnt
|
||||||
|
ENTRYPOINT ["shellcheck"]
|
54
Dockerfile_builder
Normal file
54
Dockerfile_builder
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
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/"]
|
141
LICENSE
141
LICENSE
@@ -1,5 +1,5 @@
|
|||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 19 November 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
@@ -7,15 +7,17 @@
|
|||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU Affero General Public License is a free, copyleft license for
|
The GNU General Public License is a free, copyleft license for
|
||||||
software and other kinds of works, specifically designed to ensure
|
software and other kinds of works.
|
||||||
cooperation with the community in the case of network server software.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
our General Public Licenses are intended to guarantee your freedom to
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
share and change all versions of a program--to make sure it remains free
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users.
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
@@ -24,34 +26,44 @@ them if you wish), that you receive source code or can get it if you
|
|||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
To protect your rights, we need to prevent others from denying you
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
you this License which gives you legal permission to copy, distribute
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
and/or modify the software.
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
For example, if you distribute copies of such a program, whether
|
||||||
improvements made in alternate versions of the program, if they
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
receive widespread use, become available for other developers to
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
incorporate. Many developers of free software are heartened and
|
or can get the source code. And you must show them these terms so they
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
know their rights.
|
||||||
software used on network servers, this result may fail to come about.
|
|
||||||
The GNU General Public License permits making a modified version and
|
|
||||||
letting the public access it on a server without ever releasing its
|
|
||||||
source code to the public.
|
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
ensure that, in such cases, the modified source code becomes available
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
to the community. It requires the operator of a network server to
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
provide the source code of the modified version running there to the
|
|
||||||
users of that server. Therefore, public use of a modified version, on
|
|
||||||
a publicly accessible server, gives the public access to the source
|
|
||||||
code of the modified version.
|
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
that there is no warranty for this free software. For both users' and
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
changed, so that their problems will not be attributed erroneously to
|
||||||
this license.
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
@@ -60,7 +72,7 @@ modification follow.
|
|||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
@@ -537,45 +549,35 @@ to collect a royalty for further conveying from those to whom you convey
|
|||||||
the Program, the only way you could satisfy both those terms and this
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU General Public License into a single
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
License will continue to apply to the part which is the covered work,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the work with which it is combined will remain governed by version
|
but the special requirements of the GNU Affero General Public License,
|
||||||
3 of the GNU General Public License.
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU Affero General Public License from time to time. Such new versions
|
the GNU General Public License from time to time. Such new versions will
|
||||||
will be similar in spirit to the present version, but may differ in detail to
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU Affero General
|
Program specifies that a certain numbered version of the GNU General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU Affero General Public License, you may choose any version ever published
|
GNU General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU Affero General Public License can be used, that proxy's
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
@@ -633,29 +635,40 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU Affero General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
If the program does terminal interaction, make it output a short
|
||||||
network, you should also make sure that it provides a way for users to
|
notice like this when it starts in an interactive mode:
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
<program> Copyright (C) <year> <name of author>
|
||||||
of the code. There are many ways you could offer source, and different
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
solutions will be better for different programs; see section 13 for the
|
This is free software, and you are welcome to redistribute it
|
||||||
specific requirements.
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
<http://www.gnu.org/licenses/>.
|
<http://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>.
|
||||||
|
23
Makefile
23
Makefile
@@ -1,23 +0,0 @@
|
|||||||
# TODO: Phase out Makefile in favor of Cabal
|
|
||||||
|
|
||||||
GHCFLAGS=-O9
|
|
||||||
|
|
||||||
all: shellcheck jsoncheck .tests
|
|
||||||
: Done
|
|
||||||
|
|
||||||
shellcheck: regardless
|
|
||||||
: Conditionally compiling shellcheck
|
|
||||||
ghc $(GHCFLAGS) --make shellcheck
|
|
||||||
|
|
||||||
jsoncheck: regardless
|
|
||||||
: Conditionally compiling shellcheck
|
|
||||||
ghc $(GHCFLAGS) --make jsoncheck
|
|
||||||
|
|
||||||
.tests: *.hs */*.hs
|
|
||||||
: Running unit tests
|
|
||||||
./test/runQuack && touch .tests
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -f .tests shellcheck jsoncheck *.hi *.o ShellCheck/*.hi ShellCheck/*.o
|
|
||||||
|
|
||||||
regardless:
|
|
34
README
34
README
@@ -1,34 +0,0 @@
|
|||||||
ShellCheck - A shell script static analysis tool
|
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
|
||||||
|
|
||||||
Copyright 2012, Vidar 'koala_man' Holen
|
|
||||||
Licensed under the GNU Affero General Public License, v3
|
|
||||||
|
|
||||||
The goals of ShellCheck are:
|
|
||||||
|
|
||||||
- To point out and clarify typical beginner's syntax issues,
|
|
||||||
that causes a shell to give cryptic error messages.
|
|
||||||
|
|
||||||
- To point out and clarify typical intermediate level semantic problems,
|
|
||||||
that causes 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.
|
|
||||||
|
|
||||||
ShellCheck is written in Haskell, and requires GHC, Parsec3 and Text.Regex.
|
|
||||||
To build the JSON interface and run the unit tests, it also requires QuickCheck2 and JSON.
|
|
||||||
|
|
||||||
On Ubuntu and similar, these are called:
|
|
||||||
ghc libghc-parsec3-dev libghc-json-dev libghc-regex-compat-dev libghc-quickcheck2-dev
|
|
||||||
For older releases, you may have to use:
|
|
||||||
ghc6 libghc6-parsec3-dev libghc6-quickcheck2-dev libghc6-json-dev libghc-regex-compat-dev
|
|
||||||
|
|
||||||
Executables can be built with cabal. Tests currently still rely on a Makefile.
|
|
||||||
|
|
||||||
Install:
|
|
||||||
cabal install
|
|
||||||
|
|
||||||
which shellcheck
|
|
||||||
~/.cabal/bin/shellcheck
|
|
||||||
|
|
||||||
Happy ShellChecking!
|
|
352
README.md
Normal file
352
README.md
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
# 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 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.
|
||||||
|
|
||||||
|
See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for examples of what ShellCheck can help you identify!
|
||||||
|
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
There are a variety of ways to use ShellCheck!
|
||||||
|
|
||||||
|
|
||||||
|
#### On the web
|
||||||
|
Paste a shell script on http://www.shellcheck.net for instant feedback.
|
||||||
|
|
||||||
|
[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!
|
||||||
|
|
||||||
|
|
||||||
|
#### From your terminal
|
||||||
|
Run `shellcheck yourscript` in your terminal for instant output, as seen above.
|
||||||
|
|
||||||
|
|
||||||
|
#### In your editor
|
||||||
|
|
||||||
|
You can see ShellCheck suggestions directly in a variety of editors.
|
||||||
|
|
||||||
|
* Vim, through [ALE](https://github.com/w0rp/ale) or [Syntastic](https://github.com/scrooloose/syntastic):
|
||||||
|
|
||||||
|
.
|
||||||
|
|
||||||
|
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck):
|
||||||
|
|
||||||
|
.
|
||||||
|
|
||||||
|
* Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck).
|
||||||
|
|
||||||
|
* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
|
||||||
|
|
||||||
|
* Most other editors, through [GCC error compatibility](shellcheck.1.md#user-content-formats).
|
||||||
|
|
||||||
|
|
||||||
|
#### In your build or test suites
|
||||||
|
While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Travis CI Setup
|
||||||
|
|
||||||
|
If you want to use ShellCheck in Travis CI, setting it up is simple :tada:.
|
||||||
|
|
||||||
|
```yml
|
||||||
|
language: bash
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
sources:
|
||||||
|
- debian-sid # Grab ShellCheck from the Debian repo
|
||||||
|
packages:
|
||||||
|
- shellcheck
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installing
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
On Debian based distros:
|
||||||
|
|
||||||
|
apt-get install shellcheck
|
||||||
|
|
||||||
|
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 OS X with homebrew:
|
||||||
|
|
||||||
|
brew install shellcheck
|
||||||
|
|
||||||
|
On OS X with MacPorts:
|
||||||
|
|
||||||
|
port install shellcheck
|
||||||
|
|
||||||
|
On openSUSE:Tumbleweed:
|
||||||
|
|
||||||
|
zypper in ShellCheck
|
||||||
|
|
||||||
|
On other openSUSE distributions:
|
||||||
|
|
||||||
|
add OBS devel:languages:haskell repository from https://build.opensuse.org/project/repositories/devel:languages:haskell
|
||||||
|
|
||||||
|
zypper ar http://download.opensuse.org/repositories/devel:/languages:/haskell/openSUSE_$(version)/devel:languages:haskell.repo
|
||||||
|
zypper in ShellCheck
|
||||||
|
|
||||||
|
or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
|
||||||
|
|
||||||
|
From Docker Hub:
|
||||||
|
|
||||||
|
docker pull koalaman/shellcheck
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
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`).
|
||||||
|
|
||||||
|
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/
|
||||||
|
|
||||||
|
Verify that `cabal` is installed and update its dependency list with
|
||||||
|
|
||||||
|
$ cabal update
|
||||||
|
|
||||||
|
#### Compiling ShellCheck
|
||||||
|
|
||||||
|
`git clone` this repository, and `cd` to the ShellCheck source directory to build/install:
|
||||||
|
|
||||||
|
$ cabal install
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
Log out and in again, and verify that your PATH is set up correctly:
|
||||||
|
|
||||||
|
$ 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,
|
||||||
|
make sure to use a TrueType font, not a Raster font, and set the active
|
||||||
|
codepage to UTF-8 (65001) with `chcp`:
|
||||||
|
|
||||||
|
> chcp 65001
|
||||||
|
Active code page: 65001
|
||||||
|
|
||||||
|
In Powershell ISE, you may need to additionally update the output encoding:
|
||||||
|
|
||||||
|
> [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
#### 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 $(..)
|
||||||
|
[[ "$$file" == *.jpg ]] # Comparisons that can't succeed
|
||||||
|
(( 1 -lt 2 )) # Using test operators in ((..))
|
||||||
|
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
array=( [index] = value ) # Incorrect index initialization
|
||||||
|
echo "Argument 10 is $10" # Positional parameter misreference
|
||||||
|
if $(myfunction); then ..; fi # Wrapping commands in $()
|
||||||
|
else if othercondition; then .. # Using 'else if'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
|
||||||
|
|
||||||
|
#### 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`:
|
||||||
|
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Testimonials
|
||||||
|
|
||||||
|
> At first you're like "shellcheck is awesome" but then you're like "wtf are we still using bash"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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!
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Happy ShellChecking!
|
36
Setup.hs
Normal file
36
Setup.hs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import Distribution.PackageDescription (
|
||||||
|
HookedBuildInfo,
|
||||||
|
emptyHookedBuildInfo )
|
||||||
|
import Distribution.Simple (
|
||||||
|
Args,
|
||||||
|
UserHooks ( preSDist ),
|
||||||
|
defaultMainWithHooks,
|
||||||
|
simpleUserHooks )
|
||||||
|
import Distribution.Simple.Setup ( SDistFlags )
|
||||||
|
|
||||||
|
import System.Process ( system )
|
||||||
|
|
||||||
|
|
||||||
|
main = defaultMainWithHooks myHooks
|
||||||
|
where
|
||||||
|
myHooks = simpleUserHooks { preSDist = myPreSDist }
|
||||||
|
|
||||||
|
-- | 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:
|
||||||
|
--
|
||||||
|
-- /bin/sh: pandoc: command not found
|
||||||
|
--
|
||||||
|
-- Since the man page is listed in the Extra-Source-Files section of
|
||||||
|
-- our cabal file, a failure here should result in a failure to
|
||||||
|
-- create the distribution tarball (that's a good thing).
|
||||||
|
--
|
||||||
|
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
|
||||||
|
return emptyHookedBuildInfo
|
||||||
|
where
|
||||||
|
pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"
|
@@ -1,20 +1,97 @@
|
|||||||
Name: ShellCheck
|
Name: ShellCheck
|
||||||
Version: 0.2.0
|
Version: 0.4.6
|
||||||
Description: Shell script analysis tool
|
Synopsis: Shell script analysis tool
|
||||||
|
License: GPL-3
|
||||||
License-file: LICENSE
|
License-file: LICENSE
|
||||||
|
Category: Static Analysis
|
||||||
Author: Vidar Holen
|
Author: Vidar Holen
|
||||||
Maintainer: vidar@vidarholen.net
|
Maintainer: vidar@vidarholen.net
|
||||||
Homepage: http://www.shellcheck.net/
|
Homepage: http://www.shellcheck.net/
|
||||||
Build-Type: Simple
|
Build-Type: Custom
|
||||||
Cabal-Version: >= 1.2
|
Cabal-Version: >= 1.8
|
||||||
|
Bug-reports: https://github.com/koalaman/shellcheck/issues
|
||||||
|
Description:
|
||||||
|
The goals of ShellCheck are:
|
||||||
|
.
|
||||||
|
* To point out and clarify typical beginner's syntax issues,
|
||||||
|
that causes a shell to give cryptic error messages.
|
||||||
|
.
|
||||||
|
* To point out and clarify typical intermediate level semantic problems,
|
||||||
|
that causes 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.
|
||||||
|
|
||||||
|
Extra-Source-Files:
|
||||||
|
-- documentation
|
||||||
|
README.md
|
||||||
|
shellcheck.1.md
|
||||||
|
-- built with a cabal sdist hook
|
||||||
|
shellcheck.1
|
||||||
|
-- tests
|
||||||
|
test/shellcheck.hs
|
||||||
|
|
||||||
|
source-repository head
|
||||||
|
type: git
|
||||||
|
location: git://github.com/koalaman/shellcheck.git
|
||||||
|
|
||||||
library
|
library
|
||||||
build-depends: base >= 4, parsec, containers, regex-compat, mtl, directory
|
build-depends:
|
||||||
exposed-modules: ShellCheck.AST, ShellCheck.Data, ShellCheck.Parser, ShellCheck.Analytics, ShellCheck.Simple
|
base >= 4 && < 5,
|
||||||
|
containers,
|
||||||
|
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:
|
||||||
|
ShellCheck.AST
|
||||||
|
ShellCheck.ASTLib
|
||||||
|
ShellCheck.Analytics
|
||||||
|
ShellCheck.Analyzer
|
||||||
|
ShellCheck.AnalyzerLib
|
||||||
|
ShellCheck.Checker
|
||||||
|
ShellCheck.Checks.Commands
|
||||||
|
ShellCheck.Checks.ShellSupport
|
||||||
|
ShellCheck.Data
|
||||||
|
ShellCheck.Formatter.Format
|
||||||
|
ShellCheck.Formatter.CheckStyle
|
||||||
|
ShellCheck.Formatter.GCC
|
||||||
|
ShellCheck.Formatter.JSON
|
||||||
|
ShellCheck.Formatter.TTY
|
||||||
|
ShellCheck.Interface
|
||||||
|
ShellCheck.Parser
|
||||||
|
ShellCheck.Regex
|
||||||
|
other-modules:
|
||||||
|
Paths_ShellCheck
|
||||||
|
|
||||||
executable shellcheck
|
executable shellcheck
|
||||||
|
build-depends:
|
||||||
|
ShellCheck,
|
||||||
|
base >= 4 && < 5,
|
||||||
|
containers,
|
||||||
|
directory,
|
||||||
|
json,
|
||||||
|
mtl >= 2.2.1,
|
||||||
|
parsec,
|
||||||
|
regex-tdfa,
|
||||||
|
QuickCheck >= 2.7.4
|
||||||
main-is: shellcheck.hs
|
main-is: shellcheck.hs
|
||||||
|
|
||||||
executable jsoncheck
|
test-suite test-shellcheck
|
||||||
build-depends: json
|
type: exitcode-stdio-1.0
|
||||||
main-is: jsoncheck.hs
|
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
|
||||||
|
|
||||||
|
@@ -1,61 +1,70 @@
|
|||||||
{-
|
{-
|
||||||
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
http://www.vidarholen.net/contents/shellcheck
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
ShellCheck is distributed in the hope that it will be useful,
|
ShellCheck is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU Affero General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.AST where
|
module ShellCheck.AST where
|
||||||
|
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Control.Monad.Identity
|
import Control.Monad.Identity
|
||||||
import qualified Text.Regex as Re
|
import Text.Parsec
|
||||||
|
import qualified ShellCheck.Regex as Re
|
||||||
|
|
||||||
data Id = Id Int deriving (Show, Eq, Ord)
|
data Id = Id Int deriving (Show, Eq, Ord)
|
||||||
|
|
||||||
data Quoted = Quoted | Unquoted deriving (Show, Eq)
|
data Quoted = Quoted | Unquoted deriving (Show, Eq)
|
||||||
data Dashed = Dashed | Undashed deriving (Show, Eq)
|
data Dashed = Dashed | Undashed deriving (Show, Eq)
|
||||||
data AssignmentMode = Assign | Append deriving (Show, Eq)
|
data AssignmentMode = Assign | Append deriving (Show, Eq)
|
||||||
|
data FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
|
||||||
|
data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
|
||||||
|
data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
|
||||||
|
|
||||||
|
data Root = Root Token
|
||||||
data Token =
|
data Token =
|
||||||
TA_Base Id String Token
|
TA_Binary Id String Token Token
|
||||||
| TA_Binary Id String Token Token
|
| TA_Assignment Id String Token Token
|
||||||
| TA_Expansion Id Token
|
| TA_Expansion Id [Token]
|
||||||
| TA_Literal Id String
|
| TA_Index Id Token
|
||||||
| TA_Sequence Id [Token]
|
| TA_Sequence Id [Token]
|
||||||
| TA_Trinary Id Token Token Token
|
| TA_Trinary Id Token Token Token
|
||||||
| TA_Unary Id String Token
|
| TA_Unary Id String Token
|
||||||
| TA_Variable Id String
|
|
||||||
| TC_And Id ConditionType String Token Token
|
| TC_And Id ConditionType String Token Token
|
||||||
| TC_Binary Id ConditionType String Token Token
|
| TC_Binary Id ConditionType String Token Token
|
||||||
| TC_Group Id ConditionType Token
|
| TC_Group Id ConditionType Token
|
||||||
| TC_Noary Id ConditionType Token
|
| TC_Nullary Id ConditionType Token
|
||||||
| TC_Or Id ConditionType String Token Token
|
| TC_Or Id ConditionType String Token Token
|
||||||
| TC_Unary Id ConditionType String Token
|
| TC_Unary Id ConditionType String Token
|
||||||
| T_AND_IF Id
|
| T_AND_IF Id
|
||||||
| T_AndIf Id (Token) (Token)
|
| T_AndIf Id (Token) (Token)
|
||||||
| T_Arithmetic Id Token
|
| T_Arithmetic Id Token
|
||||||
| T_Array Id [Token]
|
| T_Array Id [Token]
|
||||||
| T_Assignment Id AssignmentMode String (Maybe Token) Token
|
| T_IndexedElement Id [Token] Token
|
||||||
|
-- Store the index as string, and parse as arithmetic or string later
|
||||||
|
| T_UnparsedIndex Id SourcePos String
|
||||||
|
| T_Assignment Id AssignmentMode String [Token] Token
|
||||||
| T_Backgrounded Id Token
|
| T_Backgrounded Id Token
|
||||||
| T_Backticked Id [Token]
|
| T_Backticked Id [Token]
|
||||||
| T_Bang Id
|
| T_Bang Id
|
||||||
| T_Banged Id Token
|
| T_Banged Id Token
|
||||||
| T_BraceExpansion Id String
|
| T_BraceExpansion Id [Token]
|
||||||
| T_BraceGroup Id [Token]
|
| T_BraceGroup Id [Token]
|
||||||
| T_CLOBBER Id
|
| T_CLOBBER Id
|
||||||
| T_Case Id
|
| T_Case Id
|
||||||
| T_CaseExpression Id Token [([Token],[Token])]
|
| T_CaseExpression Id Token [(CaseType, [Token], [Token])]
|
||||||
| T_Condition Id ConditionType Token
|
| T_Condition Id ConditionType Token
|
||||||
| T_DGREAT Id
|
| T_DGREAT Id
|
||||||
| T_DLESS Id
|
| T_DLESS Id
|
||||||
@@ -68,6 +77,7 @@ data Token =
|
|||||||
| T_DollarDoubleQuoted Id [Token]
|
| T_DollarDoubleQuoted Id [Token]
|
||||||
| T_DollarExpansion Id [Token]
|
| T_DollarExpansion Id [Token]
|
||||||
| T_DollarSingleQuoted Id String
|
| T_DollarSingleQuoted Id String
|
||||||
|
| T_DollarBraceCommandExpansion Id [Token]
|
||||||
| T_Done Id
|
| T_Done Id
|
||||||
| T_DoubleQuoted Id [Token]
|
| T_DoubleQuoted Id [Token]
|
||||||
| T_EOF Id
|
| T_EOF Id
|
||||||
@@ -80,7 +90,7 @@ data Token =
|
|||||||
| T_For Id
|
| T_For Id
|
||||||
| T_ForArithmetic Id Token Token Token [Token]
|
| T_ForArithmetic Id Token Token Token [Token]
|
||||||
| T_ForIn Id String [Token] [Token]
|
| T_ForIn Id String [Token] [Token]
|
||||||
| T_Function Id String Token
|
| T_Function Id FunctionKeyword FunctionParentheses String Token
|
||||||
| T_GREATAND Id
|
| T_GREATAND Id
|
||||||
| T_Glob Id String
|
| T_Glob Id String
|
||||||
| T_Greater Id
|
| T_Greater Id
|
||||||
@@ -90,6 +100,7 @@ data Token =
|
|||||||
| T_IfExpression Id [([Token],[Token])] [Token]
|
| T_IfExpression Id [([Token],[Token])] [Token]
|
||||||
| T_In Id
|
| T_In Id
|
||||||
| T_IoFile Id Token Token
|
| T_IoFile Id Token Token
|
||||||
|
| T_IoDuplicate Id Token String
|
||||||
| T_LESSAND Id
|
| T_LESSAND Id
|
||||||
| T_LESSGREAT Id
|
| T_LESSGREAT Id
|
||||||
| T_Lbrace Id
|
| T_Lbrace Id
|
||||||
@@ -100,7 +111,8 @@ data Token =
|
|||||||
| T_NormalWord Id [Token]
|
| T_NormalWord Id [Token]
|
||||||
| T_OR_IF Id
|
| T_OR_IF Id
|
||||||
| T_OrIf Id (Token) (Token)
|
| T_OrIf Id (Token) (Token)
|
||||||
| T_Pipeline Id [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_ProcSub Id String [Token]
|
||||||
| T_Rbrace Id
|
| T_Rbrace Id
|
||||||
| T_Redirecting Id [Token] Token
|
| T_Redirecting Id [Token] Token
|
||||||
@@ -117,25 +129,37 @@ data Token =
|
|||||||
| T_UntilExpression Id [Token] [Token]
|
| T_UntilExpression Id [Token] [Token]
|
||||||
| T_While Id
|
| T_While Id
|
||||||
| T_WhileExpression Id [Token] [Token]
|
| T_WhileExpression Id [Token] [Token]
|
||||||
|
| T_Annotation Id [Annotation] Token
|
||||||
|
| T_Pipe Id String
|
||||||
|
| T_CoProc Id (Maybe String) Token
|
||||||
|
| T_CoProcBody Id Token
|
||||||
|
| T_Include Id Token Token -- . & source: SimpleCommand T_Script
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
|
data Annotation =
|
||||||
|
DisableComment Integer
|
||||||
|
| SourceOverride String
|
||||||
|
| ShellOverride String
|
||||||
|
deriving (Show, Eq)
|
||||||
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
|
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
|
||||||
|
|
||||||
-- I apologize for nothing!
|
-- This is an abomination.
|
||||||
lolHax s = Re.subRegex (Re.mkRegex "(Id [0-9]+)") (show s) "(Id 0)"
|
tokenEquals :: Token -> Token -> Bool
|
||||||
|
tokenEquals a b = kludge a == kludge b
|
||||||
|
where kludge s = Re.subRegex (Re.mkRegex "\\(Id [0-9]+\\)") (show s) "(Id 0)"
|
||||||
|
|
||||||
instance Eq Token where
|
instance Eq Token where
|
||||||
(==) a b = (lolHax a) == (lolHax b)
|
(==) = tokenEquals
|
||||||
|
|
||||||
|
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> m Token) -> Token -> m Token
|
||||||
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> Token) -> Token -> m Token
|
analyze f g i =
|
||||||
analyze f g i t =
|
round
|
||||||
round t
|
|
||||||
where
|
where
|
||||||
round t = do
|
round t = do
|
||||||
f t
|
f t
|
||||||
newT <- delve t
|
newT <- delve t
|
||||||
g t
|
g t
|
||||||
return . i $ newT
|
i newT
|
||||||
roundAll = mapM round
|
roundAll = mapM round
|
||||||
|
|
||||||
roundMaybe Nothing = return Nothing
|
roundMaybe Nothing = return Nothing
|
||||||
@@ -149,7 +173,7 @@ analyze f g i t =
|
|||||||
dll l m v = do
|
dll l m v = do
|
||||||
x <- roundAll l
|
x <- roundAll l
|
||||||
y <- roundAll m
|
y <- roundAll m
|
||||||
return $ v x m
|
return $ v x y
|
||||||
d1 t v = do
|
d1 t v = do
|
||||||
x <- round t
|
x <- round t
|
||||||
return $ v x
|
return $ v x
|
||||||
@@ -162,23 +186,30 @@ analyze f g i t =
|
|||||||
delve (T_DoubleQuoted id list) = dl list $ T_DoubleQuoted id
|
delve (T_DoubleQuoted id list) = dl list $ T_DoubleQuoted id
|
||||||
delve (T_DollarDoubleQuoted id list) = dl list $ T_DollarDoubleQuoted id
|
delve (T_DollarDoubleQuoted id list) = dl list $ T_DollarDoubleQuoted id
|
||||||
delve (T_DollarExpansion id list) = dl list $ T_DollarExpansion id
|
delve (T_DollarExpansion id list) = dl list $ T_DollarExpansion id
|
||||||
|
delve (T_DollarBraceCommandExpansion id list) = dl list $ T_DollarBraceCommandExpansion id
|
||||||
|
delve (T_BraceExpansion id list) = dl list $ T_BraceExpansion id
|
||||||
delve (T_Backticked id list) = dl list $ T_Backticked id
|
delve (T_Backticked id list) = dl list $ T_Backticked id
|
||||||
delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id
|
delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id
|
||||||
delve (T_DollarBracket id c) = d1 c $ T_DollarBracket id
|
delve (T_DollarBracket id c) = d1 c $ T_DollarBracket id
|
||||||
delve (T_IoFile id op file) = d2 op file $ T_IoFile id
|
delve (T_IoFile id op file) = d2 op file $ T_IoFile id
|
||||||
|
delve (T_IoDuplicate id op num) = d1 op $ \x -> T_IoDuplicate id x num
|
||||||
delve (T_HereString id word) = d1 word $ T_HereString id
|
delve (T_HereString id word) = d1 word $ T_HereString id
|
||||||
delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v
|
delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v
|
||||||
delve (T_Assignment id mode var index value) = do
|
delve (T_Assignment id mode var indices value) = do
|
||||||
a <- roundMaybe index
|
a <- roundAll indices
|
||||||
b <- round value
|
b <- round value
|
||||||
return $ T_Assignment id mode var a b
|
return $ T_Assignment id mode var a b
|
||||||
delve (T_Array id t) = dl t $ T_Array id
|
delve (T_Array id t) = dl t $ T_Array id
|
||||||
|
delve (T_IndexedElement id indices t) = do
|
||||||
|
a <- roundAll indices
|
||||||
|
b <- round t
|
||||||
|
return $ T_IndexedElement id a b
|
||||||
delve (T_Redirecting id redirs cmd) = do
|
delve (T_Redirecting id redirs cmd) = do
|
||||||
a <- roundAll redirs
|
a <- roundAll redirs
|
||||||
b <- round cmd
|
b <- round cmd
|
||||||
return $ T_Redirecting id a b
|
return $ T_Redirecting id a b
|
||||||
delve (T_SimpleCommand id vars cmds) = dll vars cmds $ T_SimpleCommand id
|
delve (T_SimpleCommand id vars cmds) = dll vars cmds $ T_SimpleCommand id
|
||||||
delve (T_Pipeline id l) = dl l $ T_Pipeline id
|
delve (T_Pipeline id l1 l2) = dll l1 l2 $ T_Pipeline id
|
||||||
delve (T_Banged id l) = d1 l $ T_Banged id
|
delve (T_Banged id l) = d1 l $ T_Banged id
|
||||||
delve (T_AndIf id t u) = d2 t u $ T_AndIf id
|
delve (T_AndIf id t u) = d2 t u $ T_AndIf id
|
||||||
delve (T_OrIf id t u) = d2 t u $ T_OrIf id
|
delve (T_OrIf id t u) = d2 t u $ T_OrIf id
|
||||||
@@ -201,10 +232,10 @@ analyze f g i t =
|
|||||||
delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v
|
delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v
|
||||||
delve (T_CaseExpression id word cases) = do
|
delve (T_CaseExpression id word cases) = do
|
||||||
newWord <- round word
|
newWord <- round word
|
||||||
newCases <- mapM (\(c, t) -> do
|
newCases <- mapM (\(o, c, t) -> do
|
||||||
x <- mapM round c
|
x <- mapM round c
|
||||||
y <- mapM round t
|
y <- mapM round t
|
||||||
return (x,y)
|
return (o, x,y)
|
||||||
) cases
|
) cases
|
||||||
return $ T_CaseExpression id newWord newCases
|
return $ T_CaseExpression id newWord newCases
|
||||||
|
|
||||||
@@ -216,7 +247,7 @@ analyze f g i t =
|
|||||||
return $ T_ForArithmetic id x y z list
|
return $ T_ForArithmetic id x y z list
|
||||||
|
|
||||||
delve (T_Script id s l) = dl l $ T_Script id s
|
delve (T_Script id s l) = dl l $ T_Script id s
|
||||||
delve (T_Function id name body) = d1 body $ T_Function id name
|
delve (T_Function id a b name body) = d1 body $ T_Function id a b name
|
||||||
delve (T_Condition id typ token) = d1 token $ T_Condition id typ
|
delve (T_Condition id typ token) = d1 token $ T_Condition id typ
|
||||||
delve (T_Extglob id str l) = dl l $ T_Extglob id str
|
delve (T_Extglob id str l) = dl l $ T_Extglob id str
|
||||||
delve (T_DollarBraced id op) = d1 op $ T_DollarBraced id
|
delve (T_DollarBraced id op) = d1 op $ T_DollarBraced id
|
||||||
@@ -227,9 +258,10 @@ analyze f g i t =
|
|||||||
delve (TC_Group id typ token) = d1 token $ TC_Group id typ
|
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_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_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_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
|
||||||
delve (TA_Unary id op t1) = d1 t1 $ TA_Unary id op
|
delve (TA_Unary id op t1) = d1 t1 $ TA_Unary id op
|
||||||
delve (TA_Sequence id l) = dl l $ TA_Sequence id
|
delve (TA_Sequence id l) = dl l $ TA_Sequence id
|
||||||
delve (TA_Trinary id t1 t2 t3) = do
|
delve (TA_Trinary id t1 t2 t3) = do
|
||||||
@@ -237,8 +269,12 @@ analyze f g i t =
|
|||||||
b <- round t2
|
b <- round t2
|
||||||
c <- round t3
|
c <- round t3
|
||||||
return $ TA_Trinary id a b c
|
return $ TA_Trinary id a b c
|
||||||
delve (TA_Expansion id t) = d1 t $ TA_Expansion id
|
delve (TA_Expansion id t) = dl t $ TA_Expansion id
|
||||||
delve (TA_Base id b t) = d1 t $ TA_Base id b
|
delve (TA_Index id t) = d1 t $ TA_Index id
|
||||||
|
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 = return t
|
delve t = return t
|
||||||
|
|
||||||
getId t = case t of
|
getId t = case t of
|
||||||
@@ -284,15 +320,19 @@ getId t = case t of
|
|||||||
T_DollarBraced id _ -> id
|
T_DollarBraced id _ -> id
|
||||||
T_DollarArithmetic id _ -> id
|
T_DollarArithmetic id _ -> id
|
||||||
T_BraceExpansion id _ -> id
|
T_BraceExpansion id _ -> id
|
||||||
|
T_ParamSubSpecialChar id _ -> id
|
||||||
|
T_DollarBraceCommandExpansion id _ -> id
|
||||||
T_IoFile id _ _ -> id
|
T_IoFile id _ _ -> id
|
||||||
|
T_IoDuplicate id _ _ -> id
|
||||||
T_HereDoc id _ _ _ _ -> id
|
T_HereDoc id _ _ _ _ -> id
|
||||||
T_HereString id _ -> id
|
T_HereString id _ -> id
|
||||||
T_FdRedirect id _ _ -> id
|
T_FdRedirect id _ _ -> id
|
||||||
T_Assignment id _ _ _ _ -> id
|
T_Assignment id _ _ _ _ -> id
|
||||||
T_Array id _ -> id
|
T_Array id _ -> id
|
||||||
|
T_IndexedElement id _ _ -> id
|
||||||
T_Redirecting id _ _ -> id
|
T_Redirecting id _ _ -> id
|
||||||
T_SimpleCommand id _ _ -> id
|
T_SimpleCommand id _ _ -> id
|
||||||
T_Pipeline id _ -> id
|
T_Pipeline id _ _ -> id
|
||||||
T_Banged id _ -> id
|
T_Banged id _ -> id
|
||||||
T_AndIf id _ _ -> id
|
T_AndIf id _ _ -> id
|
||||||
T_OrIf id _ _ -> id
|
T_OrIf id _ _ -> id
|
||||||
@@ -305,7 +345,7 @@ getId t = case t of
|
|||||||
T_ForIn id _ _ _ -> id
|
T_ForIn id _ _ _ -> id
|
||||||
T_SelectIn id _ _ _ -> id
|
T_SelectIn id _ _ _ -> id
|
||||||
T_CaseExpression id _ _ -> id
|
T_CaseExpression id _ _ -> id
|
||||||
T_Function id _ _ -> id
|
T_Function id _ _ _ _ -> id
|
||||||
T_Arithmetic id _ -> id
|
T_Arithmetic id _ -> id
|
||||||
T_Script id _ _ -> id
|
T_Script id _ _ -> id
|
||||||
T_Condition id _ _ -> id
|
T_Condition id _ _ -> id
|
||||||
@@ -316,25 +356,30 @@ getId t = case t of
|
|||||||
TC_Group id _ _ -> id
|
TC_Group id _ _ -> id
|
||||||
TC_Binary id _ _ _ _ -> id
|
TC_Binary id _ _ _ _ -> id
|
||||||
TC_Unary id _ _ _ -> id
|
TC_Unary id _ _ _ -> id
|
||||||
TC_Noary id _ _ -> id
|
TC_Nullary id _ _ -> id
|
||||||
TA_Binary id _ _ _ -> id
|
TA_Binary id _ _ _ -> id
|
||||||
|
TA_Assignment id _ _ _ -> id
|
||||||
TA_Unary id _ _ -> id
|
TA_Unary id _ _ -> id
|
||||||
TA_Sequence id _ -> id
|
TA_Sequence id _ -> id
|
||||||
TA_Variable id _ -> id
|
|
||||||
TA_Trinary id _ _ _ -> id
|
TA_Trinary id _ _ _ -> id
|
||||||
TA_Expansion id _ -> id
|
TA_Expansion id _ -> id
|
||||||
TA_Literal id _ -> id
|
TA_Index id _ -> id
|
||||||
TA_Base id _ _ -> id
|
|
||||||
T_ProcSub id _ _ -> id
|
T_ProcSub id _ _ -> id
|
||||||
T_Glob id _ -> id
|
T_Glob id _ -> id
|
||||||
T_ForArithmetic id _ _ _ _ -> id
|
T_ForArithmetic id _ _ _ _ -> id
|
||||||
T_DollarSingleQuoted id _ -> id
|
T_DollarSingleQuoted id _ -> id
|
||||||
T_DollarDoubleQuoted id _ -> id
|
T_DollarDoubleQuoted id _ -> id
|
||||||
T_DollarBracket id _ -> id
|
T_DollarBracket id _ -> id
|
||||||
|
T_Annotation id _ _ -> id
|
||||||
|
T_Pipe id _ -> id
|
||||||
|
T_CoProc id _ _ -> id
|
||||||
|
T_CoProcBody id _ -> id
|
||||||
|
T_Include id _ _ -> id
|
||||||
|
T_UnparsedIndex id _ _ -> id
|
||||||
|
|
||||||
blank :: Monad m => Token -> m ()
|
blank :: Monad m => Token -> m ()
|
||||||
blank = const $ return ()
|
blank = const $ return ()
|
||||||
doAnalysis f t = analyze f blank id t
|
doAnalysis f = analyze f blank return
|
||||||
doStackAnalysis startToken endToken t = analyze startToken endToken id t
|
doStackAnalysis startToken endToken = analyze startToken endToken return
|
||||||
doTransform i t = runIdentity $ analyze blank blank i t
|
doTransform i = runIdentity . analyze blank blank (return . i)
|
||||||
|
|
||||||
|
386
ShellCheck/ASTLib.hs
Normal file
386
ShellCheck/ASTLib.hs
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
{-
|
||||||
|
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.ASTLib where
|
||||||
|
|
||||||
|
import ShellCheck.AST
|
||||||
|
|
||||||
|
import Control.Monad.Writer
|
||||||
|
import Control.Monad
|
||||||
|
import Data.Functor
|
||||||
|
import Data.List
|
||||||
|
import Data.Maybe
|
||||||
|
|
||||||
|
-- Is this a type of loop?
|
||||||
|
isLoop t = case t of
|
||||||
|
T_WhileExpression {} -> True
|
||||||
|
T_UntilExpression {} -> True
|
||||||
|
T_ForIn {} -> True
|
||||||
|
T_ForArithmetic {} -> True
|
||||||
|
T_SelectIn {} -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
-- Will this split into multiple words when used as an argument?
|
||||||
|
willSplit x =
|
||||||
|
case x of
|
||||||
|
T_DollarBraced {} -> True
|
||||||
|
T_DollarExpansion {} -> True
|
||||||
|
T_Backticked {} -> True
|
||||||
|
T_BraceExpansion {} -> True
|
||||||
|
T_Glob {} -> True
|
||||||
|
T_Extglob {} -> True
|
||||||
|
T_NormalWord _ l -> any willSplit l
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
isGlob (T_Extglob {}) = True
|
||||||
|
isGlob (T_Glob {}) = True
|
||||||
|
isGlob (T_NormalWord _ l) = any isGlob l
|
||||||
|
isGlob _ = False
|
||||||
|
|
||||||
|
-- Is this shell word a constant?
|
||||||
|
isConstant token =
|
||||||
|
case token of
|
||||||
|
-- This ignores some cases like ~"foo":
|
||||||
|
T_NormalWord _ (T_Literal _ ('~':_) : _) -> False
|
||||||
|
T_NormalWord _ l -> all isConstant l
|
||||||
|
T_DoubleQuoted _ l -> all isConstant l
|
||||||
|
T_SingleQuoted _ _ -> True
|
||||||
|
T_Literal _ _ -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
-- Is this an empty literal?
|
||||||
|
isEmpty token =
|
||||||
|
case token of
|
||||||
|
T_NormalWord _ l -> all isEmpty l
|
||||||
|
T_DoubleQuoted _ l -> all isEmpty l
|
||||||
|
T_SingleQuoted _ "" -> True
|
||||||
|
T_Literal _ "" -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
-- Quick&lazy oversimplification of commands, throwing away details
|
||||||
|
-- and returning a list like ["find", ".", "-name", "${VAR}*" ].
|
||||||
|
oversimplify token =
|
||||||
|
case token of
|
||||||
|
(T_NormalWord _ l) -> [concat (concatMap oversimplify l)]
|
||||||
|
(T_DoubleQuoted _ l) -> [concat (concatMap oversimplify l)]
|
||||||
|
(T_SingleQuoted _ s) -> [s]
|
||||||
|
(T_DollarBraced _ _) -> ["${VAR}"]
|
||||||
|
(T_DollarArithmetic _ _) -> ["${VAR}"]
|
||||||
|
(T_DollarExpansion _ _) -> ["${VAR}"]
|
||||||
|
(T_Backticked _ _) -> ["${VAR}"]
|
||||||
|
(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
|
||||||
|
_ -> []
|
||||||
|
|
||||||
|
|
||||||
|
-- Turn a SimpleCommand foo -avz --bar=baz into args "a", "v", "z", "bar",
|
||||||
|
-- each in a tuple of (token, stringFlag). Non-flag arguments are added with
|
||||||
|
-- stringFlag == "".
|
||||||
|
getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
|
||||||
|
let tokenAndText = map (\x -> (x, concat $ oversimplify x)) args
|
||||||
|
(flagArgs, rest) = break (stopCondition . snd) tokenAndText
|
||||||
|
in
|
||||||
|
concatMap flag flagArgs ++ map (\(t, _) -> (t, "")) rest
|
||||||
|
where
|
||||||
|
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
|
||||||
|
flag (x, '-':args) = map (\v -> (x, [v])) args
|
||||||
|
flag (x, _) = [ (x, "") ]
|
||||||
|
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
|
||||||
|
|
||||||
|
-- Get all flags in a GNU way, up until --
|
||||||
|
getAllFlags = getFlagsUntil (== "--")
|
||||||
|
-- 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)
|
||||||
|
|
||||||
|
|
||||||
|
-- Given a T_DollarBraced, return a simplified version of the string contents.
|
||||||
|
bracedString (T_DollarBraced _ l) = concat $ oversimplify l
|
||||||
|
bracedString _ = error "Internal shellcheck error, please report! (bracedString on non-variable)"
|
||||||
|
|
||||||
|
-- Is this an expansion of multiple items of an array?
|
||||||
|
isArrayExpansion t@(T_DollarBraced _ _) =
|
||||||
|
let string = bracedString t in
|
||||||
|
"@" `isPrefixOf` string ||
|
||||||
|
not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string
|
||||||
|
isArrayExpansion _ = False
|
||||||
|
|
||||||
|
-- Is it possible that this arg becomes multiple args?
|
||||||
|
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
|
||||||
|
where
|
||||||
|
f t@(T_DollarBraced _ _) =
|
||||||
|
let string = bracedString t in
|
||||||
|
"!" `isPrefixOf` string
|
||||||
|
f (T_DoubleQuoted _ parts) = any f parts
|
||||||
|
f (T_NormalWord _ parts) = any f parts
|
||||||
|
f _ = False
|
||||||
|
|
||||||
|
-- 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_DoubleQuoted _ parts) = any f parts
|
||||||
|
f (T_NormalWord _ parts) = any f parts
|
||||||
|
f _ = False
|
||||||
|
|
||||||
|
-- This does token cause implicit concatenation in assignments?
|
||||||
|
willConcatInAssignment token =
|
||||||
|
case token of
|
||||||
|
t@(T_DollarBraced {}) -> isArrayExpansion t
|
||||||
|
(T_DoubleQuoted _ parts) -> any willConcatInAssignment parts
|
||||||
|
(T_NormalWord _ parts) -> any willConcatInAssignment parts
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
-- Maybe get the literal string corresponding to this token
|
||||||
|
getLiteralString :: Token -> Maybe String
|
||||||
|
getLiteralString = getLiteralStringExt (const Nothing)
|
||||||
|
|
||||||
|
-- Definitely get a literal string, skipping over all non-literals
|
||||||
|
onlyLiteralString :: Token -> String
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
-- Maybe get the literal string of this token and any globs in it.
|
||||||
|
getGlobOrLiteralString = getLiteralStringExt f
|
||||||
|
where
|
||||||
|
f (T_Glob _ str) = return str
|
||||||
|
f _ = Nothing
|
||||||
|
|
||||||
|
-- Maybe get the literal value of a token, using a custom function
|
||||||
|
-- to map unrecognized Tokens into strings.
|
||||||
|
getLiteralStringExt :: (Token -> Maybe String) -> Token -> Maybe String
|
||||||
|
getLiteralStringExt more = g
|
||||||
|
where
|
||||||
|
allInList = liftM 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 x = more x
|
||||||
|
|
||||||
|
-- Is this token a string literal?
|
||||||
|
isLiteral t = isJust $ getLiteralString t
|
||||||
|
|
||||||
|
|
||||||
|
-- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz]
|
||||||
|
getWordParts (T_NormalWord _ l) = concatMap getWordParts l
|
||||||
|
getWordParts (T_DoubleQuoted _ l) = l
|
||||||
|
-- TA_Expansion is basically T_NormalWord for arithmetic expressions
|
||||||
|
getWordParts (TA_Expansion _ l) = concatMap getWordParts l
|
||||||
|
getWordParts other = [other]
|
||||||
|
|
||||||
|
-- Return a list of NormalWords that would result from brace expansion
|
||||||
|
braceExpand (T_NormalWord id list) = take 1000 $ do
|
||||||
|
items <- mapM part list
|
||||||
|
return $ T_NormalWord id items
|
||||||
|
where
|
||||||
|
part (T_BraceExpansion id items) = do
|
||||||
|
item <- items
|
||||||
|
braceExpand item
|
||||||
|
part x = return x
|
||||||
|
|
||||||
|
-- Maybe get a SimpleCommand from immediate wrappers like T_Redirections
|
||||||
|
getCommand t =
|
||||||
|
case t of
|
||||||
|
T_Redirecting _ _ w -> getCommand w
|
||||||
|
T_SimpleCommand _ _ (w:_) -> return t
|
||||||
|
T_Annotation _ _ t -> getCommand t
|
||||||
|
otherwise -> Nothing
|
||||||
|
|
||||||
|
-- Maybe get the command name of a token representing a command
|
||||||
|
getCommandName t = do
|
||||||
|
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
|
||||||
|
s <- getLiteralString w
|
||||||
|
if "busybox" `isSuffixOf` s
|
||||||
|
then
|
||||||
|
case rest of
|
||||||
|
(applet:_) -> getLiteralString applet
|
||||||
|
_ -> return s
|
||||||
|
else
|
||||||
|
return s
|
||||||
|
|
||||||
|
-- If a command substitution is a single command, get its name.
|
||||||
|
-- $(date +%s) = Just "date"
|
||||||
|
getCommandNameFromExpansion :: Token -> Maybe String
|
||||||
|
getCommandNameFromExpansion t =
|
||||||
|
case t of
|
||||||
|
T_DollarExpansion _ [c] -> extract c
|
||||||
|
T_Backticked _ [c] -> extract c
|
||||||
|
T_DollarBraceCommandExpansion _ [c] -> extract c
|
||||||
|
otherwise -> Nothing
|
||||||
|
where
|
||||||
|
extract (T_Pipeline _ _ [cmd]) = getCommandName cmd
|
||||||
|
extract _ = Nothing
|
||||||
|
|
||||||
|
-- Get the basename of a token representing a command
|
||||||
|
getCommandBasename = liftM basename . getCommandName
|
||||||
|
where
|
||||||
|
basename = reverse . takeWhile (/= '/') . reverse
|
||||||
|
|
||||||
|
isAssignment t =
|
||||||
|
case t of
|
||||||
|
T_Redirecting _ _ w -> isAssignment w
|
||||||
|
T_SimpleCommand _ (w:_) [] -> True
|
||||||
|
T_Assignment {} -> True
|
||||||
|
T_Annotation _ _ w -> isAssignment w
|
||||||
|
otherwise -> False
|
||||||
|
|
||||||
|
isOnlyRedirection t =
|
||||||
|
case t of
|
||||||
|
T_Pipeline _ _ [x] -> isOnlyRedirection x
|
||||||
|
T_Annotation _ _ w -> isOnlyRedirection w
|
||||||
|
T_Redirecting _ (_:_) c -> isOnlyRedirection c
|
||||||
|
T_SimpleCommand _ [] [] -> True
|
||||||
|
otherwise -> 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 t =
|
||||||
|
case t of
|
||||||
|
T_Script _ _ cmds -> [cmds]
|
||||||
|
T_BraceGroup _ cmds -> [cmds]
|
||||||
|
T_Subshell _ cmds -> [cmds]
|
||||||
|
T_WhileExpression _ _ cmds -> [cmds]
|
||||||
|
T_UntilExpression _ _ cmds -> [cmds]
|
||||||
|
T_ForIn _ _ _ cmds -> [cmds]
|
||||||
|
T_ForArithmetic _ _ _ _ cmds -> [cmds]
|
||||||
|
T_IfExpression _ thens elses -> map snd thens ++ [elses]
|
||||||
|
otherwise -> []
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
name <- getCommandName t
|
||||||
|
guard $ name == "declare" || name == "typeset"
|
||||||
|
let flags = getAllFlags t
|
||||||
|
guard $ elem "A" $ map snd flags
|
||||||
|
let args = map fst . filter ((==) "" . snd) $ flags
|
||||||
|
let names = mapMaybe (getLiteralStringExt nameAssignments) args
|
||||||
|
return $ tell names
|
||||||
|
f _ = return ()
|
||||||
|
|
||||||
|
nameAssignments t =
|
||||||
|
case t of
|
||||||
|
T_Assignment _ _ name _ _ -> return name
|
||||||
|
otherwise -> 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]
|
||||||
|
|
||||||
|
-- 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 []
|
||||||
|
|
||||||
|
wordsCanBeEqual x y = fromMaybe True $
|
||||||
|
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
|
File diff suppressed because it is too large
Load Diff
45
ShellCheck/Analyzer.hs
Normal file
45
ShellCheck/Analyzer.hs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{-
|
||||||
|
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.Analyzer (analyzeScript) where
|
||||||
|
|
||||||
|
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 {
|
||||||
|
arComments =
|
||||||
|
filterByAnnotation (asScript spec) . nub $
|
||||||
|
runAnalytics spec
|
||||||
|
++ runChecker params (checkers params)
|
||||||
|
}
|
||||||
|
where
|
||||||
|
params = makeParameters spec
|
||||||
|
|
||||||
|
checkers params = mconcat $ map ($ params) [
|
||||||
|
ShellCheck.Checks.Commands.checker,
|
||||||
|
ShellCheck.Checks.ShellSupport.checker
|
||||||
|
]
|
727
ShellCheck/AnalyzerLib.hs
Normal file
727
ShellCheck/AnalyzerLib.hs
Normal file
@@ -0,0 +1,727 @@
|
|||||||
|
{-
|
||||||
|
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.RWS
|
||||||
|
import Control.Monad.State
|
||||||
|
import Control.Monad.Writer
|
||||||
|
import Data.Char
|
||||||
|
import Data.List
|
||||||
|
import Data.Maybe
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
|
import Test.QuickCheck.All (forAllProperties)
|
||||||
|
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||||
|
|
||||||
|
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 Monoid Checker where
|
||||||
|
mempty = Checker {
|
||||||
|
perScript = nullCheck,
|
||||||
|
perToken = nullCheck
|
||||||
|
}
|
||||||
|
mappend x y = Checker {
|
||||||
|
perScript = perScript x `composeAnalyzers` perScript y,
|
||||||
|
perToken = perToken x `composeAnalyzers` perToken y
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis
|
||||||
|
composeAnalyzers f g x = f x >> g x
|
||||||
|
|
||||||
|
data Parameters = Parameters {
|
||||||
|
variableFlow :: [StackData],
|
||||||
|
parentMap :: Map.Map Id Token,
|
||||||
|
shellType :: Shell,
|
||||||
|
shellTypeSpecified :: Bool,
|
||||||
|
rootNode :: Token
|
||||||
|
}
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
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
|
||||||
|
|
||||||
|
-- For testing. If parsed, returns whether there are any comments
|
||||||
|
producesComments :: Checker -> String -> Maybe Bool
|
||||||
|
producesComments c s = do
|
||||||
|
root <- pScript s
|
||||||
|
let spec = defaultSpec root
|
||||||
|
let params = makeParameters spec
|
||||||
|
return . not . null $ runChecker params c
|
||||||
|
|
||||||
|
makeComment :: Severity -> Id -> Code -> String -> TokenComment
|
||||||
|
makeComment severity id code note =
|
||||||
|
TokenComment id $ Comment severity code note
|
||||||
|
|
||||||
|
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 {
|
||||||
|
rootNode = root,
|
||||||
|
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
|
||||||
|
prop_determineShell7 = determineShell (fromJust $ pScript "#! /bin/ash") == Dash
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
--- 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_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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
getClosestCommandM t = do
|
||||||
|
tree <- asks parentMap
|
||||||
|
return $ getClosestCommand tree t
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
-- 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 [] _ -> [(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_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
|
||||||
|
|
||||||
|
-- Run a command if the shell is in the given list
|
||||||
|
whenShell l c = do
|
||||||
|
shell <- asks shellType
|
||||||
|
when (shell `elem` l ) c
|
||||||
|
|
||||||
|
|
||||||
|
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 }) ) |])
|
182
ShellCheck/Checker.hs
Normal file
182
ShellCheck/Checker.hs
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
{-
|
||||||
|
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\""
|
||||||
|
|
||||||
|
prop_filewideAnnotationBase = [2086] == check "#!/bin/sh\necho $1"
|
||||||
|
prop_filewideAnnotation1 = null $
|
||||||
|
check "#!/bin/sh\n# shellcheck disable=2086\necho $1"
|
||||||
|
prop_filewideAnnotation2 = null $
|
||||||
|
check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
|
||||||
|
prop_filewideAnnotation3 = null $
|
||||||
|
check "#!/bin/sh\n#unerlated\n# shellcheck disable=2086\ntrue\necho $1"
|
||||||
|
prop_filewideAnnotation4 = null $
|
||||||
|
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
|
||||||
|
prop_filewideAnnotation5 = null $
|
||||||
|
check "#!/bin/sh\n\n\n\n#shellcheck disable=2086\ntrue\necho $1"
|
||||||
|
prop_filewideAnnotation6 = null $
|
||||||
|
check "#shellcheck shell=sh\n#unrelated\n#shellcheck disable=2086\ntrue\necho $1"
|
||||||
|
prop_filewideAnnotation7 = null $
|
||||||
|
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
|
||||||
|
|
||||||
|
prop_filewideAnnotationBase2 = [2086, 2181] == check "true\n[ $? == 0 ] && echo $1"
|
||||||
|
prop_filewideAnnotation8 = null $
|
||||||
|
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
|
||||||
|
|
||||||
|
return []
|
||||||
|
runTests = $quickCheckAll
|
685
ShellCheck/Checks/Commands.hs
Normal file
685
ShellCheck/Checks/Commands.hs
Normal file
@@ -0,0 +1,685 @@
|
|||||||
|
{-
|
||||||
|
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 (checker
|
||||||
|
, 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.RWS
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
verify :: CommandCheck -> String -> Bool
|
||||||
|
verify f s = producesComments (getChecker [f]) s == Just True
|
||||||
|
verifyNot f s = producesComments (getChecker [f]) s == Just False
|
||||||
|
|
||||||
|
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
|
||||||
|
,checkTimeParameters
|
||||||
|
,checkTimedCommand
|
||||||
|
,checkLocalScope
|
||||||
|
,checkDeprecatedTempfile
|
||||||
|
,checkDeprecatedEgrep
|
||||||
|
,checkDeprecatedFgrep
|
||||||
|
]
|
||||||
|
|
||||||
|
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
||||||
|
buildCommandMap = foldl' addCheck Map.empty
|
||||||
|
where
|
||||||
|
addCheck map (CommandCheck name function) =
|
||||||
|
Map.insertWith' composeAnalyzers 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 ()
|
||||||
|
|
||||||
|
getChecker :: [CommandCheck] -> Checker
|
||||||
|
getChecker list = Checker {
|
||||||
|
perScript = const $ return (),
|
||||||
|
perToken = checkCommand map
|
||||||
|
}
|
||||||
|
where
|
||||||
|
map = buildCommandMap list
|
||||||
|
|
||||||
|
|
||||||
|
checker :: Parameters -> Checker
|
||||||
|
checker params = getChecker commandChecks
|
||||||
|
|
||||||
|
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
|
||||||
|
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
|
||||||
|
prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'"
|
||||||
|
prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'"
|
||||||
|
prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'"
|
||||||
|
prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'"
|
||||||
|
prop_checkTr4 = verifyNot checkTr "ls [a-z]"
|
||||||
|
prop_checkTr5 = verify checkTr "tr foo bar"
|
||||||
|
prop_checkTr6 = verify checkTr "tr 'hello' 'world'"
|
||||||
|
prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
|
||||||
|
prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
|
||||||
|
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
|
||||||
|
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
|
||||||
|
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"
|
||||||
|
prop_checkGrepRe12= verifyNot checkGrepRe "grep -F 'Foo*' file"
|
||||||
|
|
||||||
|
checkGrepRe = CommandCheck (Basename "grep") check where
|
||||||
|
check cmd = f cmd (arguments cmd)
|
||||||
|
-- --regex=*(extglob) doesn't work. Fixme?
|
||||||
|
skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s
|
||||||
|
skippable _ = False
|
||||||
|
f _ [] = return ()
|
||||||
|
f cmd (x:r) | skippable (getLiteralStringExt (const $ return "_") x) = f cmd r
|
||||||
|
f cmd (re:_) = do
|
||||||
|
when (isGlob re) $
|
||||||
|
warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it."
|
||||||
|
|
||||||
|
unless (cmd `hasFlag` "F") $ do
|
||||||
|
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", "dash", "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", "-fls", "-fprint", "-fprint0", "-fprintf", "-ls", "-ok", "-okdir", "-printf" ]
|
||||||
|
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"
|
||||||
|
prop_checkPrintfVar11= verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
|
||||||
|
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 (dropWhile (/= ')') 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
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10"
|
||||||
|
prop_checkTimeParameters2 = verifyNot checkTimeParameters "time sleep 10"
|
||||||
|
prop_checkTimeParameters3 = verifyNot checkTimeParameters "time -p foo"
|
||||||
|
prop_checkTimeParameters4 = verifyNot checkTimeParameters "command time -f lol sleep 10"
|
||||||
|
checkTimeParameters = CommandCheck (Exactly "time") f
|
||||||
|
where
|
||||||
|
f (T_SimpleCommand _ _ (cmd:args:_)) =
|
||||||
|
whenShell [Bash, Sh] $
|
||||||
|
let s = concat $ oversimplify args in
|
||||||
|
when ("-" `isPrefixOf` s && s /= "-p") $
|
||||||
|
info (getId cmd) 2023 "The shell may override 'time' as seen in man time(1). Use 'command time ..' for that one."
|
||||||
|
|
||||||
|
f _ = return ()
|
||||||
|
|
||||||
|
prop_checkTimedCommand1 = verify checkTimedCommand "#!/bin/sh\ntime -p foo | bar"
|
||||||
|
prop_checkTimedCommand2 = verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar; )"
|
||||||
|
prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1"
|
||||||
|
checkTimedCommand = CommandCheck (Exactly "time") f where
|
||||||
|
f (T_SimpleCommand _ _ (c:args@(_:_))) =
|
||||||
|
whenShell [Sh, Dash] $ do
|
||||||
|
let cmd = last args -- "time" is parsed with a command as argument
|
||||||
|
when (isPiped cmd) $
|
||||||
|
warn (getId c) 2176 "'time' is undefined for pipelines. time single stage or bash -c instead."
|
||||||
|
when (isSimple cmd == Just False) $
|
||||||
|
warn (getId cmd) 2177 "'time' is undefined for compound commands, time sh -c instead."
|
||||||
|
f _ = return ()
|
||||||
|
isPiped cmd =
|
||||||
|
case cmd of
|
||||||
|
T_Pipeline _ _ (_:_:_) -> True
|
||||||
|
_ -> False
|
||||||
|
getCommand cmd =
|
||||||
|
case cmd of
|
||||||
|
T_Pipeline _ _ (T_Redirecting _ _ a : _) -> return a
|
||||||
|
_ -> fail ""
|
||||||
|
isSimple cmd = do
|
||||||
|
innerCommand <- getCommand cmd
|
||||||
|
case innerCommand of
|
||||||
|
T_SimpleCommand {} -> return True
|
||||||
|
_ -> return False
|
||||||
|
|
||||||
|
prop_checkLocalScope1 = verify checkLocalScope "local foo=3"
|
||||||
|
prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }"
|
||||||
|
checkLocalScope = CommandCheck (Exactly "local") $ \t ->
|
||||||
|
whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local
|
||||||
|
path <- getPathM t
|
||||||
|
unless (any isFunction path) $
|
||||||
|
err (getId t) 2168 "'local' is only valid in functions."
|
||||||
|
|
||||||
|
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
|
||||||
|
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
|
||||||
|
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
|
||||||
|
\t -> warn (getId t) 2186 "tempfile is deprecated. Use mktemp instead."
|
||||||
|
|
||||||
|
prop_checkDeprecatedEgrep = verify checkDeprecatedEgrep "egrep '.+'"
|
||||||
|
checkDeprecatedEgrep = CommandCheck (Basename "egrep") $
|
||||||
|
\t -> info (getId t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."
|
||||||
|
|
||||||
|
prop_checkDeprecatedFgrep = verify checkDeprecatedFgrep "fgrep '*' files"
|
||||||
|
checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $
|
||||||
|
\t -> info (getId t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
|
||||||
|
|
||||||
|
return []
|
||||||
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
388
ShellCheck/Checks/ShellSupport.hs
Normal file
388
ShellCheck/Checks/ShellSupport.hs
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
{-
|
||||||
|
Copyright 2012-2016 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.Checks.ShellSupport (checker
|
||||||
|
, ShellCheck.Checks.ShellSupport.runTests
|
||||||
|
) 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
|
||||||
|
import Test.QuickCheck.All (forAllProperties)
|
||||||
|
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||||
|
|
||||||
|
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
|
||||||
|
]
|
||||||
|
|
||||||
|
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_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
|
||||||
|
prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
|
||||||
|
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
|
||||||
|
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_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
|
||||||
|
prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"
|
||||||
|
prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
|
||||||
|
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
|
||||||
|
prop_checkBashisms5 = verify checkBashisms "source file"
|
||||||
|
prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
|
||||||
|
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
|
||||||
|
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
|
||||||
|
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
|
||||||
|
prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}"
|
||||||
|
prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}"
|
||||||
|
prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}"
|
||||||
|
prop_checkBashisms13= verify checkBashisms "exec -c env"
|
||||||
|
prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \""
|
||||||
|
prop_checkBashisms15= verify checkBashisms "let n++"
|
||||||
|
prop_checkBashisms16= verify checkBashisms "echo $RANDOM"
|
||||||
|
prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))"
|
||||||
|
prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"
|
||||||
|
prop_checkBashisms19= verify checkBashisms "foo > file*.txt"
|
||||||
|
prop_checkBashisms20= verify checkBashisms "read -ra foo"
|
||||||
|
prop_checkBashisms21= verify checkBashisms "[ -a foo ]"
|
||||||
|
prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]"
|
||||||
|
prop_checkBashisms23= verify checkBashisms "trap mything ERR INT"
|
||||||
|
prop_checkBashisms24= verifyNot checkBashisms "trap mything INT TERM"
|
||||||
|
prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123"
|
||||||
|
prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM"
|
||||||
|
prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*"
|
||||||
|
prop_checkBashisms28= verify checkBashisms "exec {n}>&2"
|
||||||
|
prop_checkBashisms29= verify checkBashisms "echo ${!var}"
|
||||||
|
prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\""
|
||||||
|
prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\""
|
||||||
|
prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
|
||||||
|
prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo"
|
||||||
|
prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo"
|
||||||
|
prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo"
|
||||||
|
prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
|
||||||
|
prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
|
||||||
|
prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM"
|
||||||
|
prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }"
|
||||||
|
prop_checkBashisms40= verify checkBashisms "echo $(<file)"
|
||||||
|
prop_checkBashisms41= verify checkBashisms "echo `<file`"
|
||||||
|
prop_checkBashisms42= verify checkBashisms "trap foo int"
|
||||||
|
prop_checkBashisms43= verify checkBashisms "trap foo sigint"
|
||||||
|
prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
|
||||||
|
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
|
||||||
|
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
|
||||||
|
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
|
||||||
|
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/dash\necho $LINENO"
|
||||||
|
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
|
||||||
|
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
|
||||||
|
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
||||||
|
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
||||||
|
prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
||||||
|
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"
|
||||||
|
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` [ "-nt", "-ef", "\\<", "\\>"] =
|
||||||
|
unless isDash $ warnMsg id $ op ++ " is"
|
||||||
|
bashism (TC_Binary id SingleBracket "==" _ _) =
|
||||||
|
warnMsg id "== in place of = 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_Expansion id _) | isBashism =
|
||||||
|
warnMsg id $ fromJust str ++ " is"
|
||||||
|
where
|
||||||
|
str = getLiteralString t
|
||||||
|
isBashism = isJust str && isBashVariable (fromJust str)
|
||||||
|
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", "type"] else []
|
||||||
|
allowedFlags = Map.fromList [
|
||||||
|
("read", if isDash then ["r", "p"] else ["r"]),
|
||||||
|
("ulimit", ["f"]),
|
||||||
|
("printf", []),
|
||||||
|
("exec", [])
|
||||||
|
]
|
||||||
|
|
||||||
|
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 $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is")
|
||||||
|
]
|
||||||
|
bashVars = [
|
||||||
|
"LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
||||||
|
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
|
||||||
|
]
|
||||||
|
bashDynamicVars = [ "RANDOM", "SECONDS" ]
|
||||||
|
dashVars = [ "LINENO" ]
|
||||||
|
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_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
|
||||||
|
prop_checkEchoSed2 = 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_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}"
|
||||||
|
prop_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}"
|
||||||
|
prop_checkBraceExpansionVars3 = verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg"
|
||||||
|
prop_checkBraceExpansionVars4 = verify checkBraceExpansionVars "echo {$i..100}"
|
||||||
|
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_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3"
|
||||||
|
prop_checkMultiDimensionalArrays2 = verifyNot checkMultiDimensionalArrays "foo[a]=3"
|
||||||
|
prop_checkMultiDimensionalArrays3 = verify checkMultiDimensionalArrays "foo=( [a][b]=c )"
|
||||||
|
prop_checkMultiDimensionalArrays4 = verifyNot checkMultiDimensionalArrays "foo=( [a]=c )"
|
||||||
|
prop_checkMultiDimensionalArrays5 = verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}"
|
||||||
|
prop_checkMultiDimensionalArrays6 = verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}"
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
return []
|
||||||
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -1,8 +1,14 @@
|
|||||||
module ShellCheck.Data where
|
module ShellCheck.Data where
|
||||||
|
|
||||||
|
import ShellCheck.Interface
|
||||||
|
import Data.Version (showVersion)
|
||||||
|
import Paths_ShellCheck (version)
|
||||||
|
|
||||||
|
shellcheckVersion = showVersion version
|
||||||
|
|
||||||
internalVariables = [
|
internalVariables = [
|
||||||
-- Generic
|
-- Generic
|
||||||
"", "_",
|
"", "_", "rest", "REST",
|
||||||
|
|
||||||
-- Bash
|
-- Bash
|
||||||
"BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
|
"BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
|
||||||
@@ -19,25 +25,30 @@ internalVariables = [
|
|||||||
"FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",
|
"FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",
|
||||||
"HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",
|
"HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",
|
||||||
"IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
|
"IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
|
||||||
"LC_MESSAGES", "LC_NUMERIC", "LINES", "MAIL", "MAILCHECK", "MAILPATH",
|
"LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LINES", "MAIL",
|
||||||
"OPTERR", "PATH", "POSIXLY_CORRECT", "PROMPT_COMMAND",
|
"MAILCHECK", "MAILPATH", "OPTERR", "PATH", "POSIXLY_CORRECT",
|
||||||
"PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT",
|
"PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL",
|
||||||
"TMOUT", "TMPDIR", "auto_resume", "histchars",
|
"TIMEFORMAT", "TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC",
|
||||||
|
|
||||||
-- Zsh
|
-- Other
|
||||||
"ARGV0", "BAUD", "cdpath", "COLUMNS", "CORRECT_IGNORE",
|
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
|
||||||
"DIRSTACKSIZE", "ENV", "FCEDIT", "fignore", "fpath", "histchars",
|
"HOSTNAME", "KRB5CCNAME", "XAUTHORITY"
|
||||||
"HISTCHARS", "HISTFILE", "HISTSIZE", "HOME", "IFS", "KEYBOARD_HACK",
|
|
||||||
"KEYTIMEOUT", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
|
-- Ksh
|
||||||
"LC_MESSAGES", "LC_NUMERIC", "LC_TIME", "LINES", "LISTMAX",
|
, ".sh.version"
|
||||||
"LOGCHECK", "MAIL", "MAILCHECK", "mailpath", "manpath", "module_path",
|
]
|
||||||
"NULLCMD", "path", "POSTEDIT", "PROMPT", "PROMPT2", "PROMPT3",
|
|
||||||
"PROMPT4", "prompt", "PROMPT_EOL_MARK", "PS1", "PS2", "PS3", "PS4",
|
variablesWithoutSpaces = [
|
||||||
"psvar", "READNULLCMD", "REPORTTIME", "REPLY", "reply", "RPROMPT",
|
"$", "-", "?", "!",
|
||||||
"RPS1", "RPROMPT2", "RPS2", "SAVEHIST", "SPROMPT", "STTY", "TERM",
|
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
|
||||||
"TERMINFO", "TIMEFMT", "TMOUT", "TMPPREFIX", "watch", "WATCHFMT",
|
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
|
||||||
"WORDCHARS", "ZBEEP", "ZDOTDIR", "ZLE_LINE_ABORTED",
|
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
|
||||||
"ZLE_REMOVE_SUFFIX_CHARS", "ZLE_SPACE_SUFFIX_CHARS"
|
]
|
||||||
|
|
||||||
|
arrayVariables = [
|
||||||
|
"BASH_ALIASES", "BASH_ARGC", "BASH_ARGV", "BASH_CMDS", "BASH_LINENO",
|
||||||
|
"BASH_REMATCH", "BASH_SOURCE", "BASH_VERSINFO", "COMP_WORDS", "COPROC",
|
||||||
|
"DIRSTACK", "FUNCNAME", "GROUPS", "MAPFILE", "PIPESTATUS", "COMPREPLY"
|
||||||
]
|
]
|
||||||
|
|
||||||
commonCommands = [
|
commonCommands = [
|
||||||
@@ -65,3 +76,34 @@ commonCommands = [
|
|||||||
"val", "vi", "wait", "wc", "what", "who", "write", "xargs", "yacc",
|
"val", "vi", "wait", "wc", "what", "who", "write", "xargs", "yacc",
|
||||||
"zcat"
|
"zcat"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
sampleWords = [
|
||||||
|
"alpha", "bravo", "charlie", "delta", "echo", "foxtrot",
|
||||||
|
"golf", "hotel", "india", "juliett", "kilo", "lima", "mike",
|
||||||
|
"november", "oscar", "papa", "quebec", "romeo", "sierra",
|
||||||
|
"tango", "uniform", "victor", "whiskey", "xray", "yankee",
|
||||||
|
"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
|
||||||
|
otherwise -> Nothing
|
||||||
|
82
ShellCheck/Formatter/CheckStyle.hs
Normal file
82
ShellCheck/Formatter/CheckStyle.hs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
{-
|
||||||
|
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.CheckStyle (format) where
|
||||||
|
|
||||||
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Formatter.Format
|
||||||
|
|
||||||
|
import Data.Char
|
||||||
|
import Data.List
|
||||||
|
import GHC.Exts
|
||||||
|
import System.IO
|
||||||
|
|
||||||
|
format :: IO Formatter
|
||||||
|
format = return Formatter {
|
||||||
|
header = do
|
||||||
|
putStrLn "<?xml version='1.0' encoding='UTF-8'?>"
|
||||||
|
putStrLn "<checkstyle version='4.3'>",
|
||||||
|
|
||||||
|
onFailure = outputError,
|
||||||
|
onResult = outputResult,
|
||||||
|
|
||||||
|
footer = putStrLn "</checkstyle>"
|
||||||
|
}
|
||||||
|
|
||||||
|
outputResult result contents = do
|
||||||
|
let comments = makeNonVirtual (crComments result) contents
|
||||||
|
putStrLn . formatFile (crFilename result) $ comments
|
||||||
|
|
||||||
|
formatFile name comments = concat [
|
||||||
|
"<file ", attr "name" name, ">\n",
|
||||||
|
concatMap formatComment comments,
|
||||||
|
"</file>"
|
||||||
|
]
|
||||||
|
|
||||||
|
formatComment c = concat [
|
||||||
|
"<error ",
|
||||||
|
attr "line" $ show . lineNo $ c,
|
||||||
|
attr "column" $ show . colNo $ c,
|
||||||
|
attr "severity" . severity $ severityText c,
|
||||||
|
attr "message" $ messageText c,
|
||||||
|
attr "source" $ "ShellCheck.SC" ++ show (codeNo c),
|
||||||
|
"/>\n"
|
||||||
|
]
|
||||||
|
|
||||||
|
outputError file error = putStrLn $ concat [
|
||||||
|
"<file ", attr "name" file, ">\n",
|
||||||
|
"<error ",
|
||||||
|
attr "line" "1",
|
||||||
|
attr "column" "1",
|
||||||
|
attr "severity" "error",
|
||||||
|
attr "message" error,
|
||||||
|
attr "source" "ShellCheck",
|
||||||
|
"/>\n",
|
||||||
|
"</file>"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
attr s v = concat [ s, "='", escape v, "' " ]
|
||||||
|
escape = concatMap escape'
|
||||||
|
escape' c = if isOk c then [c] else "&#" ++ show (ord c) ++ ";"
|
||||||
|
isOk x = any ($x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")]
|
||||||
|
|
||||||
|
severity "error" = "error"
|
||||||
|
severity "warning" = "warning"
|
||||||
|
severity _ = "info"
|
66
ShellCheck/Formatter/Format.hs
Normal file
66
ShellCheck/Formatter/Format.hs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{-
|
||||||
|
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.Format where
|
||||||
|
|
||||||
|
import ShellCheck.Data
|
||||||
|
import ShellCheck.Interface
|
||||||
|
|
||||||
|
-- A formatter that carries along an arbitrary piece of data
|
||||||
|
data Formatter = Formatter {
|
||||||
|
header :: IO (),
|
||||||
|
onResult :: CheckResult -> String -> 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
|
||||||
|
|
||||||
|
severityText :: PositionedComment -> String
|
||||||
|
severityText (PositionedComment _ _ (Comment c _ _)) =
|
||||||
|
case c of
|
||||||
|
ErrorC -> "error"
|
||||||
|
WarningC -> "warning"
|
||||||
|
InfoC -> "info"
|
||||||
|
StyleC -> "style"
|
||||||
|
|
||||||
|
-- Realign comments from a tabstop of 8 to 1
|
||||||
|
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
|
||||||
|
realignColumn lineNo colNo c =
|
||||||
|
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
|
||||||
|
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
|
||||||
|
else colNo c
|
||||||
|
real _ r v target | target <= v = r
|
||||||
|
real [] r v _ = r -- should never happen
|
||||||
|
real ('\t':rest) r v target =
|
||||||
|
real rest (r+1) (v + 8 - (v `mod` 8)) target
|
||||||
|
real (_:rest) r v target = real rest (r+1) (v+1) target
|
54
ShellCheck/Formatter/GCC.hs
Normal file
54
ShellCheck/Formatter/GCC.hs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{-
|
||||||
|
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.GCC (format) where
|
||||||
|
|
||||||
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Formatter.Format
|
||||||
|
|
||||||
|
import Data.List
|
||||||
|
import GHC.Exts
|
||||||
|
import System.IO
|
||||||
|
|
||||||
|
format :: IO Formatter
|
||||||
|
format = return Formatter {
|
||||||
|
header = return (),
|
||||||
|
footer = return (),
|
||||||
|
onFailure = outputError,
|
||||||
|
onResult = outputResult
|
||||||
|
}
|
||||||
|
|
||||||
|
outputError file error = hPutStrLn stderr $ file ++ ": " ++ error
|
||||||
|
|
||||||
|
outputResult result contents = do
|
||||||
|
let comments = makeNonVirtual (crComments result) contents
|
||||||
|
mapM_ (putStrLn . formatComment (crFilename result)) comments
|
||||||
|
|
||||||
|
formatComment filename c = concat [
|
||||||
|
filename, ":",
|
||||||
|
show $ lineNo c, ":",
|
||||||
|
show $ colNo c, ": ",
|
||||||
|
case severityText c of
|
||||||
|
"error" -> "error"
|
||||||
|
"warning" -> "warning"
|
||||||
|
_ -> "note",
|
||||||
|
": ",
|
||||||
|
concat . lines $ messageText c,
|
||||||
|
" [SC", show $ codeNo c, "]"
|
||||||
|
]
|
60
ShellCheck/Formatter/JSON.hs
Normal file
60
ShellCheck/Formatter/JSON.hs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
{-
|
||||||
|
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
|
||||||
|
|
91
ShellCheck/Formatter/TTY.hs
Normal file
91
ShellCheck/Formatter/TTY.hs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
{-
|
||||||
|
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"
|
116
ShellCheck/Interface.hs
Normal file
116
ShellCheck/Interface.hs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
{-
|
||||||
|
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
|
||||||
|
|
2248
ShellCheck/Parser.hs
2248
ShellCheck/Parser.hs
File diff suppressed because it is too large
Load Diff
80
ShellCheck/Regex.hs
Normal file
80
ShellCheck/Regex.hs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
{-
|
||||||
|
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 FlexibleContexts #-}
|
||||||
|
|
||||||
|
-- Basically Text.Regex based on regex-tdfa instead of the buggy regex-posix.
|
||||||
|
module ShellCheck.Regex where
|
||||||
|
|
||||||
|
import Data.List
|
||||||
|
import Data.Maybe
|
||||||
|
import Control.Monad
|
||||||
|
import Text.Regex.TDFA
|
||||||
|
|
||||||
|
-- Precompile the regex
|
||||||
|
mkRegex :: String -> Regex
|
||||||
|
mkRegex str =
|
||||||
|
let make :: RegexMaker Regex CompOption ExecOption String => String -> Regex
|
||||||
|
make = makeRegex
|
||||||
|
in
|
||||||
|
make str
|
||||||
|
|
||||||
|
-- Does the regex match?
|
||||||
|
matches :: String -> Regex -> Bool
|
||||||
|
matches = flip match
|
||||||
|
|
||||||
|
-- Get all subgroups of the first match
|
||||||
|
matchRegex :: Regex -> String -> Maybe [String]
|
||||||
|
matchRegex re str = do
|
||||||
|
(_, _, _, groups) <- matchM re str :: Maybe (String,String,String,[String])
|
||||||
|
return groups
|
||||||
|
|
||||||
|
-- Get all full matches
|
||||||
|
matchAllStrings :: Regex -> String -> [String]
|
||||||
|
matchAllStrings re = unfoldr f
|
||||||
|
where
|
||||||
|
f :: String -> Maybe (String, String)
|
||||||
|
f str = do
|
||||||
|
(_, match, rest, _) <- matchM re str :: Maybe (String, String, String, [String])
|
||||||
|
return (match, rest)
|
||||||
|
|
||||||
|
-- Get all subgroups from all matches
|
||||||
|
matchAllSubgroups :: Regex -> String -> [[String]]
|
||||||
|
matchAllSubgroups re = unfoldr f
|
||||||
|
where
|
||||||
|
f :: String -> Maybe ([String], String)
|
||||||
|
f str = do
|
||||||
|
(_, _, rest, groups) <- matchM re str :: Maybe (String, String, String, [String])
|
||||||
|
return (groups, rest)
|
||||||
|
|
||||||
|
-- Replace regex in input with string
|
||||||
|
subRegex :: Regex -> String -> String -> String
|
||||||
|
subRegex re input replacement = f input
|
||||||
|
where
|
||||||
|
f str = fromMaybe str $ do
|
||||||
|
(before, match, after) <- matchM re str :: Maybe (String, String, String)
|
||||||
|
when (null match) $ error ("Internal error: substituted empty in " ++ str)
|
||||||
|
return $ before ++ replacement ++ f after
|
||||||
|
|
||||||
|
-- Split a string based on a regex.
|
||||||
|
splitOn :: String -> Regex -> [String]
|
||||||
|
splitOn input re =
|
||||||
|
case matchM re input :: Maybe (String, String, String) of
|
||||||
|
Just (before, match, after) -> before : after `splitOn` re
|
||||||
|
Nothing -> [input]
|
@@ -1,49 +0,0 @@
|
|||||||
{-
|
|
||||||
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 Affero 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 Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
-}
|
|
||||||
module ShellCheck.Simple (shellCheck, ShellCheckComment, scLine, scColumn, scSeverity, scMessage) where
|
|
||||||
|
|
||||||
import ShellCheck.Parser
|
|
||||||
import ShellCheck.Analytics
|
|
||||||
import Data.Maybe
|
|
||||||
import Text.Parsec.Pos
|
|
||||||
import Data.List
|
|
||||||
|
|
||||||
shellCheck :: String -> [ShellCheckComment]
|
|
||||||
shellCheck script =
|
|
||||||
let (ParseResult result notes) = parseShell "-" script in
|
|
||||||
let allNotes = notes ++ (concat $ maybeToList $ do
|
|
||||||
(tree, map) <- result
|
|
||||||
let newMap = runAllAnalytics tree map
|
|
||||||
return $ notesFromMap newMap
|
|
||||||
)
|
|
||||||
in
|
|
||||||
map formatNote $ nub $ sortNotes allNotes
|
|
||||||
|
|
||||||
data ShellCheckComment = ShellCheckComment { scLine :: Int, scColumn :: Int, scSeverity :: String, scMessage :: String }
|
|
||||||
|
|
||||||
instance Show ShellCheckComment where
|
|
||||||
show c = concat ["(", show $ scLine c, ",", show $ scColumn c, ") ", scSeverity c, ": ", scMessage c]
|
|
||||||
|
|
||||||
severityToString s =
|
|
||||||
case s of
|
|
||||||
ErrorC -> "error"
|
|
||||||
WarningC -> "warning"
|
|
||||||
InfoC -> "info"
|
|
||||||
StyleC -> "style"
|
|
||||||
|
|
||||||
formatNote (ParseNote pos severity text) = ShellCheckComment (sourceLine pos) (sourceColumn pos) (severityToString severity) text
|
|
BIN
doc/emacs-flycheck.png
Normal file
BIN
doc/emacs-flycheck.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
doc/terminal.png
Normal file
BIN
doc/terminal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
BIN
doc/vim-syntastic.png
Normal file
BIN
doc/vim-syntastic.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
32
jsoncheck.hs
32
jsoncheck.hs
@@ -1,32 +0,0 @@
|
|||||||
{-
|
|
||||||
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 Affero 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 Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
-}
|
|
||||||
import ShellCheck.Simple
|
|
||||||
import Text.JSON
|
|
||||||
|
|
||||||
instance JSON ShellCheckComment where
|
|
||||||
showJSON c = makeObj [
|
|
||||||
("line", showJSON $ scLine c),
|
|
||||||
("column", showJSON $ scColumn c),
|
|
||||||
("level", showJSON $ scSeverity c),
|
|
||||||
("message", showJSON $ scMessage c)
|
|
||||||
]
|
|
||||||
readJSON = undefined
|
|
||||||
|
|
||||||
main = do
|
|
||||||
script <- getContents
|
|
||||||
putStrLn $ encodeStrict $ shellCheck script
|
|
10
nextnumber
Executable file
10
nextnumber
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# TODO: Find a less trashy way to get the next available error code
|
||||||
|
|
||||||
|
shopt -s globstar
|
||||||
|
|
||||||
|
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
|
5
quickrun
Executable file
5
quickrun
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# quickrun runs ShellCheck in an interpreted mode.
|
||||||
|
# This allows testing changes without recompiling.
|
||||||
|
|
||||||
|
runghc -idist/build/autogen shellcheck.hs "$@"
|
22
quicktest
Executable file
22
quicktest
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# quicktest runs the ShellCheck unit tests in an interpreted mode.
|
||||||
|
# This allows running tests without compiling, which can be faster.
|
||||||
|
# 'cabal test' remains the source of truth.
|
||||||
|
|
||||||
|
(
|
||||||
|
var=$(echo 'liftM and $ sequence [
|
||||||
|
ShellCheck.Analytics.runTests
|
||||||
|
,ShellCheck.Parser.runTests
|
||||||
|
,ShellCheck.Checker.runTests
|
||||||
|
,ShellCheck.Checks.Commands.runTests
|
||||||
|
,ShellCheck.Checks.ShellSupport.runTests
|
||||||
|
,ShellCheck.AnalyzerLib.runTests
|
||||||
|
]' | tr -d '\n' | cabal repl 2>&1 | tee /dev/stderr)
|
||||||
|
if [[ $var == *$'\nTrue'* ]]
|
||||||
|
then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
grep -C 3 -e "Fail" -e "Tracing" <<< "$var"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
) 2>&1
|
203
shellcheck.1.md
Normal file
203
shellcheck.1.md
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
% SHELLCHECK(1) Shell script analysis tool
|
||||||
|
|
||||||
|
# NAME
|
||||||
|
|
||||||
|
shellcheck - Shell script analysis tool
|
||||||
|
|
||||||
|
# SYNOPSIS
|
||||||
|
|
||||||
|
**shellcheck** [*OPTIONS*...] *FILES*...
|
||||||
|
|
||||||
|
# DESCRIPTION
|
||||||
|
|
||||||
|
ShellCheck is a static analysis and linting tool for sh/bash scripts. It's
|
||||||
|
mainly focused on handling typical beginner and intermediate level syntax
|
||||||
|
errors and pitfalls where the shell just gives a cryptic error message or
|
||||||
|
strange behavior, but it also reports on a few more advanced issues where
|
||||||
|
corner cases can cause delayed failures.
|
||||||
|
|
||||||
|
ShellCheck gives shell specific advice. Consider this line:
|
||||||
|
|
||||||
|
(( area = 3.14*r*r ))
|
||||||
|
|
||||||
|
+ For scripts starting with `#!/bin/sh` (or when using `-s sh`), ShellCheck
|
||||||
|
will warn that `(( .. ))` is not POSIX compliant (similar to checkbashisms).
|
||||||
|
|
||||||
|
+ For scripts starting with `#!/bin/bash` (or using `-s bash`), ShellCheck
|
||||||
|
will warn that decimals are not supported.
|
||||||
|
|
||||||
|
+ For scripts starting with `#!/bin/ksh` (or using `-s ksh`), ShellCheck will
|
||||||
|
not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
||||||
|
|
||||||
|
|
||||||
|
# OPTIONS
|
||||||
|
|
||||||
|
**-C**[*WHEN*],\ **--color**[=*WHEN*]
|
||||||
|
|
||||||
|
: For TTY output, enable colors *always*, *never* or *auto*. The default
|
||||||
|
is *auto*. **--color** without an argument is equivalent to
|
||||||
|
**--color=always**.
|
||||||
|
|
||||||
|
**-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...]
|
||||||
|
|
||||||
|
: Explicitly exclude the specified codes from the report. Subsequent **-e**
|
||||||
|
options are cumulative, but all the codes can be specified at once,
|
||||||
|
comma-separated as a single argument.
|
||||||
|
|
||||||
|
**-f** *FORMAT*, **--format=***FORMAT*
|
||||||
|
|
||||||
|
: Specify the output format of shellcheck, which prints its results in the
|
||||||
|
standard output. Subsequent **-f** options are ignored, see **FORMATS**
|
||||||
|
below for more information.
|
||||||
|
|
||||||
|
**-s**\ *shell*,\ **--shell=***shell*
|
||||||
|
|
||||||
|
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
|
||||||
|
The default is to use the file's shebang, or *bash* if the target shell
|
||||||
|
can't be determined.
|
||||||
|
|
||||||
|
**-V**,\ **--version**
|
||||||
|
|
||||||
|
: Print version information and exit.
|
||||||
|
|
||||||
|
**-x**,\ **--external-sources**
|
||||||
|
|
||||||
|
: Follow 'source' statements even when the file is not specified as input.
|
||||||
|
By default, `shellcheck` will only follow files specified on the command
|
||||||
|
line (plus `/dev/null`). This option allows following any file the script
|
||||||
|
may `source`.
|
||||||
|
|
||||||
|
# FORMATS
|
||||||
|
|
||||||
|
**tty**
|
||||||
|
|
||||||
|
: Plain text, human readable output. This is the default.
|
||||||
|
|
||||||
|
**gcc**
|
||||||
|
|
||||||
|
: GCC compatible output. Useful for editors that support compiling and
|
||||||
|
showing syntax errors.
|
||||||
|
|
||||||
|
For example, in Vim, `:set makeprg=shellcheck\ -f\ gcc\ %` will allow
|
||||||
|
using `:make` to check the script, and `:cnext` to jump to the next error.
|
||||||
|
|
||||||
|
<file>:<line>:<column>: <type>: <message>
|
||||||
|
|
||||||
|
**checkstyle**
|
||||||
|
|
||||||
|
: Checkstyle compatible XML output. Supported directly or through plugins
|
||||||
|
by many IDEs and build monitoring systems.
|
||||||
|
|
||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<checkstyle version='4.3'>
|
||||||
|
<file name='file'>
|
||||||
|
<error
|
||||||
|
line='line'
|
||||||
|
column='column'
|
||||||
|
severity='severity'
|
||||||
|
message='message'
|
||||||
|
source='ShellCheck.SC####' />
|
||||||
|
...
|
||||||
|
</file>
|
||||||
|
...
|
||||||
|
</checkstyle>
|
||||||
|
|
||||||
|
**json**
|
||||||
|
|
||||||
|
: Json is a popular serialization format that is more suitable for web
|
||||||
|
applications. ShellCheck's json is compact and contains only the bare
|
||||||
|
minimum.
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"file": "filename",
|
||||||
|
"line": lineNumber,
|
||||||
|
"column": columnNumber,
|
||||||
|
"level": "severitylevel",
|
||||||
|
"code": errorCode,
|
||||||
|
"message": "warning message"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
# DIRECTIVES
|
||||||
|
ShellCheck directives can be specified as comments in the shell script
|
||||||
|
before a command or block:
|
||||||
|
|
||||||
|
# shellcheck key=value key=value
|
||||||
|
command-or-structure
|
||||||
|
|
||||||
|
For example, to suppress SC2035 about using `./*.jpg`:
|
||||||
|
|
||||||
|
# shellcheck disable=SC2035
|
||||||
|
echo "Files: " *.jpg
|
||||||
|
|
||||||
|
To tell ShellCheck where to look for an otherwise dynamically determined file:
|
||||||
|
|
||||||
|
# shellcheck source=./lib.sh
|
||||||
|
source "$(find_install_dir)/lib.sh"
|
||||||
|
|
||||||
|
Here a shell brace group is used to suppress a warning on multiple lines:
|
||||||
|
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
{
|
||||||
|
echo 'Modifying $PATH'
|
||||||
|
echo 'PATH=foo:$PATH' >> ~/.bashrc
|
||||||
|
}
|
||||||
|
|
||||||
|
Valid keys are:
|
||||||
|
|
||||||
|
**disable**
|
||||||
|
: Disables a comma separated list of error codes for the following command.
|
||||||
|
The command can be a simple command like `echo foo`, or a compound command
|
||||||
|
like a function definition, subshell block or loop.
|
||||||
|
|
||||||
|
**source**
|
||||||
|
: Overrides the filename included by a `source`/`.` statement. This can be
|
||||||
|
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`.
|
||||||
|
|
||||||
|
# ENVIRONMENT VARIABLES
|
||||||
|
The environment variable `SHELLCHECK_OPTS` can be set with default flags:
|
||||||
|
|
||||||
|
export SHELLCHECK_OPTS='--shell=bash --exclude=SC2016'
|
||||||
|
|
||||||
|
Its value will be split on spaces and prepended to the command line on each
|
||||||
|
invocation.
|
||||||
|
|
||||||
|
# RETURN VALUES
|
||||||
|
|
||||||
|
ShellCheck uses the follow exit codes:
|
||||||
|
|
||||||
|
+ 0: All files successfully scanned with no issues.
|
||||||
|
+ 1: All files successfully scanned with some issues.
|
||||||
|
+ 2: Some files could not be processed (e.g. file not found).
|
||||||
|
+ 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.
|
||||||
|
|
||||||
|
# REPORTING BUGS
|
||||||
|
Bugs and issues can be reported on GitHub:
|
||||||
|
|
||||||
|
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 ALSO
|
||||||
|
|
||||||
|
sh(1) bash(1)
|
370
shellcheck.hs
370
shellcheck.hs
@@ -1,85 +1,343 @@
|
|||||||
{-
|
{-
|
||||||
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
http://www.vidarholen.net/contents/shellcheck
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
ShellCheck is distributed in the hope that it will be useful,
|
ShellCheck is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU Affero General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
|
import ShellCheck.Data
|
||||||
|
import ShellCheck.Checker
|
||||||
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Regex
|
||||||
|
|
||||||
|
import ShellCheck.Formatter.Format
|
||||||
|
import qualified ShellCheck.Formatter.CheckStyle
|
||||||
|
import qualified ShellCheck.Formatter.GCC
|
||||||
|
import qualified ShellCheck.Formatter.JSON
|
||||||
|
import qualified ShellCheck.Formatter.TTY
|
||||||
|
|
||||||
|
import Control.Exception
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import GHC.Exts
|
import Control.Monad.Except
|
||||||
import GHC.IO.Device
|
import Data.Bits
|
||||||
import ShellCheck.Simple
|
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.Directory
|
||||||
import System.Environment
|
import System.Environment
|
||||||
import System.Exit
|
import System.Exit
|
||||||
import System.IO
|
import System.IO
|
||||||
|
|
||||||
clear = ansi 0
|
data Flag = Flag String String
|
||||||
ansi n = "\x1B[" ++ (show n) ++ "m"
|
data Status =
|
||||||
|
NoProblems
|
||||||
|
| SomeProblems
|
||||||
|
| SupportFailure
|
||||||
|
| SyntaxFailure
|
||||||
|
| RuntimeException
|
||||||
|
deriving (Ord, Eq, Show)
|
||||||
|
|
||||||
colorForLevel "error" = 31 -- red
|
instance Monoid Status where
|
||||||
colorForLevel "warning" = 33 -- yellow
|
mempty = NoProblems
|
||||||
colorForLevel "info" = 32 -- green
|
mappend = max
|
||||||
colorForLevel "style" = 32 -- green
|
|
||||||
colorForLevel "message" = 1 -- bold
|
|
||||||
colorForLevel "source" = 0 -- none
|
|
||||||
colorForLevel _ = 0 -- none
|
|
||||||
|
|
||||||
colorComment level comment = (ansi $ colorForLevel level) ++ comment ++ clear
|
data Options = Options {
|
||||||
|
checkSpec :: CheckSpec,
|
||||||
|
externalSources :: Bool,
|
||||||
|
formatterOptions :: FormatterOptions
|
||||||
|
}
|
||||||
|
|
||||||
doFile path colorFunc = do
|
defaultOptions = Options {
|
||||||
let actualPath = if path == "-" then "/dev/stdin" else path
|
checkSpec = emptyCheckSpec,
|
||||||
exists <- doesFileExist actualPath
|
externalSources = False,
|
||||||
if exists then do
|
formatterOptions = FormatterOptions {
|
||||||
contents <- readFile actualPath
|
foColorOption = ColorAuto
|
||||||
doInput path contents colorFunc
|
}
|
||||||
else do
|
}
|
||||||
hPutStrLn stderr (colorFunc "error" $ "No such file: " ++ actualPath)
|
|
||||||
return False
|
|
||||||
|
|
||||||
doInput filename contents colorFunc = do
|
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
|
||||||
let fileLines = lines contents
|
options = [
|
||||||
let lineCount = length fileLines
|
Option "e" ["exclude"]
|
||||||
let comments = shellCheck contents
|
(ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings",
|
||||||
let groups = groupWith scLine comments
|
Option "f" ["format"]
|
||||||
mapM_ (\x -> do
|
(ReqArg (Flag "format") "FORMAT") "output format",
|
||||||
let lineNum = scLine (head x)
|
Option "C" ["color"]
|
||||||
let line = if lineNum < 1 || lineNum > lineCount
|
(OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN")
|
||||||
then ""
|
"Use color (auto, always, never)",
|
||||||
else fileLines !! (lineNum - 1)
|
Option "s" ["shell"]
|
||||||
putStrLn ""
|
(ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (sh,bash,dash,ksh)",
|
||||||
putStrLn $ colorFunc "message" ("In " ++ filename ++" line " ++ (show $ lineNum) ++ ":")
|
Option "x" ["external-sources"]
|
||||||
putStrLn (colorFunc "source" line)
|
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES.",
|
||||||
mapM (\c -> putStrLn (colorFunc (scSeverity c) $ cuteIndent c)) x
|
Option "V" ["version"]
|
||||||
putStrLn ""
|
(NoArg $ Flag "version" "true") "Print version information"
|
||||||
) groups
|
]
|
||||||
return $ null comments
|
|
||||||
|
|
||||||
cuteIndent comment =
|
printErr = lift . hPutStrLn stderr
|
||||||
(replicate ((scColumn comment) - 1) ' ') ++ "^-- " ++ (scMessage comment)
|
|
||||||
|
|
||||||
getColorFunc = do
|
parseArguments :: [String] -> ExceptT Status IO ([Flag], [FilePath])
|
||||||
term <- hIsTerminalDevice stdout
|
parseArguments argv =
|
||||||
return $ if term then colorComment else const id
|
case getOpt Permute options argv of
|
||||||
|
(opts, files, []) -> return (opts, files)
|
||||||
|
(_, _, errors) -> do
|
||||||
|
printErr $ concat errors ++ "\n" ++ usageInfo usageHeader options
|
||||||
|
throwError SyntaxFailure
|
||||||
|
|
||||||
|
formats :: FormatterOptions -> Map.Map String (IO Formatter)
|
||||||
|
formats options = Map.fromList [
|
||||||
|
("checkstyle", ShellCheck.Formatter.CheckStyle.format),
|
||||||
|
("gcc", ShellCheck.Formatter.GCC.format),
|
||||||
|
("json", ShellCheck.Formatter.JSON.format),
|
||||||
|
("tty", ShellCheck.Formatter.TTY.format options)
|
||||||
|
]
|
||||||
|
|
||||||
|
getOption [] _ = Nothing
|
||||||
|
getOption (Flag var val:_) name | name == var = return val
|
||||||
|
getOption (_:rest) flag = getOption rest flag
|
||||||
|
|
||||||
|
getOptions options name =
|
||||||
|
map (\(Flag _ val) -> val) . filter (\(Flag var _) -> var == name) $ options
|
||||||
|
|
||||||
|
split char str =
|
||||||
|
split' str []
|
||||||
|
where
|
||||||
|
split' (a:rest) element =
|
||||||
|
if a == char
|
||||||
|
then reverse element : split' rest []
|
||||||
|
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
|
||||||
|
|
||||||
|
getEnvArgs = do
|
||||||
|
opts <- getEnv "SHELLCHECK_OPTS" `catch` cantWaitForLookupEnv
|
||||||
|
return . filter (not . null) $ opts `splitOn` mkRegex " +"
|
||||||
|
where
|
||||||
|
cantWaitForLookupEnv :: IOException -> IO String
|
||||||
|
cantWaitForLookupEnv = const $ return ""
|
||||||
|
|
||||||
main = do
|
main = do
|
||||||
args <- getArgs
|
params <- getArgs
|
||||||
colors <- getColorFunc
|
envOpts <- getEnvArgs
|
||||||
if null args then do
|
let args = envOpts ++ params
|
||||||
hPutStrLn stderr "shellcheck -- bash/sh script static analysis tool"
|
status <- toStatus $ do
|
||||||
hPutStrLn stderr "Usage: shellcheck filenames..."
|
(flags, files) <- parseArguments args
|
||||||
exitFailure
|
process flags files
|
||||||
else do
|
exitWith $ statusToCode status
|
||||||
statuses <- mapM (\f -> doFile f colors) args
|
|
||||||
if and statuses then exitSuccess else exitFailure
|
|
||||||
|
|
||||||
|
statusToCode status =
|
||||||
|
case status of
|
||||||
|
NoProblems -> ExitSuccess
|
||||||
|
SomeProblems -> ExitFailure 1
|
||||||
|
SyntaxFailure -> ExitFailure 3
|
||||||
|
SupportFailure -> ExitFailure 4
|
||||||
|
RuntimeException -> ExitFailure 2
|
||||||
|
|
||||||
|
process :: [Flag] -> [FilePath] -> ExceptT Status IO Status
|
||||||
|
process flags files = do
|
||||||
|
options <- foldM (flip parseOption) defaultOptions flags
|
||||||
|
verifyFiles files
|
||||||
|
let format = fromMaybe "tty" $ getOption flags "format"
|
||||||
|
let formatters = formats $ formatterOptions options
|
||||||
|
formatter <-
|
||||||
|
case Map.lookup format formatters of
|
||||||
|
Nothing -> do
|
||||||
|
printErr $ "Unknown format " ++ format
|
||||||
|
printErr "Supported formats:"
|
||||||
|
mapM_ (printErr . write) $ Map.keys formatters
|
||||||
|
throwError SupportFailure
|
||||||
|
where write s = " " ++ s
|
||||||
|
Just f -> ExceptT $ fmap Right f
|
||||||
|
sys <- lift $ ioInterface options files
|
||||||
|
lift $ runFormatter sys formatter options files
|
||||||
|
|
||||||
|
runFormatter :: SystemInterface IO -> Formatter -> Options -> [FilePath]
|
||||||
|
-> IO Status
|
||||||
|
runFormatter sys format options files = do
|
||||||
|
header format
|
||||||
|
result <- foldM f NoProblems files
|
||||||
|
footer format
|
||||||
|
return result
|
||||||
|
where
|
||||||
|
f :: Status -> FilePath -> IO Status
|
||||||
|
f status file = 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)
|
||||||
|
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
|
||||||
|
|
||||||
|
parseColorOption colorOption =
|
||||||
|
case colorOption of
|
||||||
|
"auto" -> ColorAuto
|
||||||
|
"always" -> ColorAlways
|
||||||
|
"never" -> ColorNever
|
||||||
|
_ -> error $ "Bad value for --color `" ++ colorOption ++ "'"
|
||||||
|
|
||||||
|
parseOption flag options =
|
||||||
|
case flag of
|
||||||
|
Flag "shell" str ->
|
||||||
|
fromMaybe (die $ "Unknown shell: " ++ str) $ do
|
||||||
|
shell <- shellForExecutable str
|
||||||
|
return $ return options {
|
||||||
|
checkSpec = (checkSpec options) {
|
||||||
|
csShellTypeOverride = Just shell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Flag "exclude" str -> do
|
||||||
|
new <- mapM parseNum $ split ',' str
|
||||||
|
let old = csExcludedWarnings . checkSpec $ options
|
||||||
|
return options {
|
||||||
|
checkSpec = (checkSpec options) {
|
||||||
|
csExcludedWarnings = new ++ old
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Flag "version" _ -> do
|
||||||
|
liftIO printVersion
|
||||||
|
throwError NoProblems
|
||||||
|
|
||||||
|
Flag "externals" _ ->
|
||||||
|
return options {
|
||||||
|
externalSources = True
|
||||||
|
}
|
||||||
|
|
||||||
|
Flag "color" color ->
|
||||||
|
return options {
|
||||||
|
formatterOptions = (formatterOptions options) {
|
||||||
|
foColorOption = parseColorOption color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ -> return options
|
||||||
|
where
|
||||||
|
die s = do
|
||||||
|
printErr s
|
||||||
|
throwError SupportFailure
|
||||||
|
parseNum ('S':'C':str) = parseNum str
|
||||||
|
parseNum num = do
|
||||||
|
unless (all isDigit num) $ do
|
||||||
|
printErr $ "Bad exclusion: " ++ num
|
||||||
|
throwError SyntaxFailure
|
||||||
|
return (Prelude.read num :: Integer)
|
||||||
|
|
||||||
|
ioInterface options files = do
|
||||||
|
inputs <- mapM normalize files
|
||||||
|
return SystemInterface {
|
||||||
|
siReadFile = get inputs
|
||||||
|
}
|
||||||
|
where
|
||||||
|
get inputs file = do
|
||||||
|
ok <- allowable inputs file
|
||||||
|
if ok
|
||||||
|
then (Right <$> inputFile file) `catch` handler
|
||||||
|
else return $ Left (file ++ " was not specified as input (see shellcheck -x).")
|
||||||
|
|
||||||
|
where
|
||||||
|
handler :: IOException -> IO (Either ErrorMessage String)
|
||||||
|
handler ex = return . Left $ show ex
|
||||||
|
|
||||||
|
allowable inputs x =
|
||||||
|
if externalSources options
|
||||||
|
then return True
|
||||||
|
else do
|
||||||
|
path <- normalize x
|
||||||
|
return $ path `elem` inputs
|
||||||
|
|
||||||
|
normalize x =
|
||||||
|
canonicalizePath x `catch` fallback x
|
||||||
|
where
|
||||||
|
fallback :: FilePath -> IOException -> IO FilePath
|
||||||
|
fallback path _ = return path
|
||||||
|
|
||||||
|
inputFile file = do
|
||||||
|
handle <-
|
||||||
|
if file == "-"
|
||||||
|
then return stdin
|
||||||
|
else openBinaryFile file ReadMode
|
||||||
|
|
||||||
|
hSetBinaryMode handle True
|
||||||
|
contents <- decodeString <$> hGetContents handle -- closes handle
|
||||||
|
|
||||||
|
seq (length contents) $
|
||||||
|
return contents
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
printErr "No files specified.\n"
|
||||||
|
printErr $ usageInfo usageHeader options
|
||||||
|
throwError SyntaxFailure
|
||||||
|
|
||||||
|
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"
|
||||||
|
35
stack.yaml
Normal file
35
stack.yaml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# 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
|
||||||
|
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
|
@@ -1,65 +0,0 @@
|
|||||||
#!/usr/bin/env runhaskell
|
|
||||||
-- #!/usr/bin/env runhugs
|
|
||||||
-- $Id: quickcheck,v 1.4 2003/01/08 15:09:22 shae Exp $
|
|
||||||
-- This file defines a command
|
|
||||||
-- quickCheck <options> <files>
|
|
||||||
-- which invokes quickCheck on all properties defined in the files given as
|
|
||||||
-- arguments, by generating an input script for hugs and then invoking it.
|
|
||||||
-- quickCheck recognises the options
|
|
||||||
-- +names print the name of each property before checking it
|
|
||||||
-- -names do not print property names (the default)
|
|
||||||
-- +verbose displays each test case before running
|
|
||||||
-- -verbose do not displays each test case before running (the default)
|
|
||||||
-- Other options (beginning with + or -) are passed unchanged to hugs.
|
|
||||||
--
|
|
||||||
-- Change the first line of this file to the location of runhaskell or runhugs
|
|
||||||
-- on your system.
|
|
||||||
-- Make the file executable.
|
|
||||||
--
|
|
||||||
-- TODO:
|
|
||||||
-- someone on #haskell asked about supporting QC tests inside LaTeX, ex. \{begin} \{end}, how?
|
|
||||||
|
|
||||||
import System.Cmd
|
|
||||||
import System.Directory (findExecutable)
|
|
||||||
import System.Environment
|
|
||||||
import Data.List
|
|
||||||
import Data.Maybe (fromJust)
|
|
||||||
|
|
||||||
main :: IO ()
|
|
||||||
main = do as<-getArgs
|
|
||||||
sequence_ (map (process (filter isOption as))
|
|
||||||
(filter (not.isOption) as))
|
|
||||||
|
|
||||||
-- ugly hack for .lhs files, is there a better way?
|
|
||||||
unlit [] = []
|
|
||||||
unlit x = if (head x) == '>' then (tail x) else x
|
|
||||||
|
|
||||||
process opts file =
|
|
||||||
let (namesOpt,opts') = getOption "names" "-names" opts
|
|
||||||
(verboseOpt,opts'') = getOption "verbose" "-verbose" opts' in
|
|
||||||
do xs<-readFile file
|
|
||||||
let names = nub$ filter (\x -> (("> prop_" `isPrefixOf` x) || ("prop_" `isPrefixOf` x)))
|
|
||||||
(map (fst.head.lex.unlit) (lines xs))
|
|
||||||
if null names then
|
|
||||||
putStr (file++": no properties to check\n")
|
|
||||||
else do writeFile "hugsin"$
|
|
||||||
unlines ((":load "++file):
|
|
||||||
":m +Test.QuickCheck":
|
|
||||||
"let quackCheck p = quickCheckWith (stdArgs { maxSuccess = 1 }) p ":
|
|
||||||
[(if namesOpt=="+names" then
|
|
||||||
"putStr \""++p++": \" >> "
|
|
||||||
else "") ++
|
|
||||||
("quackCheck ")
|
|
||||||
++ p | p<-names])
|
|
||||||
-- To use ghci
|
|
||||||
ghci <- findExecutable "ghci"
|
|
||||||
system (fromJust ghci ++options opts''++" <hugsin")
|
|
||||||
return ()
|
|
||||||
|
|
||||||
isOption xs = head xs `elem` "-+"
|
|
||||||
|
|
||||||
options opts = unwords ["\""++opt++"\"" | opt<-opts]
|
|
||||||
|
|
||||||
getOption name def opts =
|
|
||||||
let opt = head [opt | opt<-opts++[def], isPrefixOf name (drop 1 opt)] in
|
|
||||||
(opt, filter (/=opt) opts)
|
|
@@ -1,22 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Todo: Find a way to make this not suck.
|
|
||||||
|
|
||||||
ulimit -t 60 # Sometimes GHC ends in a spin loop, and this is easier than debugging
|
|
||||||
|
|
||||||
[[ -e test/quackCheck.hs ]] || { echo "Are you running me from the wrong directory?"; exit 1; }
|
|
||||||
[[ $1 == -v ]] && pattern="" || pattern="FAIL"
|
|
||||||
|
|
||||||
find . -name '*.hs' -exec bash -c '
|
|
||||||
grep -v "^module " "$1" > quack.tmp.hs
|
|
||||||
./test/quackCheck.hs +names quack.tmp.hs
|
|
||||||
' -- {} \; 2>&1 | grep -i "$pattern"
|
|
||||||
result=$?
|
|
||||||
rm -f quack.tmp.hs hugsin
|
|
||||||
|
|
||||||
if [[ $result == 0 ]]
|
|
||||||
then
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
24
test/shellcheck.hs
Normal file
24
test/shellcheck.hs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
module Main where
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import System.Exit
|
||||||
|
import qualified ShellCheck.Checker
|
||||||
|
import qualified ShellCheck.Analytics
|
||||||
|
import qualified ShellCheck.AnalyzerLib
|
||||||
|
import qualified ShellCheck.Parser
|
||||||
|
import qualified ShellCheck.Checks.Commands
|
||||||
|
import qualified ShellCheck.Checks.ShellSupport
|
||||||
|
|
||||||
|
main = do
|
||||||
|
putStrLn "Running ShellCheck tests..."
|
||||||
|
results <- sequence [
|
||||||
|
ShellCheck.Checker.runTests,
|
||||||
|
ShellCheck.Checks.Commands.runTests,
|
||||||
|
ShellCheck.Checks.ShellSupport.runTests,
|
||||||
|
ShellCheck.Analytics.runTests,
|
||||||
|
ShellCheck.AnalyzerLib.runTests,
|
||||||
|
ShellCheck.Parser.runTests
|
||||||
|
]
|
||||||
|
if and results
|
||||||
|
then exitSuccess
|
||||||
|
else exitFailure
|
Reference in New Issue
Block a user