mirror of
https://github.com/koalaman/shellcheck
synced 2025-07-06 04:51:37 -07:00
Compare commits
566 commits
Author | SHA1 | Date | |
---|---|---|---|
|
20d11c1c33 | ||
|
47d358c1d4 | ||
|
ad58768563 | ||
|
62a8ecf9bf | ||
|
0b5410d759 | ||
|
975cfeee50 | ||
|
b381658dbc | ||
|
950578ae0e | ||
|
f78714e0f6 | ||
|
de07ec1c56 | ||
|
85066dd805 | ||
|
140274b810 | ||
|
dc41f0cc5b | ||
|
fbb8386797 | ||
|
efb5a5a274 | ||
|
553a80f77a | ||
|
7fc992d0dc | ||
|
c553288085 | ||
|
1be41dd652 | ||
|
2eddec86d3 | ||
|
c41f3a4b8a | ||
|
574c6d18fb | ||
|
e4853af5b0 | ||
|
72af76f443 | ||
|
8ff0c5be7a | ||
|
4f628cbe2a | ||
|
bc60607f9e | ||
|
3a9ddae06b | ||
|
cbf0b33463 | ||
|
fe315a25c4 | ||
|
d3001f337a | ||
|
7deb7e853b | ||
|
26b949b9b0 | ||
|
5adfea21ee | ||
|
0ecaf2b5f1 | ||
|
195b70db8c | ||
|
3c75d82db5 | ||
|
7f3f014d49 | ||
|
944d87915a | ||
|
47bff1d5fd | ||
|
0ee46a0f33 | ||
|
792466bc22 | ||
|
097018754b | ||
|
f2932ebcdc | ||
|
5e3e98bcb0 | ||
|
68bc17b8ea | ||
|
5c2be767ab | ||
|
79e43c4550 | ||
|
ca65071d77 | ||
|
8a1b24c7af | ||
|
88e441453b | ||
|
1487e57a46 | ||
|
68e6f02267 | ||
|
c7611dfcc6 | ||
|
15f132e167 | ||
|
4e69767b03 | ||
|
8bf8cf5cc7 | ||
|
17ebc3dda0 | ||
|
4cd76283da | ||
|
cd6fdee99b | ||
|
c831616f3a | ||
|
38c5ba7c79 | ||
|
2696c6472d | ||
|
d590a35ff8 | ||
|
6d2f3d8628 | ||
|
4c85274921 | ||
|
6593096ba0 | ||
|
98b8dc0720 | ||
|
95c0cc2e4b | ||
|
e5fdec970a | ||
|
8746c6e7f2 | ||
|
61b7e66f80 | ||
|
b408f54620 | ||
|
3946cbd4a0 | ||
|
c4b7b79b8b | ||
|
23e76de4f2 | ||
|
15de97e33f | ||
|
78d1ee0222 | ||
|
ac8fb00504 | ||
|
a13cb85f49 | ||
|
a7a906e2cb | ||
|
d705716dc4 | ||
|
76ff702e93 | ||
|
4f81dbe839 | ||
|
796c6bd848 | ||
|
69fe4e1306 | ||
|
2c5155e43d | ||
|
04a86245a1 | ||
|
79491db9f6 | ||
|
5241878e59 | ||
|
30b32af873 | ||
|
da8854cac6 | ||
|
39a035793c | ||
|
0a7bb1822e | ||
|
c4123375e0 | ||
|
52dc66349b | ||
|
9cb21c8557 | ||
|
50db9a29c4 | ||
|
94214ee725 | ||
|
37dfb67768 | ||
|
a7e65dca8d | ||
|
8bc7345aa7 | ||
|
ad3c3146f0 | ||
|
55be4543f2 | ||
|
8c4c112c25 | ||
|
d80fdfa9e8 | ||
|
1565091b1d | ||
|
d056549406 | ||
|
f5758e1789 | ||
|
6a44a19f17 | ||
|
b1b95c2c17 | ||
|
de95624d31 | ||
|
b5ab220652 | ||
|
1bce426fcf | ||
|
ba86c6363c | ||
|
67abfe159e | ||
|
025cc5266e | ||
|
5a6f4840ad | ||
|
9e0fdbe431 | ||
|
b7f88ec4b7 | ||
|
7b0589988f | ||
|
71889c139a | ||
|
a6984cddb0 | ||
|
3f40b688ee | ||
|
6c81505870 | ||
|
10afe83ce3 | ||
|
a786f996a1 | ||
|
6e5b5401c6 | ||
|
71c0fcb737 | ||
|
add49cda17 | ||
|
e1ad063834 | ||
|
ee41c780f4 | ||
|
980e7d3ca8 | ||
|
dedf932fe8 | ||
|
3bd7df955b | ||
|
dab77b2c8d | ||
|
f983d9ae93 | ||
|
bfe4342697 | ||
|
a47a42cb45 | ||
|
eed0174e90 | ||
|
0c46b8b2d5 | ||
|
208e38358e | ||
|
c1452e0d17 | ||
|
c97abdb939 | ||
|
f242922a2e | ||
|
a37803d2b8 | ||
|
09d04c4c9b | ||
|
e5028481e2 | ||
|
5a961371a7 | ||
|
e5208ccb50 | ||
|
4c1d9171b2 | ||
|
a9e7bf1950 | ||
|
f2729f73cb | ||
|
175d3cc9b7 | ||
|
5c50b0b189 | ||
|
74282b0a93 | ||
|
b6d4952e2e | ||
|
fdcce458c1 | ||
|
ca255fe326 | ||
|
a3b8be82fe | ||
|
ac63dc33c9 | ||
|
903421fb5d | ||
|
00ffd2db33 | ||
|
1e1045e73e | ||
|
be8e4b2b8a | ||
|
a71a13c2fc | ||
|
1aeab287e6 | ||
|
2a95bc6be3 | ||
|
4fd0615501 | ||
|
8b3c37aa36 | ||
|
dc2f388310 | ||
|
99a94421ab | ||
|
6a6d8e9fc4 | ||
|
592c17e4f2 | ||
|
9605396bef | ||
|
c89ec2fd49 | ||
|
410ec54617 | ||
|
90d3172dfe | ||
|
d18b2553cf | ||
|
dd747b2a98 | ||
|
9490b94886 | ||
|
372c0b667e | ||
|
01aee1a859 | ||
|
c9e27c2470 | ||
|
4ffa9cc397 | ||
|
b625cc1acc | ||
|
f03c437e2f | ||
|
824c802b63 | ||
|
b3932dfa10 | ||
|
a54965dd2c | ||
|
46b678fca8 | ||
|
be0d5d4163 | ||
|
5fec3f9b34 | ||
|
1164aa4efc | ||
|
ff85a5a2a2 | ||
|
08b437974e | ||
|
15fd2c314c | ||
|
e6e8ab0415 | ||
|
b1ca3929e3 | ||
|
c05380d518 | ||
|
2842ce97b8 | ||
|
78dea1d4f9 | ||
|
5a3eb89e38 | ||
|
a526ee0829 | ||
|
8c5fdc3522 | ||
|
ae199edb68 | ||
|
7cfcf6db8a | ||
|
a7c5be93dc | ||
|
8754c21244 | ||
|
985ca2530d | ||
|
3cae6cd6ab | ||
|
74b1745a19 | ||
|
495e34d101 | ||
|
2a16a4e8c1 | ||
|
3342902d9a | ||
|
0786b2bf3c | ||
|
84d8530f14 | ||
|
86e2b76730 | ||
|
b770984dfc | ||
|
d9c9e60fb0 | ||
|
14056a7f3a | ||
|
a524929b69 | ||
|
fa7943ac0e | ||
|
81c2ecaccb | ||
|
fcba462a99 | ||
|
43aca62ca7 | ||
|
128351f5ef | ||
|
d71d6ff294 | ||
|
bd65b67578 | ||
|
149b4dbd6f | ||
|
ef5f9a7af5 | ||
|
581981ba76 | ||
|
fcc473e27f | ||
|
0845b81183 | ||
|
966fb3e3dd | ||
|
f28462b01c | ||
|
ccab132b38 | ||
|
4806719035 | ||
|
0df9345142 | ||
|
77069f7445 | ||
|
04db46381f | ||
|
c76b8d9a32 | ||
|
d0dd81e1fa | ||
|
f440912279 | ||
|
3ce310e939 | ||
|
a30ac402eb | ||
|
4a27c9a8d5 | ||
|
b5f5e6347d | ||
|
c57e447c89 | ||
|
e9784fa9a7 | ||
|
f1148b8b41 | ||
|
982681fc05 | ||
|
52dac51cd4 | ||
|
30bb0e0093 | ||
|
d1d574c091 | ||
|
ea4e0091c7 | ||
|
81d9f7e640 | ||
|
69469c3603 | ||
|
5cf6e01ce9 | ||
|
f7857028f7 | ||
|
b261ec24f9 | ||
|
819470fa1d | ||
|
2f28847b08 | ||
|
e47480e93a | ||
|
9caeec104b | ||
|
95b3cbf071 | ||
|
e7f05d662a | ||
|
3ee4419ef4 | ||
|
8dc0fdb4cc | ||
|
da4885a71d | ||
|
642ad86125 | ||
|
f77a545282 | ||
|
7946bf5657 | ||
|
cc04b40119 | ||
|
c3bce51de3 | ||
|
a4042f7523 | ||
|
363c0633e0 | ||
|
7ceb1f1519 | ||
|
f1bdda54cb | ||
|
9aa4c22aa6 | ||
|
399c04cc17 | ||
|
fd595d1058 | ||
|
7c44e1060f | ||
|
2821552688 | ||
|
2034e3886e | ||
|
fa15c0a454 | ||
|
88cdb4e2c9 | ||
|
2292e852e5 | ||
|
ade2bf7b87 | ||
|
e6e558946c | ||
|
3a118246ef | ||
|
dd626686c4 | ||
|
866cbd0aa4 | ||
|
d7971dafd1 | ||
|
9092080a84 | ||
|
499c99372e | ||
|
d9a9d5db86 | ||
|
c5de58ae84 | ||
|
4c186c20b9 | ||
|
e5ad4cf420 | ||
|
eea823e3d0 | ||
|
3b6972fbf1 | ||
|
14a38b94cc | ||
|
71f1db6609 | ||
|
bcca66eb6b | ||
|
8db220ae43 | ||
|
efd49e486f | ||
|
0dd5c67bdf | ||
|
290fc8b945 | ||
|
7b2092b3cd | ||
|
788aee1b7c | ||
|
0d128dd918 | ||
|
c3aaa27540 | ||
|
3aedda766d | ||
|
205ba429b3 | ||
|
05bdeae3ab | ||
|
38251abe26 | ||
|
6f7eee4a27 | ||
|
23cddb037e | ||
|
093df8cb24 | ||
|
fac97a5301 | ||
|
ad92cb4112 | ||
|
3a296cd788 | ||
|
db4701d8b5 | ||
|
e7df718724 | ||
|
b044f5b23a | ||
|
8012f6761d | ||
|
2536507060 | ||
|
09aa15c9b7 | ||
|
9a54e91195 | ||
|
4e703e5c61 | ||
|
64733cc110 | ||
|
dc9032fca5 | ||
|
40216487d6 | ||
|
747bd8fd6a | ||
|
f5fd9c2fed | ||
|
10817533d6 | ||
|
b5da99c6b0 | ||
|
b0f05018c1 | ||
|
9d64d78c32 | ||
|
081f7eba24 | ||
|
ecacc2e9bb | ||
|
81b7ee5598 | ||
|
c85ce2cb06 | ||
|
98c7934c46 | ||
|
7384cec3f6 | ||
|
5b6fd60279 | ||
|
da7b28213e | ||
|
c61fc7546e | ||
|
8c0bf8d41f | ||
|
bb0a571a1e | ||
|
fed4a048bc | ||
|
e5745568e8 | ||
|
4dd762253f | ||
|
378c9a2f2c | ||
|
cf8066c07c | ||
|
9b61506e0b | ||
|
2f61b17518 | ||
|
b939f86331 | ||
|
a44f3edb14 | ||
|
e33146d530 | ||
|
fe81dc1c27 | ||
|
fbc8d2cb2f | ||
|
c471e45822 | ||
|
754ab22d94 | ||
|
4956b006ac | ||
|
02e07625d1 | ||
|
44471b73cc | ||
|
364c33395e | ||
|
0d58337cdd | ||
|
9eb63c97e6 | ||
|
8be60028ef | ||
|
9b077e28cb | ||
|
99f6554c9b | ||
|
163629825f | ||
|
022bc8277c | ||
|
5e60f1eddb | ||
|
163b2f12e2 | ||
|
5100960303 | ||
|
b61a7658d6 | ||
|
ab369a35c9 | ||
|
331e89be99 | ||
|
fe25a2b00e | ||
|
9e60b3ea84 | ||
|
d47f3ff986 | ||
|
2f26600653 | ||
|
aaa3554720 | ||
|
cff3e22911 | ||
|
5669eb2203 | ||
|
b68df1882d | ||
|
087865c680 | ||
|
19c6f22c3f | ||
|
98952df35b | ||
|
a277efdbb1 | ||
|
45687b0548 | ||
|
ecdc21b0b7 | ||
|
4eb42fa3c1 | ||
|
f02c297fdd | ||
|
ea83b602d7 | ||
|
88cd21fd0f | ||
|
83435c4f2e | ||
|
4324b4a213 | ||
|
a69d6cb661 | ||
|
8442695b73 | ||
|
670c1de01b | ||
|
b9b6975bfa | ||
|
d6bb8fc0d8 | ||
|
8bb5e01401 | ||
|
2e59eba6eb | ||
|
15ff87cf80 | ||
|
99e9d5c54b | ||
|
dff8f9492a | ||
|
c5756760cb | ||
|
2e5c56b270 | ||
|
9584266a8b | ||
|
5fbaae2bb3 | ||
|
fbb14d6b38 | ||
|
2cfd1f2714 | ||
|
953d9bc56d | ||
|
e272fa04ee | ||
|
81e84c2939 | ||
|
34939ca0b7 | ||
|
e7820479f0 | ||
|
8480563672 | ||
|
dfbcc9595e | ||
|
2c0766825e | ||
|
cb4f4e7edc | ||
|
0607039d41 | ||
|
46f177b5be | ||
|
eaccd3d02c | ||
|
35033a9f2f | ||
|
19355226e1 | ||
|
4e7e3f9456 | ||
|
bd3299edd3 | ||
|
cc3884cf9f | ||
|
6ba1af0898 | ||
|
8e332ce879 | ||
|
7e40d97e7a | ||
|
775c0c11d7 | ||
|
5196ab1f95 | ||
|
b625562d60 | ||
|
18e80284ec | ||
|
65044c2568 | ||
|
61b7dd610d | ||
|
4b0e5ca119 | ||
|
619662adb6 | ||
|
28d3279ba6 | ||
|
256457c47a | ||
|
3104cec770 | ||
|
f100c2939e | ||
|
8d99926554 | ||
|
218deb6d01 | ||
|
c4cc2debb7 | ||
|
cfd68ee0c2 | ||
|
58783ab3cc | ||
|
43191fa71d | ||
|
c9be7ab2eb | ||
|
fb89cdf4ad | ||
|
9e59bcca91 | ||
|
a62d9f10c2 | ||
|
e72fbb2640 | ||
|
17e591233f | ||
|
50067ddf94 | ||
|
3fa5b7d3bd | ||
|
5e6d50f493 | ||
|
e779aedac3 | ||
|
3ef1175566 | ||
|
506ffa849b | ||
|
b864242caa | ||
|
3e50a2fce8 | ||
|
10c2d827fa | ||
|
e0e2edd525 | ||
|
c5b6d6f027 | ||
|
beee9b22ca | ||
|
1ac2c31728 | ||
|
cc81bdee31 | ||
|
34885142e7 | ||
|
14e6806092 | ||
|
5d753212fb | ||
|
5b86777f9d | ||
|
7a9dbc042b | ||
|
9793d94206 | ||
|
baab5b53e0 | ||
|
210cdcd01a | ||
|
1b884a17ea | ||
|
b52f58473d | ||
|
376e78b631 | ||
|
40aacc3345 | ||
|
739eaadbf5 | ||
|
6b88a341f3 | ||
|
a61d8a232c | ||
|
12d9c1b76d | ||
|
a2b5b6a500 | ||
|
5cf2c00ff7 | ||
|
a08ad3bee9 | ||
|
417e13f129 | ||
|
536cb584f4 | ||
|
c2a15ce8e9 | ||
|
d6adbfde78 | ||
|
2030b83607 | ||
|
8aa40c43ed | ||
|
5a42f4b938 | ||
|
a7a406c43c | ||
|
1d126960f3 | ||
|
60e80e4ce1 | ||
|
e0daa936d2 | ||
|
75863a887e | ||
|
413f0048b8 | ||
|
e7b5fb9742 | ||
|
30523555af | ||
|
58d3e50f43 | ||
|
73cc11fd0a | ||
|
163c710ba7 | ||
|
ab1610b004 | ||
|
148468be70 | ||
|
5eac721fcf | ||
|
b58bb4ba9d | ||
|
999b7e2596 | ||
|
a9d564a8bc | ||
|
8a7497c4f0 | ||
|
1eac0d7340 | ||
|
f8c1ffb0dc | ||
|
3e17a20965 | ||
|
1c6202dba4 | ||
|
64c31d9142 | ||
|
8a6679fd8a | ||
|
facf0d1e27 | ||
|
cd38afce26 | ||
|
5084ba8d7e | ||
|
ed331b816b | ||
|
cfa2a663af | ||
|
df4928f4e3 | ||
|
9747b1d5c3 | ||
|
fa841cb270 | ||
|
e8501151dd | ||
|
9027a9239f | ||
|
773e98868d | ||
|
d45ab327b0 | ||
|
0f9b0f18a4 | ||
|
322842b57e | ||
|
b6cff5ea0e | ||
|
8f105074fe | ||
|
d22e0aa4a7 | ||
|
fb55072302 | ||
|
0cc5ed4563 | ||
|
ca41440a67 | ||
|
1cf0aa25e9 | ||
|
4604066c37 | ||
|
2ebf522a52 | ||
|
e4eb2d157f | ||
|
f109f9ab92 | ||
|
67e091674e | ||
|
f833ee3d5a | ||
|
f55d8c45e5 | ||
|
14ee462ccd | ||
|
b3c04ce3d0 | ||
|
b0dbc79f69 | ||
|
2a8170ba05 | ||
|
01f4423465 | ||
|
d2fa88dd91 | ||
|
a30e42ab05 | ||
|
84d6e53659 | ||
|
00574dd1fc | ||
|
a82e606e8d | ||
|
fdd02c94c0 | ||
|
9423691039 |
82 changed files with 9442 additions and 2088 deletions
|
@ -1,73 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
build_linux() {
|
|
||||||
# Linux Docker image
|
|
||||||
name="$DOCKER_BASE"
|
|
||||||
DOCKER_BUILDS="$DOCKER_BUILDS $name"
|
|
||||||
docker build -t "$name:current" .
|
|
||||||
docker run "$name:current" --version
|
|
||||||
printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
|
|
||||||
docker run -v "$PWD:/mnt" "$name:current" myscript
|
|
||||||
|
|
||||||
# Copy static executable from docker image
|
|
||||||
id=$(docker create "$name:current")
|
|
||||||
docker cp "$id:/bin/shellcheck" "shellcheck"
|
|
||||||
docker rm "$id"
|
|
||||||
ls -l shellcheck
|
|
||||||
./shellcheck myscript
|
|
||||||
for tag in $TAGS
|
|
||||||
do
|
|
||||||
cp "shellcheck" "deploy/shellcheck-$tag.linux-x86_64";
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
build_aarch64() {
|
|
||||||
# Linux aarch64 static executable
|
|
||||||
docker run -v "$PWD:/mnt" koalaman/aarch64-builder 'buildsc'
|
|
||||||
for tag in $TAGS
|
|
||||||
do
|
|
||||||
cp "shellcheck" "deploy/shellcheck-$tag.linux-aarch64"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
build_armv6hf() {
|
|
||||||
# Linux armv6hf static executable
|
|
||||||
docker run -v "$PWD:/mnt" koalaman/armv6hf-builder -c 'compile-shellcheck'
|
|
||||||
for tag in $TAGS
|
|
||||||
do
|
|
||||||
cp "shellcheck" "deploy/shellcheck-$tag.linux-armv6hf";
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
build_windows() {
|
|
||||||
# Windows .exe
|
|
||||||
docker run -v "$PWD:/appdata" koalaman/winghc cuib
|
|
||||||
for tag in $TAGS
|
|
||||||
do
|
|
||||||
cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe";
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
build_osx() {
|
|
||||||
# Darwin x86_64 executable
|
|
||||||
brew update
|
|
||||||
brew install cabal-install pandoc gnu-tar
|
|
||||||
sudo ln -sf /usr/local/bin/gsha512sum /usr/local/bin/sha512sum
|
|
||||||
sudo ln -sf /usr/local/bin/gtar /usr/local/bin/tar
|
|
||||||
export PATH="/usr/local/bin:$PATH"
|
|
||||||
|
|
||||||
cabal update
|
|
||||||
cabal install --dependencies-only
|
|
||||||
cabal build shellcheck
|
|
||||||
|
|
||||||
# Cabal 3 no longer has a predictable output path
|
|
||||||
path="$(find . -name 'shellcheck' -type f -perm +111)"
|
|
||||||
[[ -e "$path" ]]
|
|
||||||
|
|
||||||
for tag in $TAGS
|
|
||||||
do
|
|
||||||
cp "$path" "deploy/shellcheck-$tag.darwin-x86_64";
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
4
.github/ISSUE_TEMPLATE.md
vendored
4
.github/ISSUE_TEMPLATE.md
vendored
|
@ -2,10 +2,10 @@
|
||||||
- Rule Id (if any, e.g. SC1000):
|
- Rule Id (if any, e.g. SC1000):
|
||||||
- My shellcheck version (`shellcheck --version` or "online"):
|
- My shellcheck version (`shellcheck --version` or "online"):
|
||||||
- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
|
- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
|
||||||
- [ ] I tried on shellcheck.net and verified that this is still a problem on the latest commit
|
- [ ] I tried on https://www.shellcheck.net/ and verified that this is still a problem on the latest commit
|
||||||
|
|
||||||
#### For new checks and feature suggestions
|
#### For new checks and feature suggestions
|
||||||
- [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this
|
- [ ] https://www.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
|
- [ ] I searched through https://github.com/koalaman/shellcheck/issues and didn't find anything related
|
||||||
|
|
||||||
|
|
||||||
|
|
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
159
.github/workflows/build.yml
vendored
Normal file
159
.github/workflows/build.yml
vendored
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
name: Build ShellCheck
|
||||||
|
|
||||||
|
# Run this workflow every time a new commit pushed to your repository
|
||||||
|
on: push
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
package_source:
|
||||||
|
name: Package Source Code
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-mark manual ghc # Don't bother installing ghc just to tar up source
|
||||||
|
sudo apt-get install cabal-install
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Deduce tags
|
||||||
|
run: |
|
||||||
|
mkdir source
|
||||||
|
echo "latest" > source/tags
|
||||||
|
if tag=$(git describe --exact-match --tags)
|
||||||
|
then
|
||||||
|
echo "stable" >> source/tags
|
||||||
|
echo "$tag" >> source/tags
|
||||||
|
fi
|
||||||
|
cat source/tags
|
||||||
|
|
||||||
|
- name: Package Source
|
||||||
|
run: |
|
||||||
|
grep "stable" source/tags || ./setgitversion
|
||||||
|
cabal sdist
|
||||||
|
mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: source
|
||||||
|
path: source/
|
||||||
|
|
||||||
|
run_tests:
|
||||||
|
name: Run tests
|
||||||
|
needs: package_source
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update && sudo apt-get install ghc cabal-install
|
||||||
|
cabal update
|
||||||
|
|
||||||
|
- name: Unpack source
|
||||||
|
run: |
|
||||||
|
cd source
|
||||||
|
tar xvf source.tar.gz --strip-components=1
|
||||||
|
|
||||||
|
- name: Build and run tests
|
||||||
|
run: |
|
||||||
|
cd source
|
||||||
|
cabal test
|
||||||
|
|
||||||
|
build_source:
|
||||||
|
name: Build
|
||||||
|
needs: package_source
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
build: [linux.x86_64, linux.aarch64, linux.armv6hf, linux.riscv64, darwin.x86_64, darwin.aarch64, windows.x86_64]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
|
||||||
|
- name: Build source
|
||||||
|
run: |
|
||||||
|
mkdir -p bin
|
||||||
|
mkdir -p bin/${{matrix.build}}
|
||||||
|
( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} )
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{matrix.build}}.bin
|
||||||
|
path: bin/
|
||||||
|
|
||||||
|
package_binary:
|
||||||
|
name: Package Binaries
|
||||||
|
needs: build_source
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
|
||||||
|
- name: Work around GitHub permissions bug
|
||||||
|
run: chmod +x *.bin/*/shellcheck*
|
||||||
|
|
||||||
|
- name: Package binaries
|
||||||
|
run: |
|
||||||
|
export TAGS="$(cat source/tags)"
|
||||||
|
mkdir -p deploy
|
||||||
|
cp -r *.bin/* deploy
|
||||||
|
cd deploy
|
||||||
|
../.prepare_deploy
|
||||||
|
rm -rf */ README* LICENSE*
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deploy
|
||||||
|
path: deploy/
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: Deploy binaries
|
||||||
|
needs: package_binary
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: Deploy
|
||||||
|
steps:
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install hub
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
|
||||||
|
- name: Upload to GitHub
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
export TAGS="$(cat source/tags)"
|
||||||
|
./.github_deploy
|
||||||
|
|
||||||
|
- name: Waiting for GitHub to replicate uploaded releases
|
||||||
|
run: |
|
||||||
|
sleep 300
|
||||||
|
|
||||||
|
- name: Upload to Docker Hub
|
||||||
|
env:
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
DOCKER_EMAIL: ${{ secrets.DOCKER_EMAIL }}
|
||||||
|
DOCKER_BASE: ${{ secrets.DOCKER_USERNAME }}/shellcheck
|
||||||
|
run: |
|
||||||
|
export TAGS="$(cat source/tags)"
|
||||||
|
( source ./.multi_arch_docker && set -eux && multi_arch_docker::main )
|
|
@ -2,39 +2,10 @@
|
||||||
set -x
|
set -x
|
||||||
shopt -s extglob
|
shopt -s extglob
|
||||||
|
|
||||||
if [[ "$TRAVIS_SECURE_ENV_VARS" != "true" ]]
|
|
||||||
then
|
|
||||||
echo >&2 "Missing TRAVIS_SECURE_ENV_VARS. Skipping GitHub deployment."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
install_deps() {
|
|
||||||
version="2.7.0" # 2.14.1 fails to overwrite duplicates
|
|
||||||
case "$(uname)" in
|
|
||||||
Linux)
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install curl
|
|
||||||
curl -L "https://github.com/github/hub/releases/download/v$version/hub-linux-amd64-$version.tgz" | tar xvz --strip-components=1 "hub-linux-amd64-$version/bin/hub"
|
|
||||||
;;
|
|
||||||
Darwin)
|
|
||||||
curl -L "https://github.com/github/hub/releases/download/v$version/hub-darwin-amd64-$version.tgz" | tar xvz --strip-components=1 "hub-darwin-amd64-$version/bin/hub"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown: $(uname)"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
hub_path="$PWD/bin/hub"
|
|
||||||
hub() {
|
|
||||||
"$hub_path" "$@"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
install_deps
|
|
||||||
|
|
||||||
export EDITOR="touch"
|
export EDITOR="touch"
|
||||||
|
|
||||||
# Sanity check
|
# Sanity check
|
||||||
|
gh --version || exit 1
|
||||||
hub release show latest || exit 1
|
hub release show latest || exit 1
|
||||||
|
|
||||||
for tag in $TAGS
|
for tag in $TAGS
|
||||||
|
@ -50,8 +21,8 @@ do
|
||||||
for file in deploy/*
|
for file in deploy/*
|
||||||
do
|
do
|
||||||
[[ $file == *.@(xz|gz|zip) ]] || continue
|
[[ $file == *.@(xz|gz|zip) ]] || continue
|
||||||
files+=(-a "$file")
|
[[ $file == *"$tag"* ]] || continue
|
||||||
|
files+=("$file")
|
||||||
done
|
done
|
||||||
hub release edit "${files[@]}" "$tag" || exit 1
|
gh release upload "$tag" "${files[@]}" --clobber || exit 1
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -20,3 +20,4 @@ cabal.config
|
||||||
/parts/
|
/parts/
|
||||||
/prime/
|
/prime/
|
||||||
*.snap
|
*.snap
|
||||||
|
/dist-newstyle/
|
||||||
|
|
|
@ -1,36 +1,12 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# This script builds and deploys multi-architecture docker images from the
|
# This script builds and deploys multi-architecture docker images from the
|
||||||
# binaries previously built and deployed to GCS by the Travis pipeline.
|
# binaries previously built and deployed to GitHub.
|
||||||
|
|
||||||
if [[ "$TRAVIS_SECURE_ENV_VARS" != "true" ]]
|
|
||||||
then
|
|
||||||
echo >&2 "Missing TRAVIS_SECURE_ENV_VARS. Skipping Docker builds."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
function multi_arch_docker::install_docker_buildx() {
|
function multi_arch_docker::install_docker_buildx() {
|
||||||
# Install up-to-date version of docker, with buildx support.
|
|
||||||
local -r docker_apt_repo='https://download.docker.com/linux/ubuntu'
|
|
||||||
curl -fsSL "${docker_apt_repo}/gpg" | sudo apt-key add -
|
|
||||||
local -r os="$(lsb_release -cs)"
|
|
||||||
sudo add-apt-repository "deb [arch=amd64] $docker_apt_repo $os stable"
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce
|
|
||||||
|
|
||||||
# Enable docker daemon experimental support (for 'pull --platform').
|
|
||||||
local -r config='/etc/docker/daemon.json'
|
|
||||||
if [[ -e "$config" ]]; then
|
|
||||||
sudo sed -i -e 's/{/{ "experimental": true, /' "$config"
|
|
||||||
else
|
|
||||||
echo '{ "experimental": true }' | sudo tee "$config"
|
|
||||||
fi
|
|
||||||
sudo systemctl restart docker
|
|
||||||
|
|
||||||
# Install QEMU multi-architecture support for docker buildx.
|
# Install QEMU multi-architecture support for docker buildx.
|
||||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||||
|
|
||||||
# Instantiate docker buildx builder with multi-architecture support.
|
# Instantiate docker buildx builder with multi-architecture support.
|
||||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
|
||||||
docker buildx create --name mybuilder
|
docker buildx create --name mybuilder
|
||||||
docker buildx use mybuilder
|
docker buildx use mybuilder
|
||||||
# Start up buildx and verify that all is OK.
|
# Start up buildx and verify that all is OK.
|
||||||
|
@ -104,10 +80,10 @@ function multi_arch_docker::main() {
|
||||||
export DOCKER_PLATFORMS='linux/amd64'
|
export DOCKER_PLATFORMS='linux/amd64'
|
||||||
DOCKER_PLATFORMS+=' linux/arm64'
|
DOCKER_PLATFORMS+=' linux/arm64'
|
||||||
DOCKER_PLATFORMS+=' linux/arm/v6'
|
DOCKER_PLATFORMS+=' linux/arm/v6'
|
||||||
|
DOCKER_PLATFORMS+=' linux/riscv64'
|
||||||
|
|
||||||
multi_arch_docker::install_docker_buildx
|
multi_arch_docker::install_docker_buildx
|
||||||
multi_arch_docker::login_to_docker_hub
|
multi_arch_docker::login_to_docker_hub
|
||||||
multi_arch_docker::build_and_push_all
|
multi_arch_docker::build_and_push_all
|
||||||
set +x
|
|
||||||
multi_arch_docker::test_all
|
multi_arch_docker::test_all
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# This script packages up Travis compiled binaries
|
# This script packages up compiled binaries
|
||||||
set -ex
|
set -ex
|
||||||
shopt -s nullglob
|
shopt -s nullglob extglob
|
||||||
cd deploy
|
|
||||||
|
ls -l
|
||||||
|
|
||||||
cp ../LICENSE LICENSE.txt
|
cp ../LICENSE LICENSE.txt
|
||||||
sed -e $'s/$/\r/' > README.txt << END
|
sed -e $'s/$/\r/' > README.txt << END
|
||||||
|
@ -22,44 +23,32 @@ This binary was compiled on $(date -u).
|
||||||
$(git log -n 3)
|
$(git log -n 3)
|
||||||
END
|
END
|
||||||
|
|
||||||
for file in ./*.exe
|
for dir in */
|
||||||
do
|
do
|
||||||
zip "${file%.*}.zip" README.txt LICENSE.txt "$file"
|
cp LICENSE.txt README.txt "$dir"
|
||||||
done
|
done
|
||||||
|
|
||||||
for file in *.linux-x86_64
|
echo "Tags are $TAGS"
|
||||||
|
|
||||||
|
for tag in $TAGS
|
||||||
do
|
do
|
||||||
base="${file%.*}"
|
|
||||||
cp "$file" "shellcheck"
|
for dir in windows.*/
|
||||||
tar -cJf "$base.linux.x86_64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
|
do
|
||||||
rm "shellcheck"
|
( cd "$dir" && zip "../shellcheck-$tag.zip" * )
|
||||||
done
|
done
|
||||||
|
|
||||||
for file in *.linux-aarch64
|
for dir in {linux,darwin}.*/
|
||||||
do
|
do
|
||||||
base="${file%.*}"
|
base="${dir%/}"
|
||||||
cp "$file" "shellcheck"
|
( cd "$dir" && tar -cJf "../shellcheck-$tag.$base.tar.xz" --transform="s:^:shellcheck-$tag/:" * )
|
||||||
tar -cJf "$base.linux.aarch64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
|
|
||||||
rm "shellcheck"
|
|
||||||
done
|
done
|
||||||
|
|
||||||
for file in *.linux-armv6hf
|
|
||||||
do
|
|
||||||
base="${file%.*}"
|
|
||||||
cp "$file" "shellcheck"
|
|
||||||
tar -cJf "$base.linux.armv6hf.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
|
|
||||||
rm "shellcheck"
|
|
||||||
done
|
|
||||||
|
|
||||||
for file in *.darwin-x86_64
|
|
||||||
do
|
|
||||||
base="${file%.*}"
|
|
||||||
cp "$file" "shellcheck"
|
|
||||||
tar -cJf "$base.darwin.x86_64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
|
|
||||||
rm "shellcheck"
|
|
||||||
done
|
done
|
||||||
|
|
||||||
for file in ./*
|
for file in ./*
|
||||||
do
|
do
|
||||||
|
[[ -f "$file" ]] || continue
|
||||||
sha512sum "$file" > "$file.sha512sum"
|
sha512sum "$file" > "$file.sha512sum"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
ls -l
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
# In 2015, cabal-install had a http bug triggered when proxies didn't keep
|
|
||||||
# the connection open. This version made it into Ubuntu Xenial as used by
|
|
||||||
# Snapcraft. In June 2018, Snapcraft's proxy started triggering this bug.
|
|
||||||
#
|
|
||||||
# https://bugs.launchpad.net/launchpad-buildd/+bug/1797809
|
|
||||||
#
|
|
||||||
# Workaround: add more proxy
|
|
||||||
|
|
||||||
visible_hostname localhost
|
|
||||||
http_port 8888
|
|
||||||
cache_peer 10.10.10.1 parent 8222 0 no-query default
|
|
||||||
cache_peer_domain localhost !.internal
|
|
||||||
http_access allow all
|
|
||||||
|
|
64
.travis.yml
64
.travis.yml
|
@ -1,64 +0,0 @@
|
||||||
language: shell
|
|
||||||
os: linux
|
|
||||||
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
include:
|
|
||||||
- stage: Build
|
|
||||||
|
|
||||||
# This must weirdly not have a dash, otherwise an empty job is created
|
|
||||||
env: BUILD=linux
|
|
||||||
- env: BUILD=windows
|
|
||||||
- env: BUILD=armv6hf
|
|
||||||
- env: BUILD=aarch64
|
|
||||||
- env: BUILD=osx
|
|
||||||
os: osx
|
|
||||||
|
|
||||||
- stage: Deploy docker image
|
|
||||||
# Deploy only for pushes to master branch, not other branches, not PRs.
|
|
||||||
if: type = push
|
|
||||||
script:
|
|
||||||
- source ./.multi_arch_docker
|
|
||||||
- set -ex; multi_arch_docker::main; set +x
|
|
||||||
|
|
||||||
# This is in global context and runs for every stage that doesn't override it.
|
|
||||||
before_install: |
|
|
||||||
DOCKER_BASE="$DOCKER_USERNAME/shellcheck"
|
|
||||||
DOCKER_BUILDS=""
|
|
||||||
export TAGS=""
|
|
||||||
test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
|
|
||||||
test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
|
|
||||||
echo "Tags are $TAGS"
|
|
||||||
|
|
||||||
# This is in global context and runs for every stage that doesn't override it.
|
|
||||||
script:
|
|
||||||
- mkdir -p deploy
|
|
||||||
- source ./.compile_binaries
|
|
||||||
- ./striptests
|
|
||||||
- set -ex; build_"$BUILD"; set +x;
|
|
||||||
- ./.prepare_deploy
|
|
||||||
- ./.github_deploy
|
|
||||||
|
|
||||||
# This is in global context and runs for every stage that doesn't override it.
|
|
||||||
after_failure: |
|
|
||||||
id
|
|
||||||
pwd
|
|
||||||
df -h
|
|
||||||
find . -name '*.log' -type f -exec grep "" /dev/null {} +
|
|
||||||
find . -ls
|
|
||||||
|
|
||||||
# This is in global context and runs for every stage that doesn't override it.
|
|
||||||
deploy:
|
|
||||||
provider: gcs
|
|
||||||
skip_cleanup: true
|
|
||||||
access_key_id: GOOG7MDN7WEH6IIGBDCA
|
|
||||||
secret_access_key:
|
|
||||||
secure: Bcx2cT0/E2ikj7sdamVq52xlLZF9dz9ojGPtoKfPyQhkkZa+McVI4xgUSuyyoSxyKj77sofx2y8m6PJYYumT4g5hREV1tfeUkl0J2DQFMbGDYEt7kxVkXCxojNvhHwTzLFv0ezstrxWWxQm81BfQQ4U9lggRXtndAP4czZnOeHPINPSiue1QNwRAEw05r5UoIUJXy/5xyUrjIxn381pAs+gJqP2COeN9kTKYH53nS/AAws29RprfZFnPlo7xxWmcjRcdS5KPdGXI/c6tQp5zl2iTh510VC1PN2w1Wvnn/oNWhiNdqPyVDsojIX5+sS3nejzJA+KFMxXSBlyXIY3wPpS/MdscU79X6Q5f9ivsFfsm7gNBmxHUPNn0HAvU4ROT/CCE9j6jSbs5PC7QBo3CK4++jxAwE/pd9HUc2rs3k0ofx3rgveJ7txpy5yPKfwIIBi98kVKlC4w7dLvNTOfjW1Imt2yH87XTfsE0UIG9st1WII6s4l/WgBx2GuwKdt6+3QUYiAlCFckkxWi+fAvpHZUEL43Qxub5fN+ZV7Zib1n7opchH4QKGBb6/y0WaDCmtCfu0lppoe/TH6saOTjDFj67NJSElK6ZDxGZ3uw4R+ret2gm6WRKT2Oeub8J33VzSa7VkmFpMPrAAfPa9N1Z4ewBLoTmvxSg2A0dDrCdJio=
|
|
||||||
bucket: shellcheck-private
|
|
||||||
local_dir: deploy
|
|
||||||
on:
|
|
||||||
repo: koalaman/shellcheck
|
|
||||||
condition: $TRAVIS_BUILD_STAGE_NAME = Build
|
|
||||||
all_branches: true
|
|
136
CHANGELOG.md
136
CHANGELOG.md
|
@ -1,3 +1,135 @@
|
||||||
|
## Git
|
||||||
|
### Added
|
||||||
|
- SC2327/SC2328: Warn about capturing the output of redirected commands.
|
||||||
|
- SC2329: Warn when (non-escaping) functions are never invoked.
|
||||||
|
- SC2330: Warn about unsupported glob matches with [[ .. ]] in BusyBox.
|
||||||
|
- SC2331: Suggest using standard -e instead of unary -a in tests.
|
||||||
|
- SC2332: Warn about `[ ! -o opt ]` being unconditionally true in Bash.
|
||||||
|
- SC3062: Warn about bashism `[ -o opt ]`.
|
||||||
|
- Precompiled binaries for Linux riscv64 (linux.riscv64)
|
||||||
|
### Changed
|
||||||
|
- SC2002 about Useless Use Of Cat is now disabled by default. It can be
|
||||||
|
re-enabled with `--enable=useless-use-of-cat` or equivalent directive.
|
||||||
|
- SC2015 about `A && B || C` no longer triggers when B is a test command.
|
||||||
|
- SC3012: Do not warn about `\<` and `\>` in test/[] as specified in POSIX.1-2024
|
||||||
|
### Fixed
|
||||||
|
- SC2218 about function use-before-define is now more accurate.
|
||||||
|
- SC2317 about unreachable commands is now less spammy for nested ones.
|
||||||
|
- SC2292, optional suggestion for [[ ]], now triggers for Busybox.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- SC3013: removed since the operators `-ot/-nt/-ef` are specified in POSIX.1-2024
|
||||||
|
|
||||||
|
## v0.10.0 - 2024-03-07
|
||||||
|
### Added
|
||||||
|
- Precompiled binaries for macOS ARM64 (darwin.aarch64)
|
||||||
|
- Added support for busybox sh
|
||||||
|
- Added flag --rcfile to specify an rc file by name.
|
||||||
|
- Added `extended-analysis=true` directive to enable/disable dataflow analysis
|
||||||
|
(with a corresponding --extended-analysis flag).
|
||||||
|
- SC2324: Warn when x+=1 appends instead of increments
|
||||||
|
- SC2325: Warn about multiple `!`s in dash/sh.
|
||||||
|
- SC2326: Warn about `foo | ! bar` in bash/dash/sh.
|
||||||
|
- SC3012: Warn about lexicographic-compare bashism in test like in [ ]
|
||||||
|
- SC3013: Warn bashism `test _ -op/-nt/-ef _` like in [ ]
|
||||||
|
- SC3014: Warn bashism `test _ == _` like in [ ]
|
||||||
|
- SC3015: Warn bashism `test _ =~ _` like in [ ]
|
||||||
|
- SC3016: Warn bashism `test -v _` like in [ ]
|
||||||
|
- SC3017: Warn bashism `test -a _` like in [ ]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- source statements with here docs now work correctly
|
||||||
|
- "(Array.!): undefined array element" error should no longer occur
|
||||||
|
|
||||||
|
|
||||||
|
## v0.9.0 - 2022-12-12
|
||||||
|
### Added
|
||||||
|
- SC2316: Warn about 'local readonly foo' and similar (thanks, patrickxia!)
|
||||||
|
- SC2317: Warn about unreachable commands
|
||||||
|
- SC2318: Warn about backreferences in 'declare x=1 y=$x'
|
||||||
|
- SC2319/SC2320: Warn when $? refers to echo/printf/[ ]/[[ ]]/test
|
||||||
|
- SC2321: Suggest removing $((..)) in array[$((idx))]=val
|
||||||
|
- SC2322: Suggest collapsing double parentheses in arithmetic contexts
|
||||||
|
- SC2323: Suggest removing wrapping parentheses in a[(x+1)]=val
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- SC2086: Now uses DFA to make more accurate predictions about values
|
||||||
|
- SC2086: No longer warns about values declared as integer with declare -i
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- ShellCheck now has a Data Flow Analysis engine to make smarter decisions
|
||||||
|
based on control flow rather than just syntax. Existing checks will
|
||||||
|
gradually start using it, which may cause them to trigger differently
|
||||||
|
(but more accurately).
|
||||||
|
- Values in directives/shellcheckrc can now be quoted with '' or ""
|
||||||
|
|
||||||
|
|
||||||
|
## v0.8.0 - 2021-11-06
|
||||||
|
### Added
|
||||||
|
- `disable=all` now conveniently disables all warnings
|
||||||
|
- `external-sources=true` directive can be added to .shellcheckrc to make
|
||||||
|
shellcheck behave as if `-x` was specified.
|
||||||
|
- Optional `check-extra-masked-returns` for pointing out commands with
|
||||||
|
suppressed exit codes (SC2312).
|
||||||
|
- Optional `require-double-brackets` for recommending \[\[ ]] (SC2292).
|
||||||
|
- SC2286-SC2288: Warn when command name ends in a symbol like `/.)'"`
|
||||||
|
- SC2289: Warn when command name contains tabs or linefeeds
|
||||||
|
- SC2291: Warn about repeated unquoted spaces between words in echo
|
||||||
|
- SC2292: Suggest [[ over [ in Bash/Ksh scripts (optional)
|
||||||
|
- SC2293/SC2294: Warn when calling `eval` with arrays
|
||||||
|
- SC2295: Warn about "${x#$y}" treating $y as a pattern when not quoted
|
||||||
|
- SC2296-SC2301: Improved warnings for bad parameter expansions
|
||||||
|
- SC2302/SC2303: Warn about loops over array values when using them as keys
|
||||||
|
- SC2304-SC2306: Warn about unquoted globs in expr arguments
|
||||||
|
- SC2307: Warn about insufficient number of arguments to expr
|
||||||
|
- SC2308: Suggest other approaches for non-standard expr extensions
|
||||||
|
- SC2313: Warn about `read` with unquoted, array indexed variable
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- SC2102 about repetitions in ranges no longer triggers on [[ -v arr[xx] ]]
|
||||||
|
- SC2155 now recognizes `typeset` and local read-only `declare` statements
|
||||||
|
- SC2181 now tries to avoid triggering for error handling functions
|
||||||
|
- SC2290: Warn about misused = in declare & co, which were not caught by SC2270+
|
||||||
|
- The flag --color=auto no longer outputs color when TERM is "dumb" or unset
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- SC2048: Warning about $\* now also applies to ${array[\*]}
|
||||||
|
- SC2181 now only triggers on single condition tests like `[ $? = 0 ]`.
|
||||||
|
- Quote warnings are now emitted for declaration utilities in sh
|
||||||
|
- Leading `_` can now be used to suppress warnings about unused variables
|
||||||
|
- TTY output now includes warning level in text as well as color
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- SC1004: Literal backslash+linefeed in '' was found to be usually correct
|
||||||
|
|
||||||
|
|
||||||
|
## v0.7.2 - 2021-04-19
|
||||||
|
### Added
|
||||||
|
- `disable` directives can now be a range, e.g. `disable=SC3000-SC4000`
|
||||||
|
- SC1143: Warn about line continuations in comments
|
||||||
|
- SC2259/SC2260: Warn when redirections override pipes
|
||||||
|
- SC2261: Warn about multiple competing redirections
|
||||||
|
- SC2262/SC2263: Warn about aliases declared and used in the same parsing unit
|
||||||
|
- SC2264: Warn about wrapper functions that blatantly recurse
|
||||||
|
- SC2265/SC2266: Warn when using & or | with test statements
|
||||||
|
- SC2267: Warn when using xargs -i instead of -I
|
||||||
|
- SC2268: Warn about unnecessary x-comparisons like `[ x$var = xval ]`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- SC1072/SC1073 now respond to disable annotations, though ignoring parse errors
|
||||||
|
is still purely cosmetic and does not allow ShellCheck to continue.
|
||||||
|
- Improved error reporting for trailing tokens after ]/]] and compound commands
|
||||||
|
- `#!/usr/bin/env -S shell` is now handled correctly
|
||||||
|
- Here docs with \r are now parsed correctly and give better warnings
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Assignments are now parsed to spec, without leniency for leading $ or spaces
|
||||||
|
- POSIX/dash unsupported feature warnings now have individual SC3xxx codes
|
||||||
|
- SC1090: A leading `$x/` or `$(x)/` is now treated as `./` when locating files
|
||||||
|
- SC2154: Variables appearing in -z/-n tests are no longer considered unassigned
|
||||||
|
- SC2270-SC2285: Improved warnings about misused `=`, e.g. `${var}=42`
|
||||||
|
|
||||||
|
|
||||||
## v0.7.1 - 2020-04-04
|
## v0.7.1 - 2020-04-04
|
||||||
### Fixed
|
### Fixed
|
||||||
- `-f diff` no longer claims that it found more issues when it didn't
|
- `-f diff` no longer claims that it found more issues when it didn't
|
||||||
|
@ -138,7 +270,7 @@
|
||||||
- SC2204/SC2205: Warn about `( -z foo )` and `( foo -eq bar )`
|
- SC2204/SC2205: Warn about `( -z foo )` and `( foo -eq bar )`
|
||||||
- SC2200/SC2201: Warn about brace expansion in [/[[
|
- SC2200/SC2201: Warn about brace expansion in [/[[
|
||||||
- SC2198/SC2199: Warn about arrays in [/[[
|
- SC2198/SC2199: Warn about arrays in [/[[
|
||||||
- SC2196/SC2197: Warn about deprected egrep/fgrep
|
- SC2196/SC2197: Warn about deprecated egrep/fgrep
|
||||||
- SC2195: Warn about unmatchable case branches
|
- SC2195: Warn about unmatchable case branches
|
||||||
- SC2194: Warn about constant 'case' statements
|
- SC2194: Warn about constant 'case' statements
|
||||||
- SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables
|
- SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables
|
||||||
|
@ -155,7 +287,7 @@
|
||||||
### Fixed
|
### Fixed
|
||||||
- `-c` no longer suggested when using `grep -o | wc`
|
- `-c` no longer suggested when using `grep -o | wc`
|
||||||
- Comments and whitespace are now allowed before filewide directives
|
- Comments and whitespace are now allowed before filewide directives
|
||||||
- Here doc delimters with esoteric quoting like `foo""` are now handled
|
- Here doc delimiters with esoteric quoting like `foo""` are now handled
|
||||||
- SC2095 about `ssh` in while read loops is now suppressed when using `-n`
|
- SC2095 about `ssh` in while read loops is now suppressed when using `-n`
|
||||||
- `%(%Y%M%D)T` now recognized as a single formatter in `printf` checks
|
- `%(%Y%M%D)T` now recognized as a single formatter in `printf` checks
|
||||||
- `grep -F` now suppresses regex related suggestions
|
- `grep -F` now suppresses regex related suggestions
|
||||||
|
|
29
Dockerfile
29
Dockerfile
|
@ -1,29 +0,0 @@
|
||||||
# Build-only image
|
|
||||||
FROM ubuntu:18.04 AS build
|
|
||||||
USER root
|
|
||||||
WORKDIR /opt/shellCheck
|
|
||||||
|
|
||||||
# Install OS deps
|
|
||||||
RUN apt-get update && apt-get install -y ghc cabal-install
|
|
||||||
|
|
||||||
# Install Haskell deps
|
|
||||||
# (This is a separate copy/run so that source changes don't require rebuilding)
|
|
||||||
COPY ShellCheck.cabal ./
|
|
||||||
RUN cabal update && cabal install --dependencies-only --ghc-options="-optlo-Os -split-sections"
|
|
||||||
|
|
||||||
# Copy source and build it
|
|
||||||
COPY LICENSE shellcheck.hs ./
|
|
||||||
COPY src src
|
|
||||||
RUN cabal build Paths_ShellCheck && \
|
|
||||||
ghc -optl-static -optl-pthread -isrc -idist/build/autogen --make shellcheck -split-sections -optc-Wl,--gc-sections -optlo-Os && \
|
|
||||||
strip --strip-all shellcheck
|
|
||||||
|
|
||||||
RUN mkdir -p /out/bin && \
|
|
||||||
cp shellcheck /out/bin/
|
|
||||||
|
|
||||||
# Resulting ShellCheck image
|
|
||||||
FROM scratch
|
|
||||||
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
|
|
||||||
WORKDIR /mnt
|
|
||||||
COPY --from=build /out /
|
|
||||||
ENTRYPOINT ["/bin/shellcheck"]
|
|
12
LICENSE
12
LICENSE
|
@ -1,13 +1,3 @@
|
||||||
Employer mandated disclaimer:
|
|
||||||
|
|
||||||
I am providing code in the repository to you under an open source license.
|
|
||||||
Because this is my personal repository, the license you receive to my code is
|
|
||||||
from me and other individual contributors, and not my employer (Facebook).
|
|
||||||
|
|
||||||
- Vidar "koala_man" Holen
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
@ -681,4 +671,4 @@ into proprietary programs. If your program is a subroutine library, you
|
||||||
may consider it more useful to permit linking proprietary applications with
|
may consider it more useful to permit linking proprietary applications with
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
Public License instead of this License. But first, please read
|
Public License instead of this License. But first, please read
|
||||||
<https://www.gnu.org/philosophy/why-not-lgpl.html>.
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
|
|
80
README.md
80
README.md
|
@ -1,4 +1,5 @@
|
||||||
[](https://travis-ci.org/koalaman/shellcheck)
|
[](https://github.com/koalaman/shellcheck/actions/workflows/build.yml)
|
||||||
|
|
||||||
|
|
||||||
# ShellCheck - A shell script static analysis tool
|
# ShellCheck - A shell script static analysis tool
|
||||||
|
|
||||||
|
@ -76,7 +77,7 @@ You can see ShellCheck suggestions directly in a variety of editors.
|
||||||
|
|
||||||
* Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck).
|
* Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck).
|
||||||
|
|
||||||
* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
|
* Pulsar Edit (former Atom), through [linter-shellcheck-pulsar](https://github.com/pulsar-cooperative/linter-shellcheck-pulsar).
|
||||||
|
|
||||||
* VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck).
|
* VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck).
|
||||||
|
|
||||||
|
@ -109,11 +110,11 @@ Services and platforms that have ShellCheck pre-installed and ready to use:
|
||||||
* [Codacy](https://www.codacy.com/)
|
* [Codacy](https://www.codacy.com/)
|
||||||
* [Code Climate](https://codeclimate.com/)
|
* [Code Climate](https://codeclimate.com/)
|
||||||
* [Code Factor](https://www.codefactor.io/)
|
* [Code Factor](https://www.codefactor.io/)
|
||||||
|
* [Codety](https://www.codety.io/) via the [Codety Scanner](https://github.com/codetyio/codety-scanner)
|
||||||
|
* [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck)
|
||||||
* [Github](https://github.com/features/actions) (only Linux)
|
* [Github](https://github.com/features/actions) (only Linux)
|
||||||
|
* [Trunk Check](https://trunk.io/products/check) (universal linter; [allows you to explicitly version your shellcheck install](https://github.com/trunk-io/plugins/blob/bcbb361dcdbe4619af51ea7db474d7fb87540d20/.trunk/trunk.yaml#L32)) via the [shellcheck plugin](https://github.com/trunk-io/plugins/blob/main/linters/shellcheck/plugin.yaml)
|
||||||
Services and platforms with third party plugins:
|
* [CodeRabbit](https://coderabbit.ai/)
|
||||||
|
|
||||||
* [SonarQube](https://www.sonarqube.org/) through [sonar-shellcheck-plugin](https://github.com/emerald-squad/sonar-shellcheck-plugin)
|
|
||||||
|
|
||||||
Most other services, including [GitLab](https://about.gitlab.com/), let you install
|
Most other services, including [GitLab](https://about.gitlab.com/), let you install
|
||||||
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
|
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
|
||||||
|
@ -142,13 +143,13 @@ On systems with Stack (installs to `~/.local/bin`):
|
||||||
|
|
||||||
On Debian based distros:
|
On Debian based distros:
|
||||||
|
|
||||||
apt-get install shellcheck
|
sudo apt install shellcheck
|
||||||
|
|
||||||
On Arch Linux based distros:
|
On Arch Linux based distros:
|
||||||
|
|
||||||
pacman -S shellcheck
|
pacman -S shellcheck
|
||||||
|
|
||||||
or get the dependency free [shellcheck-static](https://aur.archlinux.org/packages/shellcheck-static/) from the AUR.
|
or get the dependency free [shellcheck-bin](https://aur.archlinux.org/packages/shellcheck-bin/) from the AUR.
|
||||||
|
|
||||||
On Gentoo based distros:
|
On Gentoo based distros:
|
||||||
|
|
||||||
|
@ -156,8 +157,8 @@ On Gentoo based distros:
|
||||||
|
|
||||||
On EPEL based distros:
|
On EPEL based distros:
|
||||||
|
|
||||||
yum -y install epel-release
|
sudo yum -y install epel-release
|
||||||
yum install ShellCheck
|
sudo yum install ShellCheck
|
||||||
|
|
||||||
On Fedora based distros:
|
On Fedora based distros:
|
||||||
|
|
||||||
|
@ -167,10 +168,14 @@ On FreeBSD:
|
||||||
|
|
||||||
pkg install hs-ShellCheck
|
pkg install hs-ShellCheck
|
||||||
|
|
||||||
On OS X with homebrew:
|
On macOS (OS X) with Homebrew:
|
||||||
|
|
||||||
brew install shellcheck
|
brew install shellcheck
|
||||||
|
|
||||||
|
Or with MacPorts:
|
||||||
|
|
||||||
|
sudo port install shellcheck
|
||||||
|
|
||||||
On OpenBSD:
|
On OpenBSD:
|
||||||
|
|
||||||
pkg_add shellcheck
|
pkg_add shellcheck
|
||||||
|
@ -191,12 +196,22 @@ On Windows (via [chocolatey](https://chocolatey.org/packages/shellcheck)):
|
||||||
C:\> choco install shellcheck
|
C:\> choco install shellcheck
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or Windows (via [winget](https://github.com/microsoft/winget-pkgs)):
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
C:\> winget install --id koalaman.shellcheck
|
||||||
|
```
|
||||||
|
|
||||||
Or Windows (via [scoop](http://scoop.sh)):
|
Or Windows (via [scoop](http://scoop.sh)):
|
||||||
|
|
||||||
```cmd
|
```cmd
|
||||||
C:\> scoop install shellcheck
|
C:\> scoop install shellcheck
|
||||||
```
|
```
|
||||||
|
|
||||||
|
From [conda-forge](https://anaconda.org/conda-forge/shellcheck):
|
||||||
|
|
||||||
|
conda install -c conda-forge shellcheck
|
||||||
|
|
||||||
From Snap Store:
|
From Snap Store:
|
||||||
|
|
||||||
snap install --channel=edge shellcheck
|
snap install --channel=edge shellcheck
|
||||||
|
@ -215,17 +230,26 @@ Using the [nix package manager](https://nixos.org/nix):
|
||||||
nix-env -iA nixpkgs.shellcheck
|
nix-env -iA nixpkgs.shellcheck
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Using the [Flox package manager](https://flox.dev/)
|
||||||
|
```sh
|
||||||
|
flox install shellcheck
|
||||||
|
```
|
||||||
|
|
||||||
Alternatively, you can download pre-compiled binaries for the latest release here:
|
Alternatively, you can download pre-compiled binaries for the latest release here:
|
||||||
|
|
||||||
* [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
|
* [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
|
||||||
* [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
|
* [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
|
||||||
* [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)
|
* [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)
|
||||||
* [MacOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz)
|
* [macOS, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.aarch64.tar.xz)
|
||||||
|
* [macOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz)
|
||||||
* [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip)
|
* [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip)
|
||||||
|
|
||||||
or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
|
or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
|
||||||
(including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds).
|
(including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds).
|
||||||
|
|
||||||
|
There are currently no official binaries for Apple Silicon, but third party builds are available via
|
||||||
|
[ShellCheck for Visual Studio Code](https://github.com/vscode-shellcheck/shellcheck-binaries/releases).
|
||||||
|
|
||||||
Distro packages already come with a `man` page. If you are building from source, it can be installed with:
|
Distro packages already come with a `man` page. If you are building from source, it can be installed with:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
|
@ -233,6 +257,19 @@ pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1
|
||||||
sudo mv shellcheck.1 /usr/share/man/man1
|
sudo mv shellcheck.1 /usr/share/man/man1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### pre-commit
|
||||||
|
|
||||||
|
To run ShellCheck via [pre-commit](https://pre-commit.com/), add the hook to your `.pre-commit-config.yaml`:
|
||||||
|
|
||||||
|
```
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/koalaman/shellcheck-precommit
|
||||||
|
rev: v0.7.2
|
||||||
|
hooks:
|
||||||
|
- id: shellcheck
|
||||||
|
# args: ["--severity=warning"] # Optionally only show errors and warnings
|
||||||
|
```
|
||||||
|
|
||||||
### Travis CI
|
### Travis CI
|
||||||
|
|
||||||
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
|
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
|
||||||
|
@ -264,7 +301,7 @@ This section describes how to build ShellCheck from a source directory. ShellChe
|
||||||
|
|
||||||
ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `brew`, `emerge`, `yum`, or `zypper`).
|
ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `brew`, `emerge`, `yum`, or `zypper`).
|
||||||
|
|
||||||
On MacOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source.
|
On macOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source.
|
||||||
|
|
||||||
$ brew install cabal-install
|
$ brew install cabal-install
|
||||||
|
|
||||||
|
@ -280,10 +317,6 @@ Verify that `cabal` is installed and update its dependency list with
|
||||||
|
|
||||||
$ cabal install
|
$ cabal install
|
||||||
|
|
||||||
Or if you intend to run the tests:
|
|
||||||
|
|
||||||
$ cabal install --enable-tests
|
|
||||||
|
|
||||||
This will compile ShellCheck and install it to your `~/.cabal/bin` directory.
|
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`):
|
Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
|
||||||
|
@ -339,6 +372,7 @@ echo 'Don't forget to restart!' # Singlequote closed by apostrophe
|
||||||
echo 'Don\'t try this at home' # Attempting to escape ' in ''
|
echo 'Don\'t try this at home' # Attempting to escape ' in ''
|
||||||
echo 'Path is $PATH' # Variables in single quotes
|
echo 'Path is $PATH' # Variables in single quotes
|
||||||
trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap
|
trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap
|
||||||
|
unset var[i] # Array index treated as glob
|
||||||
```
|
```
|
||||||
|
|
||||||
### Conditionals
|
### Conditionals
|
||||||
|
@ -357,6 +391,7 @@ ShellCheck can recognize many types of incorrect test statements.
|
||||||
[ grep -q foo file ] # Command without $(..)
|
[ grep -q foo file ] # Command without $(..)
|
||||||
[[ "$$file" == *.jpg ]] # Comparisons that can't succeed
|
[[ "$$file" == *.jpg ]] # Comparisons that can't succeed
|
||||||
(( 1 -lt 2 )) # Using test operators in ((..))
|
(( 1 -lt 2 )) # Using test operators in ((..))
|
||||||
|
[ x ] & [ y ] | [ z ] # Accidental backgrounding and piping
|
||||||
```
|
```
|
||||||
|
|
||||||
### Frequently misused commands
|
### Frequently misused commands
|
||||||
|
@ -428,6 +463,8 @@ echo "Hello $name" # Unassigned lowercase variables
|
||||||
cmd | read bar; echo $bar # Assignments in subshells
|
cmd | read bar; echo $bar # Assignments in subshells
|
||||||
cat foo | cp bar # Piping to commands that don't read
|
cat foo | cp bar # Piping to commands that don't read
|
||||||
printf '%s: %s\n' foo # Mismatches in printf argument count
|
printf '%s: %s\n' foo # Mismatches in printf argument count
|
||||||
|
eval "${array[@]}" # Lost word boundaries in array eval
|
||||||
|
for i in "${x[@]}"; do ${x[$i]} # Using array value as key
|
||||||
```
|
```
|
||||||
|
|
||||||
### Robustness
|
### Robustness
|
||||||
|
@ -452,6 +489,7 @@ ShellCheck will warn when using features not supported by the shebang. For examp
|
||||||
echo {1..$n} # Works in ksh, but not bash/dash/sh
|
echo {1..$n} # Works in ksh, but not bash/dash/sh
|
||||||
echo {1..10} # Works in ksh and bash, but not dash/sh
|
echo {1..10} # Works in ksh and bash, but not dash/sh
|
||||||
echo -n 42 # Works in ksh, bash and dash, undefined in sh
|
echo -n 42 # Works in ksh, bash and dash, undefined in sh
|
||||||
|
expr match str regex # Unportable alias for `expr str : regex`
|
||||||
trap 'exit 42' sigint # Unportable signal spec
|
trap 'exit 42' sigint # Unportable signal spec
|
||||||
cmd &> file # Unportable redirection operator
|
cmd &> file # Unportable redirection operator
|
||||||
read foo < /dev/tcp/host/22 # Unportable intercepted files
|
read foo < /dev/tcp/host/22 # Unportable intercepted files
|
||||||
|
@ -472,10 +510,15 @@ rm “file” # Unicode quotes
|
||||||
echo "Hello world" # Carriage return / DOS line endings
|
echo "Hello world" # Carriage return / DOS line endings
|
||||||
echo hello \ # Trailing spaces after \
|
echo hello \ # Trailing spaces after \
|
||||||
var=42 echo $var # Expansion of inlined environment
|
var=42 echo $var # Expansion of inlined environment
|
||||||
#!/bin/bash -x -e # Common shebang errors
|
!# bin/bash -x -e # Common shebang errors
|
||||||
echo $((n/180*100)) # Unnecessary loss of precision
|
echo $((n/180*100)) # Unnecessary loss of precision
|
||||||
ls *[:digit:].txt # Bad character class globs
|
ls *[:digit:].txt # Bad character class globs
|
||||||
sed 's/foo/bar/' file > file # Redirecting to input
|
sed 's/foo/bar/' file > file # Redirecting to input
|
||||||
|
var2=$var2 # Variable assigned to itself
|
||||||
|
[ x$var = xval ] # Antiquated x-comparisons
|
||||||
|
ls() { ls -l "$@"; } # Infinitely recursive wrapper
|
||||||
|
alias ls='ls -l'; ls foo # Alias used before it takes effect
|
||||||
|
for x; do for x; do # Nested loop uses same variable
|
||||||
while getopts "a" f; do case $f in "b") # Unhandled getopts flags
|
while getopts "a" f; do case $f in "b") # Unhandled getopts flags
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -519,4 +562,3 @@ Happy ShellChecking!
|
||||||
|
|
||||||
* The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221).
|
* The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221).
|
||||||
* ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)!
|
* ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)!
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
Name: ShellCheck
|
Name: ShellCheck
|
||||||
Version: 0.7.1
|
Version: 0.10.0
|
||||||
Synopsis: Shell script analysis tool
|
Synopsis: Shell script analysis tool
|
||||||
License: GPL-3
|
License: GPL-3
|
||||||
License-file: LICENSE
|
License-file: LICENSE
|
||||||
|
@ -8,7 +8,7 @@ Author: Vidar Holen
|
||||||
Maintainer: vidar@vidarholen.net
|
Maintainer: vidar@vidarholen.net
|
||||||
Homepage: https://www.shellcheck.net/
|
Homepage: https://www.shellcheck.net/
|
||||||
Build-Type: Simple
|
Build-Type: Simple
|
||||||
Cabal-Version: >= 1.8
|
Cabal-Version: 1.18
|
||||||
Bug-reports: https://github.com/koalaman/shellcheck/issues
|
Bug-reports: https://github.com/koalaman/shellcheck/issues
|
||||||
Description:
|
Description:
|
||||||
The goals of ShellCheck are:
|
The goals of ShellCheck are:
|
||||||
|
@ -22,9 +22,11 @@ Description:
|
||||||
* To point out subtle caveats, corner cases and pitfalls, that may cause an
|
* To point out subtle caveats, corner cases and pitfalls, that may cause an
|
||||||
advanced user's otherwise working script to fail under future circumstances.
|
advanced user's otherwise working script to fail under future circumstances.
|
||||||
|
|
||||||
|
Extra-Doc-Files:
|
||||||
|
README.md
|
||||||
|
CHANGELOG.md
|
||||||
Extra-Source-Files:
|
Extra-Source-Files:
|
||||||
-- documentation
|
-- documentation
|
||||||
README.md
|
|
||||||
shellcheck.1.md
|
shellcheck.1.md
|
||||||
-- A script to build the man page using pandoc
|
-- A script to build the man page using pandoc
|
||||||
manpage
|
manpage
|
||||||
|
@ -43,19 +45,26 @@ library
|
||||||
build-depends:
|
build-depends:
|
||||||
semigroups
|
semigroups
|
||||||
build-depends:
|
build-depends:
|
||||||
aeson,
|
-- The lower bounds are based on GHC 7.10.3
|
||||||
array,
|
-- The upper bounds are based on GHC 9.8.1
|
||||||
|
aeson >= 1.4.0 && < 2.3,
|
||||||
|
array >= 0.5.1 && < 0.6,
|
||||||
base >= 4.8.0.0 && < 5,
|
base >= 4.8.0.0 && < 5,
|
||||||
bytestring,
|
bytestring >= 0.10.6 && < 0.13,
|
||||||
containers >= 0.5,
|
containers >= 0.5.6 && < 0.8,
|
||||||
deepseq >= 1.4.0.0,
|
deepseq >= 1.4.1 && < 1.6,
|
||||||
Diff >= 0.2.0,
|
Diff >= 0.4.0 && < 1.1,
|
||||||
directory >= 1.2.3.0,
|
fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9),
|
||||||
mtl >= 2.2.1,
|
filepath >= 1.4.0 && < 1.6,
|
||||||
filepath,
|
mtl >= 2.2.2 && < 2.4,
|
||||||
parsec,
|
parsec >= 3.1.14 && < 3.2,
|
||||||
regex-tdfa,
|
QuickCheck >= 2.14.2 && < 2.16,
|
||||||
QuickCheck >= 2.7.4,
|
regex-tdfa >= 1.2.0 && < 1.4,
|
||||||
|
transformers >= 0.4.2 && < 0.7,
|
||||||
|
|
||||||
|
-- getXdgDirectory from 1.2.3.0
|
||||||
|
directory >= 1.2.3 && < 1.4,
|
||||||
|
|
||||||
-- When cabal supports it, move this to setup-depends:
|
-- When cabal supports it, move this to setup-depends:
|
||||||
process
|
process
|
||||||
exposed-modules:
|
exposed-modules:
|
||||||
|
@ -64,11 +73,15 @@ library
|
||||||
ShellCheck.Analytics
|
ShellCheck.Analytics
|
||||||
ShellCheck.Analyzer
|
ShellCheck.Analyzer
|
||||||
ShellCheck.AnalyzerLib
|
ShellCheck.AnalyzerLib
|
||||||
|
ShellCheck.CFG
|
||||||
|
ShellCheck.CFGAnalysis
|
||||||
ShellCheck.Checker
|
ShellCheck.Checker
|
||||||
ShellCheck.Checks.Commands
|
ShellCheck.Checks.Commands
|
||||||
|
ShellCheck.Checks.ControlFlow
|
||||||
ShellCheck.Checks.Custom
|
ShellCheck.Checks.Custom
|
||||||
ShellCheck.Checks.ShellSupport
|
ShellCheck.Checks.ShellSupport
|
||||||
ShellCheck.Data
|
ShellCheck.Data
|
||||||
|
ShellCheck.Debug
|
||||||
ShellCheck.Fixer
|
ShellCheck.Fixer
|
||||||
ShellCheck.Formatter.Format
|
ShellCheck.Formatter.Format
|
||||||
ShellCheck.Formatter.CheckStyle
|
ShellCheck.Formatter.CheckStyle
|
||||||
|
@ -80,9 +93,11 @@ library
|
||||||
ShellCheck.Formatter.Quiet
|
ShellCheck.Formatter.Quiet
|
||||||
ShellCheck.Interface
|
ShellCheck.Interface
|
||||||
ShellCheck.Parser
|
ShellCheck.Parser
|
||||||
|
ShellCheck.Prelude
|
||||||
ShellCheck.Regex
|
ShellCheck.Regex
|
||||||
other-modules:
|
other-modules:
|
||||||
Paths_ShellCheck
|
Paths_ShellCheck
|
||||||
|
default-language: Haskell98
|
||||||
|
|
||||||
executable shellcheck
|
executable shellcheck
|
||||||
if impl(ghc < 8.0)
|
if impl(ghc < 8.0)
|
||||||
|
@ -91,18 +106,21 @@ executable shellcheck
|
||||||
build-depends:
|
build-depends:
|
||||||
aeson,
|
aeson,
|
||||||
array,
|
array,
|
||||||
base >= 4 && < 5,
|
base,
|
||||||
bytestring,
|
bytestring,
|
||||||
containers,
|
containers,
|
||||||
deepseq >= 1.4.0.0,
|
deepseq,
|
||||||
Diff >= 0.2.0,
|
Diff,
|
||||||
directory >= 1.2.3.0,
|
directory,
|
||||||
mtl >= 2.2.1,
|
fgl,
|
||||||
|
mtl,
|
||||||
filepath,
|
filepath,
|
||||||
parsec >= 3.0,
|
parsec,
|
||||||
QuickCheck >= 2.7.4,
|
QuickCheck,
|
||||||
regex-tdfa,
|
regex-tdfa,
|
||||||
|
transformers,
|
||||||
ShellCheck
|
ShellCheck
|
||||||
|
default-language: Haskell98
|
||||||
main-is: shellcheck.hs
|
main-is: shellcheck.hs
|
||||||
|
|
||||||
test-suite test-shellcheck
|
test-suite test-shellcheck
|
||||||
|
@ -110,17 +128,19 @@ test-suite test-shellcheck
|
||||||
build-depends:
|
build-depends:
|
||||||
aeson,
|
aeson,
|
||||||
array,
|
array,
|
||||||
base >= 4 && < 5,
|
base,
|
||||||
bytestring,
|
bytestring,
|
||||||
containers,
|
containers,
|
||||||
deepseq >= 1.4.0.0,
|
deepseq,
|
||||||
Diff >= 0.2.0,
|
Diff,
|
||||||
directory >= 1.2.3.0,
|
directory,
|
||||||
mtl >= 2.2.1,
|
fgl,
|
||||||
filepath,
|
filepath,
|
||||||
|
mtl,
|
||||||
parsec,
|
parsec,
|
||||||
QuickCheck >= 2.7.4,
|
QuickCheck,
|
||||||
regex-tdfa,
|
regex-tdfa,
|
||||||
|
transformers,
|
||||||
ShellCheck
|
ShellCheck
|
||||||
|
default-language: Haskell98
|
||||||
main-is: test/shellcheck.hs
|
main-is: test/shellcheck.hs
|
||||||
|
|
||||||
|
|
17
build/README.md
Normal file
17
build/README.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
This directory contains Dockerfiles for all builds.
|
||||||
|
|
||||||
|
A build image will:
|
||||||
|
|
||||||
|
* Run on Linux x86\_64 with vanilla Docker (no exceptions)
|
||||||
|
* Not contain any software that would restrict easy modification or copying
|
||||||
|
* Take a `cabal sdist` style tar.gz of the ShellCheck directory on stdin
|
||||||
|
* Output a tar.gz of artifacts on stdout, in a directory named for the arch
|
||||||
|
|
||||||
|
This makes it simple to build any release without exotic hardware or software.
|
||||||
|
|
||||||
|
An image can be built and tagged using `build_builder`,
|
||||||
|
and run on a source tarball using `run_builder`.
|
||||||
|
|
||||||
|
Tip: Are you developing an image that relies on QEmu usermode emulation?
|
||||||
|
It's easy to accidentally depend on binfmt\_misc on the host OS.
|
||||||
|
Do a `echo 0 | sudo tee /proc/sys/fs/binfmt_misc/status` before testing.
|
12
build/build_builder
Executable file
12
build/build_builder
Executable file
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/sh
|
||||||
|
if [ $# -eq 0 ]
|
||||||
|
then
|
||||||
|
echo >&2 "No build image directories specified"
|
||||||
|
echo >&2 "Example: $0 build/*/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for dir
|
||||||
|
do
|
||||||
|
( cd "$dir" && docker build -t "$(cat tag)" . ) || exit 1
|
||||||
|
done
|
40
build/darwin.aarch64/Dockerfile
Normal file
40
build/darwin.aarch64/Dockerfile
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
FROM ghcr.io/shepherdjerred/macos-cross-compiler:latest
|
||||||
|
|
||||||
|
ENV TARGET aarch64-apple-darwin22
|
||||||
|
ENV TARGETNAME darwin.aarch64
|
||||||
|
|
||||||
|
# Build dependencies
|
||||||
|
USER root
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
ENV LC_ALL C.utf8
|
||||||
|
|
||||||
|
# Install basic deps
|
||||||
|
RUN apt-get update && apt-get install -y automake autoconf build-essential curl xz-utils qemu-user-static
|
||||||
|
|
||||||
|
# Install a more suitable host compiler
|
||||||
|
WORKDIR /host-ghc
|
||||||
|
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin
|
||||||
|
RUN curl -L 'https://downloads.haskell.org/~ghc/8.10.7/ghc-8.10.7-x86_64-deb10-linux.tar.xz' | tar xJ --strip-components=1
|
||||||
|
RUN ./configure && make install
|
||||||
|
|
||||||
|
# Build GHC. We have to use an old version because cross-compilation across OS has since broken.
|
||||||
|
WORKDIR /ghc
|
||||||
|
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.7/ghc-8.10.7-src.tar.xz" | tar xJ --strip-components=1
|
||||||
|
RUN apt-get install -y llvm-12
|
||||||
|
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
|
||||||
|
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
|
||||||
|
RUN make install
|
||||||
|
|
||||||
|
# Due to an apparent cabal bug, we specify our options directly to cabal
|
||||||
|
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
|
||||||
|
ENV CABALOPTS "--ghc-options;-optc-Os -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;--constraint=hashable==1.3.5.0"
|
||||||
|
|
||||||
|
# Prebuild the dependencies
|
||||||
|
RUN cabal update
|
||||||
|
RUN IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
|
||||||
|
|
||||||
|
# Copy the build script
|
||||||
|
COPY build /usr/bin
|
||||||
|
|
||||||
|
WORKDIR /scratch
|
||||||
|
ENTRYPOINT ["/usr/bin/build"]
|
16
build/darwin.aarch64/build
Executable file
16
build/darwin.aarch64/build
Executable file
|
@ -0,0 +1,16 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -xe
|
||||||
|
{
|
||||||
|
tar xzv --strip-components=1
|
||||||
|
chmod +x striptests && ./striptests
|
||||||
|
mkdir "$TARGETNAME"
|
||||||
|
( IFS=';'; cabal build $CABALOPTS )
|
||||||
|
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
|
||||||
|
ls -l "$TARGETNAME"
|
||||||
|
# Stripping invalidates the code signature and the build image does
|
||||||
|
# not appear to have anything similar to the 'codesign' tool.
|
||||||
|
# "$TARGET-strip" "$TARGETNAME/shellcheck"
|
||||||
|
ls -l "$TARGETNAME"
|
||||||
|
file "$TARGETNAME/shellcheck" | grep "Mach-O 64-bit arm64 executable"
|
||||||
|
} >&2
|
||||||
|
tar czv "$TARGETNAME"
|
1
build/darwin.aarch64/tag
Normal file
1
build/darwin.aarch64/tag
Normal file
|
@ -0,0 +1 @@
|
||||||
|
koalaman/scbuilder-darwin-aarch64
|
33
build/darwin.x86_64/Dockerfile
Normal file
33
build/darwin.x86_64/Dockerfile
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
FROM liushuyu/osxcross@sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76
|
||||||
|
|
||||||
|
ENV TARGET x86_64-apple-darwin18
|
||||||
|
ENV TARGETNAME darwin.x86_64
|
||||||
|
|
||||||
|
# Build dependencies
|
||||||
|
USER root
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get dist-upgrade -y
|
||||||
|
RUN apt-get install -y ghc automake autoconf llvm curl alex happy
|
||||||
|
|
||||||
|
# Build GHC
|
||||||
|
WORKDIR /ghc
|
||||||
|
RUN curl -L "https://downloads.haskell.org/~ghc/9.2.5/ghc-9.2.5-src.tar.xz" | tar xJ --strip-components=1
|
||||||
|
RUN ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
|
||||||
|
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
|
||||||
|
RUN make install
|
||||||
|
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin
|
||||||
|
|
||||||
|
# Due to an apparent cabal bug, we specify our options directly to cabal
|
||||||
|
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
|
||||||
|
ENV CABALOPTS "--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
|
||||||
|
|
||||||
|
# Prebuild the dependencies
|
||||||
|
RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
|
||||||
|
|
||||||
|
# Copy the build script
|
||||||
|
COPY build /usr/bin
|
||||||
|
|
||||||
|
WORKDIR /scratch
|
||||||
|
ENTRYPOINT ["/usr/bin/build"]
|
13
build/darwin.x86_64/build
Executable file
13
build/darwin.x86_64/build
Executable file
|
@ -0,0 +1,13 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -xe
|
||||||
|
{
|
||||||
|
tar xzv --strip-components=1
|
||||||
|
chmod +x striptests && ./striptests
|
||||||
|
mkdir "$TARGETNAME"
|
||||||
|
( IFS=';'; cabal build $CABALOPTS )
|
||||||
|
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
|
||||||
|
ls -l "$TARGETNAME"
|
||||||
|
"$TARGET-strip" -Sx "$TARGETNAME/shellcheck"
|
||||||
|
ls -l "$TARGETNAME"
|
||||||
|
} >&2
|
||||||
|
tar czv "$TARGETNAME"
|
1
build/darwin.x86_64/tag
Normal file
1
build/darwin.x86_64/tag
Normal file
|
@ -0,0 +1 @@
|
||||||
|
koalaman/scbuilder-darwin-x86_64
|
40
build/linux.aarch64/Dockerfile
Normal file
40
build/linux.aarch64/Dockerfile
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
FROM ubuntu:20.04
|
||||||
|
|
||||||
|
ENV TARGET aarch64-linux-gnu
|
||||||
|
ENV TARGETNAME linux.aarch64
|
||||||
|
|
||||||
|
# Build dependencies
|
||||||
|
USER root
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
|
||||||
|
# These deps are from 20.04, because GHC's compiler/llvm support moves slowly
|
||||||
|
RUN apt-get update && apt-get install -y llvm gcc-$TARGET
|
||||||
|
|
||||||
|
# The rest are from 22.10
|
||||||
|
RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list
|
||||||
|
# Kinetic does not receive updates anymore, switch to last available
|
||||||
|
RUN sed -e 's/archive.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list
|
||||||
|
RUN sed -e 's/security.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y ghc alex happy automake autoconf build-essential curl qemu-user-static
|
||||||
|
|
||||||
|
# Build GHC
|
||||||
|
WORKDIR /ghc
|
||||||
|
RUN curl -L "https://downloads.haskell.org/~ghc/9.2.8/ghc-9.2.8-src.tar.xz" | tar xJ --strip-components=1
|
||||||
|
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
|
||||||
|
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
|
||||||
|
RUN make install
|
||||||
|
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin
|
||||||
|
|
||||||
|
# Due to an apparent cabal bug, we specify our options directly to cabal
|
||||||
|
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
|
||||||
|
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;-c;hashable -arch-native"
|
||||||
|
|
||||||
|
# Prebuild the dependencies
|
||||||
|
RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
|
||||||
|
|
||||||
|
# Copy the build script
|
||||||
|
COPY build /usr/bin
|
||||||
|
|
||||||
|
WORKDIR /scratch
|
||||||
|
ENTRYPOINT ["/usr/bin/build"]
|
14
build/linux.aarch64/build
Executable file
14
build/linux.aarch64/build
Executable file
|
@ -0,0 +1,14 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -xe
|
||||||
|
{
|
||||||
|
tar xzv --strip-components=1
|
||||||
|
chmod +x striptests && ./striptests
|
||||||
|
mkdir "$TARGETNAME"
|
||||||
|
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
|
||||||
|
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
|
||||||
|
ls -l "$TARGETNAME"
|
||||||
|
"$TARGET-strip" -s "$TARGETNAME/shellcheck"
|
||||||
|
ls -l "$TARGETNAME"
|
||||||
|
qemu-aarch64-static "$TARGETNAME/shellcheck" --version
|
||||||
|
} >&2
|
||||||
|
tar czv "$TARGETNAME"
|
1
build/linux.aarch64/tag
Normal file
1
build/linux.aarch64/tag
Normal file
|
@ -0,0 +1 @@
|
||||||
|
koalaman/scbuilder-linux-aarch64
|
42
build/linux.armv6hf/Dockerfile
Normal file
42
build/linux.armv6hf/Dockerfile
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# This Docker file uses a custom QEmu fork with patches to follow execve
|
||||||
|
# to build all of ShellCheck emulated.
|
||||||
|
|
||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
ENV TARGETNAME linux.armv6hf
|
||||||
|
|
||||||
|
# Build QEmu with execve follow support
|
||||||
|
USER root
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get install -y --no-install-recommends build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev python3-setuptools ca-certificates debootstrap
|
||||||
|
WORKDIR /qemu
|
||||||
|
RUN git clone --depth 1 https://github.com/koalaman/qemu .
|
||||||
|
RUN ./configure --static --disable-werror && cd build && ninja qemu-arm
|
||||||
|
ENV QEMU_EXECVE 1
|
||||||
|
|
||||||
|
# Convenience utility
|
||||||
|
COPY scutil /bin/scutil
|
||||||
|
COPY scutil /chroot/bin/scutil
|
||||||
|
RUN chmod +x /bin/scutil /chroot/bin/scutil
|
||||||
|
|
||||||
|
# Set up an armv6 userspace
|
||||||
|
WORKDIR /
|
||||||
|
RUN debootstrap --arch armhf --variant=minbase --foreign bookworm /chroot http://mirrordirector.raspbian.org/raspbian
|
||||||
|
RUN cp /qemu/build/qemu-arm /chroot/bin/qemu
|
||||||
|
RUN scutil emu /debootstrap/debootstrap --second-stage
|
||||||
|
|
||||||
|
# Install deps in the chroot
|
||||||
|
RUN scutil emu apt-get update
|
||||||
|
RUN scutil emu apt-get install -y --no-install-recommends ghc cabal-install
|
||||||
|
RUN scutil emu cabal update
|
||||||
|
|
||||||
|
# Finally we can build the current dependencies. This takes hours.
|
||||||
|
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--gcc-options;-Os -Wl,--gc-sections -ffunction-sections -fdata-sections"
|
||||||
|
# Generated with `cabal freeze --constraint 'hashable -arch-native'`
|
||||||
|
COPY cabal.project.freeze /chroot/etc
|
||||||
|
RUN IFS=";" && scutil install_from_freeze /chroot/etc/cabal.project.freeze emu cabal install $CABALOPTS
|
||||||
|
|
||||||
|
# Copy the build script
|
||||||
|
COPY build /chroot/bin
|
||||||
|
ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"]
|
17
build/linux.armv6hf/build
Executable file
17
build/linux.armv6hf/build
Executable file
|
@ -0,0 +1,17 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -xe
|
||||||
|
mkdir /scratch && cd /scratch
|
||||||
|
{
|
||||||
|
tar xzv --strip-components=1
|
||||||
|
cp /etc/cabal.project.freeze .
|
||||||
|
chmod +x striptests && ./striptests
|
||||||
|
mkdir "$TARGETNAME"
|
||||||
|
# This script does not cabal update because compiling anything new is slow
|
||||||
|
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
|
||||||
|
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
|
||||||
|
ls -l "$TARGETNAME"
|
||||||
|
strip -s "$TARGETNAME/shellcheck"
|
||||||
|
ls -l "$TARGETNAME"
|
||||||
|
"$TARGETNAME/shellcheck" --version
|
||||||
|
} >&2
|
||||||
|
tar czv "$TARGETNAME"
|
93
build/linux.armv6hf/cabal.project.freeze
Normal file
93
build/linux.armv6hf/cabal.project.freeze
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
active-repositories: hackage.haskell.org:merge
|
||||||
|
constraints: any.Diff ==0.5,
|
||||||
|
any.OneTuple ==0.4.2,
|
||||||
|
any.QuickCheck ==2.14.3,
|
||||||
|
QuickCheck -old-random +templatehaskell,
|
||||||
|
any.StateVar ==1.2.2,
|
||||||
|
any.aeson ==2.2.3.0,
|
||||||
|
aeson +ordered-keymap,
|
||||||
|
any.array ==0.5.4.0,
|
||||||
|
any.assoc ==1.1.1,
|
||||||
|
assoc -tagged,
|
||||||
|
any.base ==4.15.1.0,
|
||||||
|
any.base-orphans ==0.9.2,
|
||||||
|
any.bifunctors ==5.6.2,
|
||||||
|
bifunctors +tagged,
|
||||||
|
any.binary ==0.8.8.0,
|
||||||
|
any.bytestring ==0.10.12.1,
|
||||||
|
any.character-ps ==0.1,
|
||||||
|
any.comonad ==5.0.8,
|
||||||
|
comonad +containers +distributive +indexed-traversable,
|
||||||
|
any.containers ==0.6.4.1,
|
||||||
|
any.contravariant ==1.5.5,
|
||||||
|
contravariant +semigroups +statevar +tagged,
|
||||||
|
any.data-array-byte ==0.1.0.1,
|
||||||
|
any.data-fix ==0.3.3,
|
||||||
|
any.deepseq ==1.4.5.0,
|
||||||
|
any.directory ==1.3.6.2,
|
||||||
|
any.distributive ==0.6.2.1,
|
||||||
|
distributive +semigroups +tagged,
|
||||||
|
any.dlist ==1.0,
|
||||||
|
dlist -werror,
|
||||||
|
any.exceptions ==0.10.4,
|
||||||
|
any.fgl ==5.8.2.0,
|
||||||
|
fgl +containers042,
|
||||||
|
any.filepath ==1.4.2.1,
|
||||||
|
any.foldable1-classes-compat ==0.1,
|
||||||
|
foldable1-classes-compat +tagged,
|
||||||
|
any.generically ==0.1.1,
|
||||||
|
any.ghc-bignum ==1.1,
|
||||||
|
any.ghc-boot-th ==9.0.2,
|
||||||
|
any.ghc-prim ==0.7.0,
|
||||||
|
any.hashable ==1.4.6.0,
|
||||||
|
hashable -arch-native +integer-gmp -random-initial-seed,
|
||||||
|
any.indexed-traversable ==0.1.4,
|
||||||
|
any.indexed-traversable-instances ==0.1.2,
|
||||||
|
any.integer-conversion ==0.1.1,
|
||||||
|
any.integer-logarithms ==1.0.3.1,
|
||||||
|
integer-logarithms -check-bounds +integer-gmp,
|
||||||
|
any.mtl ==2.2.2,
|
||||||
|
any.network-uri ==2.6.4.2,
|
||||||
|
any.parsec ==3.1.14.0,
|
||||||
|
any.pretty ==1.1.3.6,
|
||||||
|
any.primitive ==0.9.0.0,
|
||||||
|
any.process ==1.6.13.2,
|
||||||
|
any.random ==1.2.1.2,
|
||||||
|
any.regex-base ==0.94.0.2,
|
||||||
|
any.regex-tdfa ==1.3.2.2,
|
||||||
|
regex-tdfa +doctest -force-o2,
|
||||||
|
any.rts ==1.0.2,
|
||||||
|
any.scientific ==0.3.8.0,
|
||||||
|
scientific -integer-simple,
|
||||||
|
any.semialign ==1.3.1,
|
||||||
|
semialign +semigroupoids,
|
||||||
|
any.semigroupoids ==6.0.1,
|
||||||
|
semigroupoids +comonad +containers +contravariant +distributive +tagged +unordered-containers,
|
||||||
|
any.splitmix ==0.1.0.5,
|
||||||
|
splitmix -optimised-mixer,
|
||||||
|
any.stm ==2.5.0.0,
|
||||||
|
any.strict ==0.5,
|
||||||
|
any.tagged ==0.8.8,
|
||||||
|
tagged +deepseq +transformers,
|
||||||
|
any.template-haskell ==2.17.0.0,
|
||||||
|
any.text ==1.2.5.0,
|
||||||
|
any.text-iso8601 ==0.1.1,
|
||||||
|
any.text-short ==0.1.6,
|
||||||
|
text-short -asserts,
|
||||||
|
any.th-abstraction ==0.7.0.0,
|
||||||
|
any.th-compat ==0.1.5,
|
||||||
|
any.these ==1.2.1,
|
||||||
|
any.time ==1.9.3,
|
||||||
|
any.time-compat ==1.9.7,
|
||||||
|
any.transformers ==0.5.6.2,
|
||||||
|
any.transformers-compat ==0.7.2,
|
||||||
|
transformers-compat -five +five-three -four +generic-deriving +mtl -three -two,
|
||||||
|
any.unix ==2.7.2.2,
|
||||||
|
any.unordered-containers ==0.2.20,
|
||||||
|
unordered-containers -debug,
|
||||||
|
any.uuid-types ==1.0.6,
|
||||||
|
any.vector ==0.13.1.0,
|
||||||
|
vector +boundschecks -internalchecks -unsafechecks -wall,
|
||||||
|
any.vector-stream ==0.1.0.1,
|
||||||
|
any.witherable ==0.5
|
||||||
|
index-state: hackage.haskell.org 2024-06-18T02:21:19Z
|
48
build/linux.armv6hf/scutil
Normal file
48
build/linux.armv6hf/scutil
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
#!/bin/dash
|
||||||
|
# Various ShellCheck build utility functions
|
||||||
|
|
||||||
|
# Generally set a ulimit to avoid QEmu using too much memory
|
||||||
|
ulimit -v "$((10*1024*1024))"
|
||||||
|
# If we happen to invoke or run under QEmu, make sure to follow execve.
|
||||||
|
# This requires a patched QEmu.
|
||||||
|
export QEMU_EXECVE=1
|
||||||
|
|
||||||
|
# Retry a command until it succeeds
|
||||||
|
# Usage: scutil retry 3 mycmd
|
||||||
|
retry() {
|
||||||
|
n="$1"
|
||||||
|
ret=1
|
||||||
|
shift
|
||||||
|
while [ "$n" -gt 0 ]
|
||||||
|
do
|
||||||
|
"$@"
|
||||||
|
ret=$?
|
||||||
|
[ "$ret" = 0 ] && break
|
||||||
|
n=$((n-1))
|
||||||
|
done
|
||||||
|
return "$ret"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install all dependencies from a freeze file
|
||||||
|
# Usage: scutil install_from_freeze /path/cabal.project.freeze cabal install
|
||||||
|
install_from_freeze() {
|
||||||
|
linefeed=$(printf '\nx')
|
||||||
|
linefeed=${linefeed%x}
|
||||||
|
flags=$(
|
||||||
|
sed 's/constraints:/&\n /' "$1" |
|
||||||
|
grep -vw -e rts -e base |
|
||||||
|
sed -n -e 's/^ *\([^,]*\).*/\1/p' |
|
||||||
|
sed -e 's/any\.\([^ ]*\) ==\(.*\)/\1-\2/; te; s/.*/--constraint\n&/; :e')
|
||||||
|
shift
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
( IFS=$linefeed; set -x; "$@" $flags )
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run a command under emulation.
|
||||||
|
# This assumes the correct emulator is named 'qemu' and the chroot is /chroot
|
||||||
|
# Usage: scutil emu echo "Hello World"
|
||||||
|
emu() {
|
||||||
|
chroot /chroot /bin/qemu /usr/bin/env "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
"$@"
|
1
build/linux.armv6hf/tag
Normal file
1
build/linux.armv6hf/tag
Normal file
|
@ -0,0 +1 @@
|
||||||
|
koalaman/scbuilder-linux-armv6hf
|
46
build/linux.riscv64/Dockerfile
Normal file
46
build/linux.riscv64/Dockerfile
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
ENV TARGETNAME linux.riscv64
|
||||||
|
ENV TARGET riscv64-linux-gnu
|
||||||
|
|
||||||
|
USER root
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
|
||||||
|
# Init base
|
||||||
|
RUN apt-get update -y
|
||||||
|
|
||||||
|
# Install qemu
|
||||||
|
RUN apt-get install -y --no-install-recommends build-essential ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev curl ca-certificates python3-virtualenv git python3-setuptools debootstrap
|
||||||
|
WORKDIR /qemu
|
||||||
|
RUN git clone --depth 1 https://github.com/koalaman/qemu .
|
||||||
|
RUN ./configure --target-list=riscv64-linux-user --static --disable-system --disable-pie --disable-werror
|
||||||
|
RUN cd build && ninja qemu-riscv64
|
||||||
|
ENV QEMU_EXECVE 1
|
||||||
|
|
||||||
|
# Convenience utility
|
||||||
|
COPY scutil /bin/scutil
|
||||||
|
# We have to copy to /usr/bin because debootstrap will try to symlink /bin and fail if it exists
|
||||||
|
COPY scutil /chroot/usr/bin/scutil
|
||||||
|
RUN chmod +x /bin/scutil /chroot/usr/bin/scutil
|
||||||
|
|
||||||
|
# Set up a riscv64 userspace
|
||||||
|
WORKDIR /
|
||||||
|
RUN debootstrap --arch=riscv64 --variant=minbase --components=main,universe --foreign noble /chroot http://ports.ubuntu.com/ubuntu-ports
|
||||||
|
RUN cp /qemu/build/qemu-riscv64 /chroot/bin/qemu
|
||||||
|
RUN scutil emu /debootstrap/debootstrap --second-stage
|
||||||
|
|
||||||
|
# Install deps in the chroot
|
||||||
|
RUN scutil emu apt-get update
|
||||||
|
RUN scutil emu apt-get install -y --no-install-recommends ghc cabal-install
|
||||||
|
RUN scutil emu cabal update
|
||||||
|
|
||||||
|
# Generated with: cabal freeze -c 'hashable -arch-native'. We put it in /etc so cabal won't find it.
|
||||||
|
COPY cabal.project.freeze /chroot/etc
|
||||||
|
|
||||||
|
# Build all dependencies from the freeze file. The emulator segfaults at random,
|
||||||
|
# so retry a few times.
|
||||||
|
RUN scutil install_from_freeze /chroot/etc/cabal.project.freeze retry 5 emu cabal install --keep-going
|
||||||
|
|
||||||
|
# Copy the build script
|
||||||
|
COPY build /chroot/bin/build
|
||||||
|
ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"]
|
21
build/linux.riscv64/build
Executable file
21
build/linux.riscv64/build
Executable file
|
@ -0,0 +1,21 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -xe
|
||||||
|
IFS=';'
|
||||||
|
{
|
||||||
|
mkdir -p /tmp/scratch
|
||||||
|
cd /tmp/scratch
|
||||||
|
tar xzv --strip-components=1
|
||||||
|
chmod +x striptests && ./striptests
|
||||||
|
# Use a freeze file to ensure we use the same dependencies we cached during
|
||||||
|
# the docker image build. We don't want to spend time compiling anything new.
|
||||||
|
cp /etc/cabal.project.freeze .
|
||||||
|
mkdir "$TARGETNAME"
|
||||||
|
# Retry in case of random segfault
|
||||||
|
scutil retry 3 cabal build --enable-executable-static
|
||||||
|
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
|
||||||
|
ls -l "$TARGETNAME"
|
||||||
|
"$TARGET-strip" -s "$TARGETNAME/shellcheck"
|
||||||
|
ls -l "$TARGETNAME"
|
||||||
|
"$TARGETNAME/shellcheck" --version
|
||||||
|
} >&2
|
||||||
|
tar czv "$TARGETNAME"
|
93
build/linux.riscv64/cabal.project.freeze
Normal file
93
build/linux.riscv64/cabal.project.freeze
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
active-repositories: hackage.haskell.org:merge
|
||||||
|
constraints: any.Diff ==0.5,
|
||||||
|
any.OneTuple ==0.4.2,
|
||||||
|
any.QuickCheck ==2.14.3,
|
||||||
|
QuickCheck -old-random +templatehaskell,
|
||||||
|
any.StateVar ==1.2.2,
|
||||||
|
any.aeson ==2.2.3.0,
|
||||||
|
aeson +ordered-keymap,
|
||||||
|
any.array ==0.5.4.0,
|
||||||
|
any.assoc ==1.1.1,
|
||||||
|
assoc -tagged,
|
||||||
|
any.base ==4.17.2.0,
|
||||||
|
any.base-orphans ==0.9.2,
|
||||||
|
any.bifunctors ==5.6.2,
|
||||||
|
bifunctors +tagged,
|
||||||
|
any.binary ==0.8.9.1,
|
||||||
|
any.bytestring ==0.11.5.2,
|
||||||
|
any.character-ps ==0.1,
|
||||||
|
any.comonad ==5.0.8,
|
||||||
|
comonad +containers +distributive +indexed-traversable,
|
||||||
|
any.containers ==0.6.7,
|
||||||
|
any.contravariant ==1.5.5,
|
||||||
|
contravariant +semigroups +statevar +tagged,
|
||||||
|
any.data-fix ==0.3.3,
|
||||||
|
any.deepseq ==1.4.8.0,
|
||||||
|
any.directory ==1.3.7.1,
|
||||||
|
any.distributive ==0.6.2.1,
|
||||||
|
distributive +semigroups +tagged,
|
||||||
|
any.dlist ==1.0,
|
||||||
|
dlist -werror,
|
||||||
|
any.exceptions ==0.10.5,
|
||||||
|
any.fgl ==5.8.2.0,
|
||||||
|
fgl +containers042,
|
||||||
|
any.filepath ==1.4.2.2,
|
||||||
|
any.foldable1-classes-compat ==0.1,
|
||||||
|
foldable1-classes-compat +tagged,
|
||||||
|
any.generically ==0.1.1,
|
||||||
|
any.ghc-bignum ==1.3,
|
||||||
|
any.ghc-boot-th ==9.4.7,
|
||||||
|
any.ghc-prim ==0.9.1,
|
||||||
|
any.hashable ==1.4.6.0,
|
||||||
|
hashable -arch-native +integer-gmp -random-initial-seed,
|
||||||
|
any.indexed-traversable ==0.1.4,
|
||||||
|
any.indexed-traversable-instances ==0.1.2,
|
||||||
|
any.integer-conversion ==0.1.1,
|
||||||
|
any.integer-logarithms ==1.0.3.1,
|
||||||
|
integer-logarithms -check-bounds +integer-gmp,
|
||||||
|
any.mtl ==2.2.2,
|
||||||
|
any.network-uri ==2.6.4.2,
|
||||||
|
any.os-string ==2.0.3,
|
||||||
|
any.parsec ==3.1.16.1,
|
||||||
|
any.pretty ==1.1.3.6,
|
||||||
|
any.primitive ==0.9.0.0,
|
||||||
|
any.process ==1.6.17.0,
|
||||||
|
any.random ==1.2.1.2,
|
||||||
|
any.regex-base ==0.94.0.2,
|
||||||
|
any.regex-tdfa ==1.3.2.2,
|
||||||
|
regex-tdfa +doctest -force-o2,
|
||||||
|
any.rts ==1.0.2,
|
||||||
|
any.scientific ==0.3.8.0,
|
||||||
|
scientific -integer-simple,
|
||||||
|
any.semialign ==1.3.1,
|
||||||
|
semialign +semigroupoids,
|
||||||
|
any.semigroupoids ==6.0.1,
|
||||||
|
semigroupoids +comonad +containers +contravariant +distributive +tagged +unordered-containers,
|
||||||
|
any.splitmix ==0.1.0.5,
|
||||||
|
splitmix -optimised-mixer,
|
||||||
|
any.stm ==2.5.1.0,
|
||||||
|
any.strict ==0.5,
|
||||||
|
any.tagged ==0.8.8,
|
||||||
|
tagged +deepseq +transformers,
|
||||||
|
any.template-haskell ==2.19.0.0,
|
||||||
|
any.text ==2.0.2,
|
||||||
|
any.text-iso8601 ==0.1.1,
|
||||||
|
any.text-short ==0.1.6,
|
||||||
|
text-short -asserts,
|
||||||
|
any.th-abstraction ==0.7.0.0,
|
||||||
|
any.th-compat ==0.1.5,
|
||||||
|
any.these ==1.2.1,
|
||||||
|
any.time ==1.12.2,
|
||||||
|
any.time-compat ==1.9.7,
|
||||||
|
any.transformers ==0.5.6.2,
|
||||||
|
any.transformers-compat ==0.7.2,
|
||||||
|
transformers-compat -five +five-three -four +generic-deriving +mtl -three -two,
|
||||||
|
any.unix ==2.7.3,
|
||||||
|
any.unordered-containers ==0.2.20,
|
||||||
|
unordered-containers -debug,
|
||||||
|
any.uuid-types ==1.0.6,
|
||||||
|
any.vector ==0.13.1.0,
|
||||||
|
vector +boundschecks -internalchecks -unsafechecks -wall,
|
||||||
|
any.vector-stream ==0.1.0.1,
|
||||||
|
any.witherable ==0.5
|
||||||
|
index-state: hackage.haskell.org 2024-06-17T00:48:51Z
|
1
build/linux.riscv64/tag
Normal file
1
build/linux.riscv64/tag
Normal file
|
@ -0,0 +1 @@
|
||||||
|
koalaman/scbuilder-linux-riscv64
|
24
build/linux.x86_64/Dockerfile
Normal file
24
build/linux.x86_64/Dockerfile
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
FROM alpine:3.16
|
||||||
|
# alpine:3.16 (GHC 9.0.1): 5.8 megabytes
|
||||||
|
# alpine:3.17 (GHC 9.0.2): 15.0 megabytes
|
||||||
|
# alpine:3.18 (GHC 9.4.4): 29.0 megabytes
|
||||||
|
# alpine:3.19 (GHC 9.4.7): 29.0 megabytes
|
||||||
|
|
||||||
|
ENV TARGETNAME linux.x86_64
|
||||||
|
|
||||||
|
# Install GHC and cabal
|
||||||
|
USER root
|
||||||
|
RUN apk add ghc cabal g++ libffi-dev curl bash
|
||||||
|
|
||||||
|
# Use ld.bfd instead of ld.gold due to
|
||||||
|
# x86_64-linux-gnu/libpthread.a(pthread_cond_init.o)(.note.stapsdt+0x14): error:
|
||||||
|
# relocation refers to local symbol "" [2], which is defined in a discarded section
|
||||||
|
ENV CABALOPTS "--ghc-options;-optl-Wl,-fuse-ld=bfd -split-sections -optc-Os -optc-Wl,--gc-sections"
|
||||||
|
|
||||||
|
# Other archs pre-build dependencies here, but this one doesn't to detect ecosystem movement
|
||||||
|
|
||||||
|
# Copy the build script
|
||||||
|
COPY build /usr/bin
|
||||||
|
|
||||||
|
WORKDIR /scratch
|
||||||
|
ENTRYPOINT ["/usr/bin/build"]
|
15
build/linux.x86_64/build
Executable file
15
build/linux.x86_64/build
Executable file
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -xe
|
||||||
|
{
|
||||||
|
tar xzv --strip-components=1
|
||||||
|
chmod +x striptests && ./striptests
|
||||||
|
mkdir "$TARGETNAME"
|
||||||
|
cabal update
|
||||||
|
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
|
||||||
|
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
|
||||||
|
ls -l "$TARGETNAME"
|
||||||
|
strip -s "$TARGETNAME/shellcheck"
|
||||||
|
ls -l "$TARGETNAME"
|
||||||
|
"$TARGETNAME/shellcheck" --version
|
||||||
|
} >&2
|
||||||
|
tar czv "$TARGETNAME"
|
1
build/linux.x86_64/tag
Normal file
1
build/linux.x86_64/tag
Normal file
|
@ -0,0 +1 @@
|
||||||
|
koalaman/scbuilder-linux-x86_64
|
30
build/run_builder
Executable file
30
build/run_builder
Executable file
|
@ -0,0 +1,30 @@
|
||||||
|
#!/bin/bash
|
||||||
|
if [ $# -lt 2 ]
|
||||||
|
then
|
||||||
|
echo >&2 "This script builds a source archive (as produced by cabal sdist)"
|
||||||
|
echo >&2 "Usage: $0 sourcefile.tar.gz builddir..."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
file=$(realpath "$1")
|
||||||
|
shift
|
||||||
|
|
||||||
|
if [ ! -e "$file" ]
|
||||||
|
then
|
||||||
|
echo >&2 "$file does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -ex -o pipefail
|
||||||
|
|
||||||
|
for dir
|
||||||
|
do
|
||||||
|
tagfile="$dir/tag"
|
||||||
|
if [ ! -e "$tagfile" ]
|
||||||
|
then
|
||||||
|
echo >&2 "$tagfile does not exist"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run -i "$(< "$tagfile")" < "$file" | tar xz
|
||||||
|
done
|
27
build/windows.x86_64/Dockerfile
Normal file
27
build/windows.x86_64/Dockerfile
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
FROM ubuntu:20.04
|
||||||
|
|
||||||
|
ENV TARGETNAME windows.x86_64
|
||||||
|
|
||||||
|
# We don't need wine32, even though it complains
|
||||||
|
USER root
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
RUN apt-get update && apt-get install -y curl busybox wine winbind
|
||||||
|
|
||||||
|
# Fetch Windows version, will be available under z:\haskell
|
||||||
|
WORKDIR /haskell
|
||||||
|
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-x86_64-unknown-mingw32.tar.xz" | tar xJ --strip-components=1
|
||||||
|
WORKDIR /haskell/bin
|
||||||
|
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-mingw32.zip" | busybox unzip -
|
||||||
|
RUN curl -L "https://curl.se/windows/dl-8.7.1_7/curl-8.7.1_7-win64-mingw.zip" | busybox unzip - && mv curl-*-win64-mingw/bin/* .
|
||||||
|
ENV WINEPATH /haskell/bin
|
||||||
|
|
||||||
|
# It's unknown whether Cabal on Windows suffers from the same issue
|
||||||
|
# that necessitated this but I don't care enough to find out
|
||||||
|
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections"
|
||||||
|
|
||||||
|
# Precompile some deps to speed up later builds
|
||||||
|
RUN wine /haskell/bin/cabal.exe update && IFS=';' && wine /haskell/bin/cabal.exe install --lib --dependencies-only $CABALOPTS ShellCheck
|
||||||
|
|
||||||
|
COPY build /usr/bin
|
||||||
|
WORKDIR /scratch
|
||||||
|
ENTRYPOINT ["/usr/bin/build"]
|
18
build/windows.x86_64/build
Executable file
18
build/windows.x86_64/build
Executable file
|
@ -0,0 +1,18 @@
|
||||||
|
#!/bin/sh
|
||||||
|
cabal() {
|
||||||
|
wine /haskell/bin/cabal.exe "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
set -xe
|
||||||
|
{
|
||||||
|
tar xzv --strip-components=1
|
||||||
|
chmod +x striptests && ./striptests
|
||||||
|
mkdir "$TARGETNAME"
|
||||||
|
( IFS=';'; cabal build $CABALOPTS )
|
||||||
|
find dist*/ -name shellcheck.exe -type f -ls -exec mv {} "$TARGETNAME/" \;
|
||||||
|
ls -l "$TARGETNAME"
|
||||||
|
wine "/haskell/mingw/bin/strip.exe" -s "$TARGETNAME/shellcheck.exe"
|
||||||
|
ls -l "$TARGETNAME"
|
||||||
|
wine "$TARGETNAME/shellcheck.exe" --version
|
||||||
|
} >&2
|
||||||
|
tar czv "$TARGETNAME"
|
1
build/windows.x86_64/tag
Normal file
1
build/windows.x86_64/tag
Normal file
|
@ -0,0 +1 @@
|
||||||
|
koalaman/scbuilder-windows-x86_64
|
294
doc/shellcheck_logo.svg
Normal file
294
doc/shellcheck_logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 244 KiB |
|
@ -6,7 +6,7 @@ then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for i in 1 2
|
for i in 1 2 3
|
||||||
do
|
do
|
||||||
last=$(grep -hv "^prop" ./**/*.hs | grep -Ewo "${i}[0-9]{3}" | sort -n | tail -n 1)
|
last=$(grep -hv "^prop" ./**/*.hs | grep -Ewo "${i}[0-9]{3}" | sort -n | tail -n 1)
|
||||||
echo "Next ${i}xxx: $((last+1))"
|
echo "Next ${i}xxx: $((last+1))"
|
||||||
|
|
10
quickrun
10
quickrun
|
@ -2,4 +2,12 @@
|
||||||
# quickrun runs ShellCheck in an interpreted mode.
|
# quickrun runs ShellCheck in an interpreted mode.
|
||||||
# This allows testing changes without recompiling.
|
# This allows testing changes without recompiling.
|
||||||
|
|
||||||
runghc -isrc -idist/build/autogen shellcheck.hs "$@"
|
path=$(find . -type f -path './dist*/Paths_ShellCheck.hs' | sort | head -n 1)
|
||||||
|
if [ -z "$path" ]
|
||||||
|
then
|
||||||
|
echo >&2 "Unable to find Paths_ShellCheck.hs. Please 'cabal build' once."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
path="${path%/*}"
|
||||||
|
|
||||||
|
exec runghc -isrc -i"$path" shellcheck.hs "$@"
|
||||||
|
|
11
quicktest
11
quicktest
|
@ -3,8 +3,17 @@
|
||||||
# This allows running tests without compiling, which can be faster.
|
# This allows running tests without compiling, which can be faster.
|
||||||
# 'cabal test' remains the source of truth.
|
# 'cabal test' remains the source of truth.
|
||||||
|
|
||||||
|
path=$(find . -type f -path './dist*/Paths_ShellCheck.hs' | sort | head -n 1)
|
||||||
|
if [ -z "$path" ]
|
||||||
|
then
|
||||||
|
echo >&2 "Unable to find Paths_ShellCheck.hs. Please 'cabal build' once."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
path="${path%/*}"
|
||||||
|
|
||||||
|
|
||||||
(
|
(
|
||||||
var=$(echo 'main' | ghci test/shellcheck.hs 2>&1 | tee /dev/stderr)
|
var=$(echo 'main' | ghci -isrc -i"$path" test/shellcheck.hs 2>&1 | tee /dev/stderr)
|
||||||
if [[ $var == *ExitSuccess* ]]
|
if [[ $var == *ExitSuccess* ]]
|
||||||
then
|
then
|
||||||
exit 0
|
exit 0
|
||||||
|
|
11
setgitversion
Executable file
11
setgitversion
Executable file
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/sh -xe
|
||||||
|
# This script hardcodes the `git describe` version as ShellCheck's version number.
|
||||||
|
# This is done to allow shellcheck --version to differ from the cabal version when
|
||||||
|
# building git snapshots.
|
||||||
|
|
||||||
|
file="src/ShellCheck/Data.hs"
|
||||||
|
test -e "$file"
|
||||||
|
tmp=$(mktemp)
|
||||||
|
version=$(git describe)
|
||||||
|
sed -e "s/=.*VERSIONSTRING.*/= \"$version\" -- VERSIONSTRING, DO NOT SUBMIT/" "$file" > "$tmp"
|
||||||
|
mv "$tmp" "$file"
|
|
@ -56,6 +56,13 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
||||||
options are cumulative, but all the codes can be specified at once,
|
options are cumulative, but all the codes can be specified at once,
|
||||||
comma-separated as a single argument.
|
comma-separated as a single argument.
|
||||||
|
|
||||||
|
**--extended-analysis=true/false**
|
||||||
|
|
||||||
|
: Enable/disable Dataflow Analysis to identify more issues (default true). If
|
||||||
|
ShellCheck uses too much CPU/RAM when checking scripts with several
|
||||||
|
thousand lines of code, extended analysis can be disabled with this flag
|
||||||
|
or a directive. This flag overrides directives and rc files.
|
||||||
|
|
||||||
**-f** *FORMAT*, **--format=***FORMAT*
|
**-f** *FORMAT*, **--format=***FORMAT*
|
||||||
|
|
||||||
: Specify the output format of shellcheck, which prints its results in the
|
: Specify the output format of shellcheck, which prints its results in the
|
||||||
|
@ -71,6 +78,11 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
||||||
|
|
||||||
: Don't try to look for .shellcheckrc configuration files.
|
: Don't try to look for .shellcheckrc configuration files.
|
||||||
|
|
||||||
|
**--rcfile** *RCFILE*
|
||||||
|
|
||||||
|
: Prefer the specified configuration file over searching for one
|
||||||
|
in the default locations.
|
||||||
|
|
||||||
**-o**\ *NAME1*[,*NAME2*...],\ **--enable=***NAME1*[,*NAME2*...]
|
**-o**\ *NAME1*[,*NAME2*...],\ **--enable=***NAME1*[,*NAME2*...]
|
||||||
|
|
||||||
: Enable optional checks. The special name *all* enables all of them.
|
: Enable optional checks. The special name *all* enables all of them.
|
||||||
|
@ -85,7 +97,8 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
||||||
|
|
||||||
**-s**\ *shell*,\ **--shell=***shell*
|
**-s**\ *shell*,\ **--shell=***shell*
|
||||||
|
|
||||||
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
|
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash*, *ksh*,
|
||||||
|
and *busybox*.
|
||||||
The default is to deduce the shell from the file's `shell` directive,
|
The default is to deduce the shell from the file's `shell` directive,
|
||||||
shebang, or `.bash/.bats/.dash/.ksh` extension, in that order. *sh* refers to
|
shebang, or `.bash/.bats/.dash/.ksh` extension, in that order. *sh* refers to
|
||||||
POSIX `sh` (not the system's), and will warn of portability issues.
|
POSIX `sh` (not the system's), and will warn of portability issues.
|
||||||
|
@ -112,6 +125,9 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
||||||
line (plus `/dev/null`). This option allows following any file the script
|
line (plus `/dev/null`). This option allows following any file the script
|
||||||
may `source`.
|
may `source`.
|
||||||
|
|
||||||
|
This option may also be enabled using `external-sources=true` in
|
||||||
|
`.shellcheckrc`. This flag takes precedence.
|
||||||
|
|
||||||
**FILES...**
|
**FILES...**
|
||||||
|
|
||||||
: One or more script files to check, or "-" for standard input.
|
: One or more script files to check, or "-" for standard input.
|
||||||
|
@ -232,12 +248,28 @@ Valid keys are:
|
||||||
**disable**
|
**disable**
|
||||||
: Disables a comma separated list of error codes for the following command.
|
: 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
|
The command can be a simple command like `echo foo`, or a compound command
|
||||||
like a function definition, subshell block or loop.
|
like a function definition, subshell block or loop. A range can be
|
||||||
|
be specified with a dash, e.g. `disable=SC3000-SC4000` to exclude 3xxx.
|
||||||
|
All warnings can be disabled with `disable=all`.
|
||||||
|
|
||||||
**enable**
|
**enable**
|
||||||
: Enable an optional check by name, as listed with **--list-optional**.
|
: Enable an optional check by name, as listed with **--list-optional**.
|
||||||
Only file-wide `enable` directives are considered.
|
Only file-wide `enable` directives are considered.
|
||||||
|
|
||||||
|
**extended-analysis**
|
||||||
|
: Set to true/false to enable/disable dataflow analysis. Specifying
|
||||||
|
`# shellcheck extended-analysis=false` in particularly large (2000+ line)
|
||||||
|
auto-generated scripts will reduce ShellCheck's resource usage at the
|
||||||
|
expense of certain checks. Extended analysis is enabled by default.
|
||||||
|
|
||||||
|
**external-sources**
|
||||||
|
: Set to `true` in `.shellcheckrc` to always allow ShellCheck to open
|
||||||
|
arbitrary files from 'source' statements (the way most tools do).
|
||||||
|
|
||||||
|
This option defaults to `false` only due to ShellCheck's origin as a
|
||||||
|
remote service for checking untrusted scripts. It can safely be enabled
|
||||||
|
for normal development.
|
||||||
|
|
||||||
**source**
|
**source**
|
||||||
: Overrides the filename included by a `source`/`.` statement. This can be
|
: 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
|
used to tell shellcheck where to look for a file whose name is determined
|
||||||
|
@ -254,7 +286,7 @@ Valid keys are:
|
||||||
**shell**
|
**shell**
|
||||||
: Overrides the shell detected from the shebang. This is useful for
|
: Overrides the shell detected from the shebang. This is useful for
|
||||||
files meant to be included (and thus lacking a shebang), or possibly
|
files meant to be included (and thus lacking a shebang), or possibly
|
||||||
as a more targeted alternative to 'disable=2039'.
|
as a more targeted alternative to 'disable=SC2039'.
|
||||||
|
|
||||||
# RC FILES
|
# RC FILES
|
||||||
|
|
||||||
|
@ -269,6 +301,12 @@ Here is an example `.shellcheckrc`:
|
||||||
source-path=SCRIPTDIR
|
source-path=SCRIPTDIR
|
||||||
source-path=/mnt/chroot
|
source-path=/mnt/chroot
|
||||||
|
|
||||||
|
# Since 0.9.0, values can be quoted with '' or "" to allow spaces
|
||||||
|
source-path="My Documents/scripts"
|
||||||
|
|
||||||
|
# Allow opening any 'source'd file, even if not specified as input
|
||||||
|
external-sources=true
|
||||||
|
|
||||||
# Turn on warnings for unquoted variables with safe values
|
# Turn on warnings for unquoted variables with safe values
|
||||||
enable=quote-safe-variables
|
enable=quote-safe-variables
|
||||||
|
|
||||||
|
@ -279,7 +317,7 @@ Here is an example `.shellcheckrc`:
|
||||||
disable=SC2236
|
disable=SC2236
|
||||||
|
|
||||||
If no `.shellcheckrc` is found in any of the parent directories, ShellCheck
|
If no `.shellcheckrc` is found in any of the parent directories, ShellCheck
|
||||||
will look in `~/.shellcheckrc` followed by the XDG config directory
|
will look in `~/.shellcheckrc` followed by the `$XDG_CONFIG_HOME`
|
||||||
(usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on
|
(usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on
|
||||||
Windows. Only the first file found will be used.
|
Windows. Only the first file found will be used.
|
||||||
|
|
||||||
|
@ -319,10 +357,32 @@ locales where encoding is unspecified (such as the `C` locale).
|
||||||
Windows users seeing `commitBuffer: invalid argument (invalid character)`
|
Windows users seeing `commitBuffer: invalid argument (invalid character)`
|
||||||
should set their terminal to use UTF-8 with `chcp 65001`.
|
should set their terminal to use UTF-8 with `chcp 65001`.
|
||||||
|
|
||||||
# AUTHORS
|
# KNOWN INCOMPATIBILITIES
|
||||||
|
|
||||||
ShellCheck is developed and maintained by Vidar Holen, with assistance from a
|
(If nothing in this section makes sense, you are unlikely to be affected by it)
|
||||||
long list of wonderful contributors.
|
|
||||||
|
To avoid confusing and misguided suggestions, ShellCheck requires function
|
||||||
|
bodies to be either `{ brace groups; }` or `( subshells )`, and function names
|
||||||
|
containing `[]*=!` are only recognized after a `function` keyword.
|
||||||
|
|
||||||
|
The following unconventional function definitions are identical in Bash,
|
||||||
|
but ShellCheck only recognizes the latter.
|
||||||
|
|
||||||
|
[x!=y] () [[ $1 ]]
|
||||||
|
function [x!=y] () { [[ $1 ]]; }
|
||||||
|
|
||||||
|
Shells without the `function` keyword do not allow these characters in function
|
||||||
|
names to begin with. Function names containing `{}` are not supported at all.
|
||||||
|
|
||||||
|
Further, if ShellCheck sees `[x!=y]` it will assume this is an invalid
|
||||||
|
comparison. To invoke the above function, quote the command as in `'[x!=y]'`,
|
||||||
|
or to retain the same globbing behavior, use `command [x!=y]`.
|
||||||
|
|
||||||
|
ShellCheck imposes additional restrictions on the `[` command to help diagnose
|
||||||
|
common invalid uses. While `[ $x= 1 ]` is defined in POSIX, ShellCheck will
|
||||||
|
assume it was intended as the much more likely comparison `[ "$x" = 1 ]` and
|
||||||
|
fail accordingly. For unconventional or dynamic uses of the `[` command, use
|
||||||
|
`test` or `\[` instead.
|
||||||
|
|
||||||
# REPORTING BUGS
|
# REPORTING BUGS
|
||||||
|
|
||||||
|
@ -330,12 +390,17 @@ Bugs and issues can be reported on GitHub:
|
||||||
|
|
||||||
https://github.com/koalaman/shellcheck/issues
|
https://github.com/koalaman/shellcheck/issues
|
||||||
|
|
||||||
|
# AUTHORS
|
||||||
|
|
||||||
|
ShellCheck is developed and maintained by Vidar Holen, with assistance from a
|
||||||
|
long list of wonderful contributors.
|
||||||
|
|
||||||
# COPYRIGHT
|
# COPYRIGHT
|
||||||
|
|
||||||
Copyright 2012-2019, Vidar Holen and contributors.
|
Copyright 2012-2024, Vidar Holen and contributors.
|
||||||
Licensed under the GNU General Public License version 3 or later,
|
Licensed under the GNU General Public License version 3 or later,
|
||||||
see https://gnu.org/licenses/gpl.html
|
see https://gnu.org/licenses/gpl.html
|
||||||
|
|
||||||
# SEE ALSO
|
# SEE ALSO
|
||||||
|
|
||||||
sh(1) bash(1)
|
sh(1) bash(1) dash(1) ksh(1)
|
||||||
|
|
|
@ -34,6 +34,8 @@ import qualified ShellCheck.Formatter.Quiet
|
||||||
|
|
||||||
import Control.Exception
|
import Control.Exception
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
|
import Control.Monad.IO.Class
|
||||||
|
import Control.Monad.Trans.Class
|
||||||
import Control.Monad.Except
|
import Control.Monad.Except
|
||||||
import Data.Bits
|
import Data.Bits
|
||||||
import Data.Char
|
import Data.Char
|
||||||
|
@ -74,7 +76,8 @@ data Options = Options {
|
||||||
externalSources :: Bool,
|
externalSources :: Bool,
|
||||||
sourcePaths :: [FilePath],
|
sourcePaths :: [FilePath],
|
||||||
formatterOptions :: FormatterOptions,
|
formatterOptions :: FormatterOptions,
|
||||||
minSeverity :: Severity
|
minSeverity :: Severity,
|
||||||
|
rcfile :: Maybe FilePath
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultOptions = Options {
|
defaultOptions = Options {
|
||||||
|
@ -84,7 +87,8 @@ defaultOptions = Options {
|
||||||
formatterOptions = newFormatterOptions {
|
formatterOptions = newFormatterOptions {
|
||||||
foColorOption = ColorAuto
|
foColorOption = ColorAuto
|
||||||
},
|
},
|
||||||
minSeverity = StyleC
|
minSeverity = StyleC,
|
||||||
|
rcfile = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
|
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
|
||||||
|
@ -98,6 +102,8 @@ options = [
|
||||||
(ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings",
|
(ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings",
|
||||||
Option "e" ["exclude"]
|
Option "e" ["exclude"]
|
||||||
(ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings",
|
(ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings",
|
||||||
|
Option "" ["extended-analysis"]
|
||||||
|
(ReqArg (Flag "extended-analysis") "bool") "Perform dataflow analysis (default true)",
|
||||||
Option "f" ["format"]
|
Option "f" ["format"]
|
||||||
(ReqArg (Flag "format") "FORMAT") $
|
(ReqArg (Flag "format") "FORMAT") $
|
||||||
"Output format (" ++ formatList ++ ")",
|
"Output format (" ++ formatList ++ ")",
|
||||||
|
@ -105,6 +111,9 @@ options = [
|
||||||
(NoArg $ Flag "list-optional" "true") "List checks disabled by default",
|
(NoArg $ Flag "list-optional" "true") "List checks disabled by default",
|
||||||
Option "" ["norc"]
|
Option "" ["norc"]
|
||||||
(NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc files",
|
(NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc files",
|
||||||
|
Option "" ["rcfile"]
|
||||||
|
(ReqArg (Flag "rcfile") "RCFILE")
|
||||||
|
"Prefer the specified configuration file over searching for one",
|
||||||
Option "o" ["enable"]
|
Option "o" ["enable"]
|
||||||
(ReqArg (Flag "enable") "check1,check2..")
|
(ReqArg (Flag "enable") "check1,check2..")
|
||||||
"List of optional checks to enable (or 'all')",
|
"List of optional checks to enable (or 'all')",
|
||||||
|
@ -113,7 +122,7 @@ options = [
|
||||||
"Specify path when looking for sourced files (\"SCRIPTDIR\" for script's dir)",
|
"Specify path when looking for sourced files (\"SCRIPTDIR\" for script's dir)",
|
||||||
Option "s" ["shell"]
|
Option "s" ["shell"]
|
||||||
(ReqArg (Flag "shell") "SHELLNAME")
|
(ReqArg (Flag "shell") "SHELLNAME")
|
||||||
"Specify dialect (sh, bash, dash, ksh)",
|
"Specify dialect (sh, bash, dash, ksh, busybox)",
|
||||||
Option "S" ["severity"]
|
Option "S" ["severity"]
|
||||||
(ReqArg (Flag "severity") "SEVERITY")
|
(ReqArg (Flag "severity") "SEVERITY")
|
||||||
"Minimum severity of errors to consider (error, warning, info, style)",
|
"Minimum severity of errors to consider (error, warning, info, style)",
|
||||||
|
@ -225,7 +234,7 @@ runFormatter sys format options files = do
|
||||||
f :: Status -> FilePath -> IO Status
|
f :: Status -> FilePath -> IO Status
|
||||||
f status file = do
|
f status file = do
|
||||||
newStatus <- process file `catch` handler file
|
newStatus <- process file `catch` handler file
|
||||||
return $ status `mappend` newStatus
|
return $! status `mappend` newStatus
|
||||||
handler :: FilePath -> IOException -> IO Status
|
handler :: FilePath -> IOException -> IO Status
|
||||||
handler file e = reportFailure file (show e)
|
handler file e = reportFailure file (show e)
|
||||||
reportFailure file str = do
|
reportFailure file str = do
|
||||||
|
@ -234,7 +243,7 @@ runFormatter sys format options files = do
|
||||||
|
|
||||||
process :: FilePath -> IO Status
|
process :: FilePath -> IO Status
|
||||||
process filename = do
|
process filename = do
|
||||||
input <- siReadFile sys filename
|
input <- siReadFile sys Nothing filename
|
||||||
either (reportFailure filename) check input
|
either (reportFailure filename) check input
|
||||||
where
|
where
|
||||||
check contents = do
|
check contents = do
|
||||||
|
@ -250,9 +259,9 @@ runFormatter sys format options files = do
|
||||||
else SomeProblems
|
else SomeProblems
|
||||||
|
|
||||||
parseEnum name value list =
|
parseEnum name value list =
|
||||||
case filter ((== value) . fst) list of
|
case lookup value list of
|
||||||
[(name, value)] -> return value
|
Just value -> return value
|
||||||
[] -> do
|
Nothing -> do
|
||||||
printErr $ "Unknown value for --" ++ name ++ ". " ++
|
printErr $ "Unknown value for --" ++ name ++ ". " ++
|
||||||
"Valid options are: " ++ (intercalate ", " $ map fst list)
|
"Valid options are: " ++ (intercalate ", " $ map fst list)
|
||||||
throwError SupportFailure
|
throwError SupportFailure
|
||||||
|
@ -365,6 +374,11 @@ parseOption flag options =
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Flag "rcfile" str -> do
|
||||||
|
return options {
|
||||||
|
rcfile = Just str
|
||||||
|
}
|
||||||
|
|
||||||
Flag "enable" value ->
|
Flag "enable" value ->
|
||||||
let cs = checkSpec options in return options {
|
let cs = checkSpec options in return options {
|
||||||
checkSpec = cs {
|
checkSpec = cs {
|
||||||
|
@ -372,6 +386,14 @@ parseOption flag options =
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Flag "extended-analysis" str -> do
|
||||||
|
value <- parseBool str
|
||||||
|
return options {
|
||||||
|
checkSpec = (checkSpec options) {
|
||||||
|
csExtendedAnalysis = Just value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
-- This flag is handled specially in 'process'
|
-- This flag is handled specially in 'process'
|
||||||
Flag "format" _ -> return options
|
Flag "format" _ -> return options
|
||||||
|
|
||||||
|
@ -389,11 +411,20 @@ parseOption flag options =
|
||||||
throwError SyntaxFailure
|
throwError SyntaxFailure
|
||||||
return (Prelude.read num :: Integer)
|
return (Prelude.read num :: Integer)
|
||||||
|
|
||||||
|
parseBool str = do
|
||||||
|
case str of
|
||||||
|
"true" -> return True
|
||||||
|
"false" -> return False
|
||||||
|
_ -> do
|
||||||
|
printErr $ "Invalid boolean, expected true/false: " ++ str
|
||||||
|
throwError SyntaxFailure
|
||||||
|
|
||||||
|
ioInterface :: Options -> [FilePath] -> IO (SystemInterface IO)
|
||||||
ioInterface options files = do
|
ioInterface options files = do
|
||||||
inputs <- mapM normalize files
|
inputs <- mapM normalize files
|
||||||
cache <- newIORef emptyCache
|
cache <- newIORef emptyCache
|
||||||
configCache <- newIORef ("", Nothing)
|
configCache <- newIORef ("", Nothing)
|
||||||
return SystemInterface {
|
return (newSystemInterface :: SystemInterface IO) {
|
||||||
siReadFile = get cache inputs,
|
siReadFile = get cache inputs,
|
||||||
siFindSource = findSourceFile inputs (sourcePaths options),
|
siFindSource = findSourceFile inputs (sourcePaths options),
|
||||||
siGetConfig = getConfig configCache
|
siGetConfig = getConfig configCache
|
||||||
|
@ -402,14 +433,14 @@ ioInterface options files = do
|
||||||
emptyCache :: Map.Map FilePath String
|
emptyCache :: Map.Map FilePath String
|
||||||
emptyCache = Map.empty
|
emptyCache = Map.empty
|
||||||
|
|
||||||
get cache inputs file = do
|
get cache inputs rcSuggestsExternal file = do
|
||||||
map <- readIORef cache
|
map <- readIORef cache
|
||||||
case Map.lookup file map of
|
case Map.lookup file map of
|
||||||
Just x -> return $ Right x
|
Just x -> return $ Right x
|
||||||
Nothing -> fetch cache inputs file
|
Nothing -> fetch cache inputs rcSuggestsExternal file
|
||||||
|
|
||||||
fetch cache inputs file = do
|
fetch cache inputs rcSuggestsExternal file = do
|
||||||
ok <- allowable inputs file
|
ok <- allowable rcSuggestsExternal inputs file
|
||||||
if ok
|
if ok
|
||||||
then (do
|
then (do
|
||||||
(contents, shouldCache) <- inputFile file
|
(contents, shouldCache) <- inputFile file
|
||||||
|
@ -417,13 +448,16 @@ ioInterface options files = do
|
||||||
modifyIORef cache $ Map.insert file contents
|
modifyIORef cache $ Map.insert file contents
|
||||||
return $ Right contents
|
return $ Right contents
|
||||||
) `catch` handler
|
) `catch` handler
|
||||||
|
else
|
||||||
|
if rcSuggestsExternal == Just False
|
||||||
|
then return $ Left (file ++ " was not specified as input, and external files were disabled via directive.")
|
||||||
else return $ Left (file ++ " was not specified as input (see shellcheck -x).")
|
else return $ Left (file ++ " was not specified as input (see shellcheck -x).")
|
||||||
where
|
where
|
||||||
handler :: IOException -> IO (Either ErrorMessage String)
|
handler :: IOException -> IO (Either ErrorMessage String)
|
||||||
handler ex = return . Left $ show ex
|
handler ex = return . Left $ show ex
|
||||||
|
|
||||||
allowable inputs x =
|
allowable rcSuggestsExternal inputs x =
|
||||||
if externalSources options
|
if fromMaybe (externalSources options) rcSuggestsExternal
|
||||||
then return True
|
then return True
|
||||||
else do
|
else do
|
||||||
path <- normalize x
|
path <- normalize x
|
||||||
|
@ -435,8 +469,23 @@ ioInterface options files = do
|
||||||
fallback :: FilePath -> IOException -> IO FilePath
|
fallback :: FilePath -> IOException -> IO FilePath
|
||||||
fallback path _ = return path
|
fallback path _ = return path
|
||||||
|
|
||||||
|
|
||||||
-- Returns the name and contents of .shellcheckrc for the given file
|
-- Returns the name and contents of .shellcheckrc for the given file
|
||||||
getConfig cache filename = do
|
getConfig cache filename =
|
||||||
|
case rcfile options of
|
||||||
|
Just file -> do
|
||||||
|
-- We have a specified rcfile. Ignore normal rcfile resolution.
|
||||||
|
(path, result) <- readIORef cache
|
||||||
|
if path == "/"
|
||||||
|
then return result
|
||||||
|
else do
|
||||||
|
result <- readConfig file
|
||||||
|
when (isNothing result) $
|
||||||
|
hPutStrLn stderr $ "Warning: unable to read --rcfile " ++ file
|
||||||
|
writeIORef cache ("/", result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
Nothing -> do
|
||||||
path <- normalize filename
|
path <- normalize filename
|
||||||
let dir = takeDirectory path
|
let dir = takeDirectory path
|
||||||
(previousPath, result) <- readIORef cache
|
(previousPath, result) <- readIORef cache
|
||||||
|
@ -484,7 +533,7 @@ ioInterface options files = do
|
||||||
where
|
where
|
||||||
handler :: FilePath -> IOException -> IO (String, Bool)
|
handler :: FilePath -> IOException -> IO (String, Bool)
|
||||||
handler file err = do
|
handler file err = do
|
||||||
putStrLn $ file ++ ": " ++ show err
|
hPutStrLn stderr $ file ++ ": " ++ show err
|
||||||
return ("", True)
|
return ("", True)
|
||||||
|
|
||||||
andM a b arg = do
|
andM a b arg = do
|
||||||
|
@ -497,7 +546,7 @@ ioInterface options files = do
|
||||||
b <- p x
|
b <- p x
|
||||||
if b then pure (Just x) else acc
|
if b then pure (Just x) else acc
|
||||||
|
|
||||||
findSourceFile inputs sourcePathFlag currentScript sourcePathAnnotation original =
|
findSourceFile inputs sourcePathFlag currentScript rcSuggestsExternal sourcePathAnnotation original =
|
||||||
if isAbsolute original
|
if isAbsolute original
|
||||||
then
|
then
|
||||||
let (_, relative) = splitDrive original
|
let (_, relative) = splitDrive original
|
||||||
|
@ -506,8 +555,8 @@ ioInterface options files = do
|
||||||
find original original
|
find original original
|
||||||
where
|
where
|
||||||
find filename deflt = do
|
find filename deflt = do
|
||||||
sources <- findM ((allowable inputs) `andM` doesFileExist) $
|
sources <- findM ((allowable rcSuggestsExternal inputs) `andM` doesFileExist) $
|
||||||
(adjustPath filename):(map (</> filename) $ map adjustPath $ sourcePathFlag ++ sourcePathAnnotation)
|
(adjustPath filename):(map ((</> filename) . adjustPath) $ sourcePathFlag ++ sourcePathAnnotation)
|
||||||
case sources of
|
case sources of
|
||||||
Nothing -> return deflt
|
Nothing -> return deflt
|
||||||
Just first -> return first
|
Just first -> return first
|
||||||
|
|
|
@ -23,7 +23,7 @@ description: |
|
||||||
# snap connect shellcheck:removable-media
|
# snap connect shellcheck:removable-media
|
||||||
|
|
||||||
version: git
|
version: git
|
||||||
base: core18
|
base: core20
|
||||||
grade: stable
|
grade: stable
|
||||||
confinement: strict
|
confinement: strict
|
||||||
|
|
||||||
|
@ -31,6 +31,8 @@ apps:
|
||||||
shellcheck:
|
shellcheck:
|
||||||
command: usr/bin/shellcheck
|
command: usr/bin/shellcheck
|
||||||
plugs: [home, removable-media]
|
plugs: [home, removable-media]
|
||||||
|
environment:
|
||||||
|
LANG: C.UTF-8
|
||||||
|
|
||||||
parts:
|
parts:
|
||||||
shellcheck:
|
shellcheck:
|
||||||
|
@ -38,16 +40,16 @@ parts:
|
||||||
source: .
|
source: .
|
||||||
build-packages:
|
build-packages:
|
||||||
- cabal-install
|
- cabal-install
|
||||||
- squid
|
stage-packages:
|
||||||
|
- libatomic1
|
||||||
override-build: |
|
override-build: |
|
||||||
# See comments in .snapsquid.conf
|
# Give ourselves enough memory to build
|
||||||
[ "$http_proxy" ] && {
|
dd if=/dev/zero of=/tmp/swap bs=1M count=2000
|
||||||
squid3 -f .snapsquid.conf
|
mkswap /tmp/swap
|
||||||
export http_proxy="http://localhost:8888"
|
swapon /tmp/swap
|
||||||
sleep 3
|
|
||||||
}
|
|
||||||
cabal sandbox init
|
cabal sandbox init
|
||||||
cabal update || cat /var/log/squid/*
|
cabal update
|
||||||
cabal install -j
|
cabal install -j
|
||||||
|
|
||||||
install -d $SNAPCRAFT_PART_INSTALL/usr/bin
|
install -d $SNAPCRAFT_PART_INSTALL/usr/bin
|
||||||
|
|
|
@ -45,6 +45,7 @@ data InnerToken t =
|
||||||
| Inner_TA_Variable String [t]
|
| Inner_TA_Variable String [t]
|
||||||
| Inner_TA_Expansion [t]
|
| Inner_TA_Expansion [t]
|
||||||
| Inner_TA_Sequence [t]
|
| Inner_TA_Sequence [t]
|
||||||
|
| Inner_TA_Parenthesis t
|
||||||
| Inner_TA_Trinary t t t
|
| Inner_TA_Trinary t t t
|
||||||
| Inner_TA_Unary String t
|
| Inner_TA_Unary String t
|
||||||
| Inner_TC_And ConditionType String t t
|
| Inner_TC_And ConditionType String t t
|
||||||
|
@ -137,19 +138,21 @@ data InnerToken t =
|
||||||
| Inner_T_WhileExpression [t] [t]
|
| Inner_T_WhileExpression [t] [t]
|
||||||
| Inner_T_Annotation [Annotation] t
|
| Inner_T_Annotation [Annotation] t
|
||||||
| Inner_T_Pipe String
|
| Inner_T_Pipe String
|
||||||
| Inner_T_CoProc (Maybe String) t
|
| Inner_T_CoProc (Maybe Token) t
|
||||||
| Inner_T_CoProcBody t
|
| Inner_T_CoProcBody t
|
||||||
| Inner_T_Include t
|
| Inner_T_Include t
|
||||||
| Inner_T_SourceCommand t t
|
| Inner_T_SourceCommand t t
|
||||||
| Inner_T_BatsTest t t
|
| Inner_T_BatsTest String t
|
||||||
deriving (Show, Eq, Functor, Foldable, Traversable)
|
deriving (Show, Eq, Functor, Foldable, Traversable)
|
||||||
|
|
||||||
data Annotation =
|
data Annotation =
|
||||||
DisableComment Integer
|
DisableComment Integer Integer -- [from, to)
|
||||||
| EnableComment String
|
| EnableComment String
|
||||||
| SourceOverride String
|
| SourceOverride String
|
||||||
| ShellOverride String
|
| ShellOverride String
|
||||||
| SourcePath String
|
| SourcePath String
|
||||||
|
| ExternalSources Bool
|
||||||
|
| ExtendedAnalysis Bool
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
|
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
|
||||||
|
|
||||||
|
@ -203,6 +206,7 @@ pattern T_Annotation id anns t = OuterToken id (Inner_T_Annotation anns t)
|
||||||
pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c)
|
pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c)
|
||||||
pattern T_Array id t = OuterToken id (Inner_T_Array t)
|
pattern T_Array id t = OuterToken id (Inner_T_Array t)
|
||||||
pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)
|
pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)
|
||||||
|
pattern TA_Parenthesis id t = OuterToken id (Inner_TA_Parenthesis t)
|
||||||
pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value)
|
pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value)
|
||||||
pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3)
|
pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3)
|
||||||
pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1)
|
pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1)
|
||||||
|
@ -255,7 +259,7 @@ pattern T_Subshell id l = OuterToken id (Inner_T_Subshell l)
|
||||||
pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l)
|
pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l)
|
||||||
pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l)
|
pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l)
|
||||||
|
|
||||||
{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-}
|
{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, TA_Parenthesis, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-}
|
||||||
|
|
||||||
instance Eq Token where
|
instance Eq Token where
|
||||||
OuterToken _ a == OuterToken _ b = a == b
|
OuterToken _ a == OuterToken _ b = a == b
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{-
|
{-
|
||||||
Copyright 2012-2019 Vidar Holen
|
Copyright 2012-2021 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
https://www.shellcheck.net
|
https://www.shellcheck.net
|
||||||
|
@ -17,9 +17,12 @@
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
module ShellCheck.ASTLib where
|
module ShellCheck.ASTLib where
|
||||||
|
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
|
import ShellCheck.Prelude
|
||||||
|
import ShellCheck.Regex
|
||||||
|
|
||||||
import Control.Monad.Writer
|
import Control.Monad.Writer
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
|
@ -28,6 +31,13 @@ import Data.Functor
|
||||||
import Data.Functor.Identity
|
import Data.Functor.Identity
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
|
import qualified Data.List.NonEmpty as NE
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import Numeric (showHex)
|
||||||
|
|
||||||
|
import Test.QuickCheck
|
||||||
|
|
||||||
|
arguments (T_SimpleCommand _ _ (cmd:args)) = args
|
||||||
|
|
||||||
-- Is this a type of loop?
|
-- Is this a type of loop?
|
||||||
isLoop t = case t of
|
isLoop t = case t of
|
||||||
|
@ -51,10 +61,28 @@ willSplit x =
|
||||||
T_NormalWord _ l -> any willSplit l
|
T_NormalWord _ l -> any willSplit l
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
isGlob T_Extglob {} = True
|
isGlob t = case t of
|
||||||
isGlob T_Glob {} = True
|
T_Extglob {} -> True
|
||||||
isGlob (T_NormalWord _ l) = any isGlob l
|
T_Glob {} -> True
|
||||||
isGlob _ = False
|
T_NormalWord _ l -> any isGlob l || hasSplitRange l
|
||||||
|
_ -> False
|
||||||
|
where
|
||||||
|
-- foo[x${var}y] gets parsed as foo,[,x,$var,y],
|
||||||
|
-- so check if there's such an interval
|
||||||
|
hasSplitRange l =
|
||||||
|
let afterBracket = dropWhile (not . isHalfOpenRange) l
|
||||||
|
in any isClosingRange afterBracket
|
||||||
|
|
||||||
|
isHalfOpenRange t =
|
||||||
|
case t of
|
||||||
|
T_Literal _ "[" -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
isClosingRange t =
|
||||||
|
case t of
|
||||||
|
T_Literal _ str -> ']' `elem` str
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
|
||||||
-- Is this shell word a constant?
|
-- Is this shell word a constant?
|
||||||
isConstant token =
|
isConstant token =
|
||||||
|
@ -112,7 +140,7 @@ getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
|
||||||
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
|
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
|
||||||
flag (x, '-':args) = map (\v -> (x, [v])) args
|
flag (x, '-':args) = map (\v -> (x, [v])) args
|
||||||
flag (x, _) = [ (x, "") ]
|
flag (x, _) = [ (x, "") ]
|
||||||
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
|
getFlagsUntil _ _ = error $ pleaseReport "getFlags on non-command"
|
||||||
|
|
||||||
-- Get all flags in a GNU way, up until --
|
-- Get all flags in a GNU way, up until --
|
||||||
getAllFlags :: Token -> [(Token, String)]
|
getAllFlags :: Token -> [(Token, String)]
|
||||||
|
@ -130,30 +158,146 @@ isFlag token =
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
-- Is this token a flag where the - is unquoted?
|
-- Is this token a flag where the - is unquoted?
|
||||||
isUnquotedFlag token = fromMaybe False $ do
|
isUnquotedFlag token =
|
||||||
str <- getLeadingUnquotedString token
|
case getLeadingUnquotedString token of
|
||||||
return $ "-" `isPrefixOf` str
|
Just ('-':_) -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
-- getGnuOpts "erd:u:" will parse a list of arguments tokens like `read`
|
||||||
|
-- -re -d : -u 3 bar
|
||||||
|
-- into
|
||||||
|
-- Just [("r", (-re, -re)), ("e", (-re, -re)), ("d", (-d,:)), ("u", (-u,3)), ("", (bar,bar))]
|
||||||
|
--
|
||||||
|
-- Each string flag maps to a tuple of (flag, argument), where argument=flag if it
|
||||||
|
-- doesn't take a specific one.
|
||||||
|
--
|
||||||
|
-- Any unrecognized flag will result in Nothing. The exception is if arbitraryLongOpts
|
||||||
|
-- is set, in which case --anything will map to "anything".
|
||||||
|
getGnuOpts :: String -> [Token] -> Maybe [(String, (Token, Token))]
|
||||||
|
getGnuOpts str args = getOpts (True, False) str [] args
|
||||||
|
|
||||||
|
-- As above, except the first non-arg string will treat the rest as arguments
|
||||||
|
getBsdOpts :: String -> [Token] -> Maybe [(String, (Token, Token))]
|
||||||
|
getBsdOpts str args = getOpts (False, False) str [] args
|
||||||
|
|
||||||
|
-- Tests for this are in Commands.hs where it's more frequently used
|
||||||
|
getOpts ::
|
||||||
|
-- Behavioral config: gnu style, allow arbitrary long options
|
||||||
|
(Bool, Bool)
|
||||||
|
-- A getopts style string
|
||||||
|
-> String
|
||||||
|
-- List of long options and whether they take arguments
|
||||||
|
-> [(String, Bool)]
|
||||||
|
-- List of arguments (excluding command)
|
||||||
|
-> [Token]
|
||||||
|
-- List of flags to tuple of (optionToken, valueToken)
|
||||||
|
-> Maybe [(String, (Token, Token))]
|
||||||
|
|
||||||
|
getOpts (gnu, arbitraryLongOpts) string longopts args = process args
|
||||||
|
where
|
||||||
|
flagList (c:':':rest) = ([c], True) : flagList rest
|
||||||
|
flagList (c:rest) = ([c], False) : flagList rest
|
||||||
|
flagList [] = longopts
|
||||||
|
flagMap = Map.fromList $ ("", False) : flagList string
|
||||||
|
|
||||||
|
process [] = return []
|
||||||
|
process (token:rest) = do
|
||||||
|
case getLiteralStringDef "\0" token of
|
||||||
|
"--" -> return $ listToArgs rest
|
||||||
|
'-':'-':word -> do
|
||||||
|
let (name, arg) = span (/= '=') word
|
||||||
|
needsArg <-
|
||||||
|
if arbitraryLongOpts
|
||||||
|
then return $ Map.findWithDefault False name flagMap
|
||||||
|
else Map.lookup name flagMap
|
||||||
|
|
||||||
|
if needsArg && null arg
|
||||||
|
then
|
||||||
|
case rest of
|
||||||
|
(arg:rest2) -> do
|
||||||
|
more <- process rest2
|
||||||
|
return $ (name, (token, arg)) : more
|
||||||
|
_ -> fail "Missing arg"
|
||||||
|
else do
|
||||||
|
more <- process rest
|
||||||
|
-- Consider splitting up token to get arg
|
||||||
|
return $ (name, (token, token)) : more
|
||||||
|
'-':opts -> shortToOpts opts token rest
|
||||||
|
arg ->
|
||||||
|
if gnu
|
||||||
|
then do
|
||||||
|
more <- process rest
|
||||||
|
return $ ("", (token, token)):more
|
||||||
|
else return $ listToArgs (token:rest)
|
||||||
|
|
||||||
|
shortToOpts opts token args =
|
||||||
|
case opts of
|
||||||
|
c:rest -> do
|
||||||
|
needsArg <- Map.lookup [c] flagMap
|
||||||
|
case () of
|
||||||
|
_ | needsArg && null rest -> do
|
||||||
|
(next:restArgs) <- return args
|
||||||
|
more <- process restArgs
|
||||||
|
return $ ([c], (token, next)):more
|
||||||
|
_ | needsArg -> do
|
||||||
|
more <- process args
|
||||||
|
return $ ([c], (token, token)):more
|
||||||
|
_ -> do
|
||||||
|
more <- shortToOpts rest token args
|
||||||
|
return $ ([c], (token, token)):more
|
||||||
|
[] -> process args
|
||||||
|
|
||||||
|
listToArgs = map (\x -> ("", (x, x)))
|
||||||
|
|
||||||
|
|
||||||
|
-- Generic getOpts that doesn't rely on a format string, but may also be inaccurate.
|
||||||
|
-- This provides a best guess interpretation instead of failing when new options are added.
|
||||||
|
--
|
||||||
|
-- "--" is treated as end of arguments
|
||||||
|
-- "--anything[=foo]" is treated as a long option without argument
|
||||||
|
-- "-any" is treated as -a -n -y, with the next arg as an option to -y unless it starts with -
|
||||||
|
-- anything else is an argument
|
||||||
|
getGenericOpts :: [Token] -> [(String, (Token, Token))]
|
||||||
|
getGenericOpts = process
|
||||||
|
where
|
||||||
|
process (token:rest) =
|
||||||
|
case getLiteralStringDef "\0" token of
|
||||||
|
"--" -> map (\c -> ("", (c,c))) rest
|
||||||
|
'-':'-':word -> (takeWhile (`notElem` "\0=") word, (token, token)) : process rest
|
||||||
|
'-':optString ->
|
||||||
|
let opts = takeWhile (/= '\0') optString
|
||||||
|
in
|
||||||
|
case rest of
|
||||||
|
next:_ | "-" `isPrefixOf` getLiteralStringDef "\0" next ->
|
||||||
|
map (\c -> ([c], (token, token))) opts ++ process rest
|
||||||
|
next:remainder ->
|
||||||
|
case reverse opts of
|
||||||
|
last:initial ->
|
||||||
|
map (\c -> ([c], (token, token))) (reverse initial)
|
||||||
|
++ [([last], (token, next))]
|
||||||
|
++ process remainder
|
||||||
|
[] -> process remainder
|
||||||
|
[] -> map (\c -> ([c], (token, token))) opts
|
||||||
|
_ -> ("", (token, token)) : process rest
|
||||||
|
process [] = []
|
||||||
|
|
||||||
-- 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?
|
-- Is this an expansion of multiple items of an array?
|
||||||
isArrayExpansion t@(T_DollarBraced _ _ _) =
|
isArrayExpansion (T_DollarBraced _ _ l) =
|
||||||
let string = bracedString t in
|
let string = concat $ oversimplify l in
|
||||||
"@" `isPrefixOf` string ||
|
"@" `isPrefixOf` string ||
|
||||||
not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string
|
not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string
|
||||||
isArrayExpansion _ = False
|
isArrayExpansion _ = False
|
||||||
|
|
||||||
-- Is it possible that this arg becomes multiple args?
|
-- Is it possible that this arg becomes multiple args?
|
||||||
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
|
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f False t
|
||||||
where
|
where
|
||||||
f t@(T_DollarBraced _ _ _) =
|
f quoted (T_DollarBraced _ _ l) =
|
||||||
let string = bracedString t in
|
let string = concat $ oversimplify l in
|
||||||
"!" `isPrefixOf` string
|
not quoted || "!" `isPrefixOf` string
|
||||||
f (T_DoubleQuoted _ parts) = any f parts
|
f quoted (T_DoubleQuoted _ parts) = any (f True) parts
|
||||||
f (T_NormalWord _ parts) = any f parts
|
f quoted (T_NormalWord _ parts) = any (f quoted) parts
|
||||||
f _ = False
|
f _ _ = False
|
||||||
|
|
||||||
-- Is it certain that this word will becomes multiple words?
|
-- Is it certain that this word will becomes multiple words?
|
||||||
willBecomeMultipleArgs t = willConcatInAssignment t || f t
|
willBecomeMultipleArgs t = willConcatInAssignment t || f t
|
||||||
|
@ -161,7 +305,6 @@ willBecomeMultipleArgs t = willConcatInAssignment t || f t
|
||||||
f T_Extglob {} = True
|
f T_Extglob {} = True
|
||||||
f T_Glob {} = True
|
f T_Glob {} = True
|
||||||
f T_BraceExpansion {} = True
|
f T_BraceExpansion {} = True
|
||||||
f (T_DoubleQuoted _ parts) = any f parts
|
|
||||||
f (T_NormalWord _ parts) = any f parts
|
f (T_NormalWord _ parts) = any f parts
|
||||||
f _ = False
|
f _ = False
|
||||||
|
|
||||||
|
@ -193,6 +336,12 @@ getUnquotedLiteral (T_NormalWord _ list) =
|
||||||
str _ = Nothing
|
str _ = Nothing
|
||||||
getUnquotedLiteral _ = Nothing
|
getUnquotedLiteral _ = Nothing
|
||||||
|
|
||||||
|
isQuotes t =
|
||||||
|
case t of
|
||||||
|
T_DoubleQuoted {} -> True
|
||||||
|
T_SingleQuoted {} -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
-- Get the last unquoted T_Literal in a word like "${var}foo"THIS
|
-- 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.
|
-- or nothing if the word does not end in an unquoted literal.
|
||||||
getTrailingUnquotedLiteral :: Token -> Maybe Token
|
getTrailingUnquotedLiteral :: Token -> Maybe Token
|
||||||
|
@ -211,8 +360,11 @@ getTrailingUnquotedLiteral t =
|
||||||
getLeadingUnquotedString :: Token -> Maybe String
|
getLeadingUnquotedString :: Token -> Maybe String
|
||||||
getLeadingUnquotedString t =
|
getLeadingUnquotedString t =
|
||||||
case t of
|
case t of
|
||||||
T_NormalWord _ ((T_Literal _ s) : _) -> return s
|
T_NormalWord _ ((T_Literal _ s) : rest) -> return $ s ++ from rest
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
where
|
||||||
|
from ((T_Literal _ s):rest) = s ++ from rest
|
||||||
|
from _ = ""
|
||||||
|
|
||||||
-- Maybe get the literal string of this token and any globs in it.
|
-- Maybe get the literal string of this token and any globs in it.
|
||||||
getGlobOrLiteralString = getLiteralStringExt f
|
getGlobOrLiteralString = getLiteralStringExt f
|
||||||
|
@ -220,6 +372,21 @@ getGlobOrLiteralString = getLiteralStringExt f
|
||||||
f (T_Glob _ str) = return str
|
f (T_Glob _ str) = return str
|
||||||
f _ = Nothing
|
f _ = Nothing
|
||||||
|
|
||||||
|
|
||||||
|
prop_getLiteralString1 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x01") == Just "\1"
|
||||||
|
prop_getLiteralString2 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\xyz") == Just "\\xyz"
|
||||||
|
prop_getLiteralString3 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x1") == Just "\x1"
|
||||||
|
prop_getLiteralString4 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x1y") == Just "\x1y"
|
||||||
|
prop_getLiteralString5 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\xy") == Just "\\xy"
|
||||||
|
prop_getLiteralString6 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x") == Just "\\x"
|
||||||
|
prop_getLiteralString7 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1x") == Just "\1x"
|
||||||
|
prop_getLiteralString8 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\12x") == Just "\o12x"
|
||||||
|
prop_getLiteralString9 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\123x") == Just "\o123x"
|
||||||
|
prop_getLiteralString10 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1234") == Just "\o123\&4"
|
||||||
|
prop_getLiteralString11 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1") == Just "\1"
|
||||||
|
prop_getLiteralString12 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\12") == Just "\o12"
|
||||||
|
prop_getLiteralString13 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\123") == Just "\o123"
|
||||||
|
|
||||||
-- Maybe get the literal value of a token, using a custom function
|
-- Maybe get the literal value of a token, using a custom function
|
||||||
-- to map unrecognized Tokens into strings.
|
-- to map unrecognized Tokens into strings.
|
||||||
getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String
|
getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String
|
||||||
|
@ -252,14 +419,15 @@ getLiteralStringExt more = g
|
||||||
'\\' -> '\\' : rest
|
'\\' -> '\\' : rest
|
||||||
'x' ->
|
'x' ->
|
||||||
case cs of
|
case cs of
|
||||||
(x:y:more) ->
|
(x:y:more) | isHexDigit x && isHexDigit y ->
|
||||||
if isHexDigit x && isHexDigit y
|
chr (16*(digitToInt x) + (digitToInt y)) : decodeEscapes more
|
||||||
then chr (16*(digitToInt x) + (digitToInt y)) : rest
|
(x:more) | isHexDigit x ->
|
||||||
else '\\':c:rest
|
chr (digitToInt x) : decodeEscapes more
|
||||||
|
more -> '\\' : 'x' : decodeEscapes more
|
||||||
_ | isOctDigit c ->
|
_ | isOctDigit c ->
|
||||||
let digits = take 3 $ takeWhile isOctDigit (c:cs)
|
let (digits, more) = spanMax isOctDigit 3 (c:cs)
|
||||||
num = parseOct digits
|
num = (parseOct digits) `mod` 256
|
||||||
in (if num < 256 then chr num else '?') : rest
|
in (chr num) : decodeEscapes more
|
||||||
_ -> '\\' : c : rest
|
_ -> '\\' : c : rest
|
||||||
where
|
where
|
||||||
rest = decodeEscapes cs
|
rest = decodeEscapes cs
|
||||||
|
@ -267,12 +435,54 @@ getLiteralStringExt more = g
|
||||||
where
|
where
|
||||||
f n "" = n
|
f n "" = n
|
||||||
f n (c:rest) = f (n * 8 + digitToInt c) rest
|
f n (c:rest) = f (n * 8 + digitToInt c) rest
|
||||||
|
spanMax f n list =
|
||||||
|
let (first, second) = span f list
|
||||||
|
(prefix, suffix) = splitAt n first
|
||||||
|
in
|
||||||
|
(prefix, suffix ++ second)
|
||||||
decodeEscapes (c:cs) = c : decodeEscapes cs
|
decodeEscapes (c:cs) = c : decodeEscapes cs
|
||||||
decodeEscapes [] = []
|
decodeEscapes [] = []
|
||||||
|
|
||||||
-- Is this token a string literal?
|
-- Is this token a string literal?
|
||||||
isLiteral t = isJust $ getLiteralString t
|
isLiteral t = isJust $ getLiteralString t
|
||||||
|
|
||||||
|
-- Is this token a string literal number?
|
||||||
|
isLiteralNumber t = fromMaybe False $ do
|
||||||
|
s <- getLiteralString t
|
||||||
|
guard $ all isDigit s
|
||||||
|
return True
|
||||||
|
|
||||||
|
-- Escape user data for messages.
|
||||||
|
-- Messages generally avoid repeating user data, but sometimes it's helpful.
|
||||||
|
e4m = escapeForMessage
|
||||||
|
escapeForMessage :: String -> String
|
||||||
|
escapeForMessage str = concatMap f str
|
||||||
|
where
|
||||||
|
f '\\' = "\\\\"
|
||||||
|
f '\n' = "\\n"
|
||||||
|
f '\r' = "\\r"
|
||||||
|
f '\t' = "\\t"
|
||||||
|
f '\x1B' = "\\e"
|
||||||
|
f c =
|
||||||
|
if shouldEscape c
|
||||||
|
then
|
||||||
|
if ord c < 256
|
||||||
|
then "\\x" ++ (pad0 2 $ toHex c)
|
||||||
|
else "\\U" ++ (pad0 4 $ toHex c)
|
||||||
|
else [c]
|
||||||
|
|
||||||
|
shouldEscape c =
|
||||||
|
(not $ isPrint c)
|
||||||
|
|| (not (isAscii c) && not (isLetter c))
|
||||||
|
|
||||||
|
pad0 :: Int -> String -> String
|
||||||
|
pad0 n s =
|
||||||
|
let l = length s in
|
||||||
|
if l < n
|
||||||
|
then (replicate (n-l) '0') ++ s
|
||||||
|
else s
|
||||||
|
toHex :: Char -> String
|
||||||
|
toHex c = map toUpper $ showHex (ord c) ""
|
||||||
|
|
||||||
-- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz]
|
-- 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_NormalWord _ l) = concatMap getWordParts l
|
||||||
|
@ -301,7 +511,7 @@ getCommand t =
|
||||||
|
|
||||||
-- Maybe get the command name string of a token representing a command
|
-- Maybe get the command name string of a token representing a command
|
||||||
getCommandName :: Token -> Maybe String
|
getCommandName :: Token -> Maybe String
|
||||||
getCommandName = fst . getCommandNameAndToken
|
getCommandName = fst . getCommandNameAndToken False
|
||||||
|
|
||||||
-- Maybe get the name+arguments of a command.
|
-- Maybe get the name+arguments of a command.
|
||||||
getCommandArgv t = do
|
getCommandArgv t = do
|
||||||
|
@ -311,20 +521,38 @@ getCommandArgv t = do
|
||||||
-- Get the command name token from a command, i.e.
|
-- Get the command name token from a command, i.e.
|
||||||
-- the token representing 'ls' in 'ls -la 2> foo'.
|
-- the token representing 'ls' in 'ls -la 2> foo'.
|
||||||
-- If it can't be determined, return the original token.
|
-- If it can't be determined, return the original token.
|
||||||
getCommandTokenOrThis = snd . getCommandNameAndToken
|
getCommandTokenOrThis = snd . getCommandNameAndToken False
|
||||||
|
|
||||||
getCommandNameAndToken :: Token -> (Maybe String, Token)
|
-- Given a command, get the string and token that represents the command name.
|
||||||
getCommandNameAndToken t = fromMaybe (Nothing, t) $ do
|
-- If direct, return the actual command (e.g. exec in 'exec ls')
|
||||||
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
|
-- If not, return the logical command (e.g. 'ls' in 'exec ls')
|
||||||
|
|
||||||
|
getCommandNameAndToken :: Bool -> Token -> (Maybe String, Token)
|
||||||
|
getCommandNameAndToken direct t = fromMaybe (Nothing, t) $ do
|
||||||
|
cmd@(T_SimpleCommand _ _ (w:rest)) <- getCommand t
|
||||||
s <- getLiteralString w
|
s <- getLiteralString w
|
||||||
if "busybox" `isSuffixOf` s || "builtin" == s
|
return $ fromMaybe (Just s, w) $ do
|
||||||
then
|
guard $ not direct
|
||||||
case rest of
|
actual <- getEffectiveCommandToken s cmd rest
|
||||||
(applet:_) -> return (getLiteralString applet, applet)
|
return (getLiteralString actual, actual)
|
||||||
_ -> return (Just s, w)
|
where
|
||||||
else
|
getEffectiveCommandToken str cmd args =
|
||||||
return (Just s, w)
|
let
|
||||||
|
firstArg = do
|
||||||
|
arg <- listToMaybe args
|
||||||
|
guard . not $ isFlag arg
|
||||||
|
return arg
|
||||||
|
in
|
||||||
|
case str of
|
||||||
|
"busybox" -> firstArg
|
||||||
|
"builtin" -> firstArg
|
||||||
|
"command" -> firstArg
|
||||||
|
"run" -> firstArg -- Used by bats
|
||||||
|
"exec" -> do
|
||||||
|
opts <- getBsdOpts "cla:" args
|
||||||
|
(_, (t, _)) <- find (null . fst) opts
|
||||||
|
return t
|
||||||
|
_ -> fail ""
|
||||||
|
|
||||||
-- If a command substitution is a single command, get its name.
|
-- If a command substitution is a single command, get its name.
|
||||||
-- $(date +%s) = Just "date"
|
-- $(date +%s) = Just "date"
|
||||||
|
@ -341,7 +569,7 @@ getCommandNameFromExpansion t =
|
||||||
|
|
||||||
-- Get the basename of a token representing a command
|
-- Get the basename of a token representing a command
|
||||||
getCommandBasename = fmap basename . getCommandName
|
getCommandBasename = fmap basename . getCommandName
|
||||||
where
|
|
||||||
basename = reverse . takeWhile (/= '/') . reverse
|
basename = reverse . takeWhile (/= '/') . reverse
|
||||||
|
|
||||||
isAssignment t =
|
isAssignment t =
|
||||||
|
@ -400,10 +628,10 @@ getAssociativeArrays t =
|
||||||
f t@T_SimpleCommand {} = sequence_ $ do
|
f t@T_SimpleCommand {} = sequence_ $ do
|
||||||
name <- getCommandName t
|
name <- getCommandName t
|
||||||
let assocNames = ["declare","local","typeset"]
|
let assocNames = ["declare","local","typeset"]
|
||||||
guard $ elem name assocNames
|
guard $ name `elem` assocNames
|
||||||
let flags = getAllFlags t
|
let flags = getAllFlags t
|
||||||
guard $ elem "A" $ map snd flags
|
guard $ "A" `elem` map snd flags
|
||||||
let args = map fst . filter ((==) "" . snd) $ flags
|
let args = [arg | (arg, "") <- flags]
|
||||||
let names = mapMaybe (getLiteralStringExt nameAssignments) args
|
let names = mapMaybe (getLiteralStringExt nameAssignments) args
|
||||||
return $ tell names
|
return $ tell names
|
||||||
f _ = return ()
|
f _ = return ()
|
||||||
|
@ -421,38 +649,36 @@ data PseudoGlob = PGAny | PGMany | PGChar Char
|
||||||
|
|
||||||
-- Turn a word into a PG pattern, replacing all unknown/runtime values with
|
-- Turn a word into a PG pattern, replacing all unknown/runtime values with
|
||||||
-- PGMany.
|
-- PGMany.
|
||||||
wordToPseudoGlob :: Token -> Maybe [PseudoGlob]
|
wordToPseudoGlob :: Token -> [PseudoGlob]
|
||||||
wordToPseudoGlob word =
|
wordToPseudoGlob = fromMaybe [PGMany] . wordToPseudoGlob' False
|
||||||
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
|
|
||||||
where
|
|
||||||
f x = case x of
|
|
||||||
T_Literal _ s -> return $ map PGChar s
|
|
||||||
T_SingleQuoted _ s -> return $ map PGChar s
|
|
||||||
|
|
||||||
T_DollarBraced {} -> return [PGMany]
|
|
||||||
T_DollarExpansion {} -> return [PGMany]
|
|
||||||
T_Backticked {} -> return [PGMany]
|
|
||||||
|
|
||||||
T_Glob _ "?" -> return [PGAny]
|
|
||||||
T_Glob _ ('[':_) -> return [PGAny]
|
|
||||||
T_Glob {} -> return [PGMany]
|
|
||||||
|
|
||||||
T_Extglob {} -> return [PGMany]
|
|
||||||
|
|
||||||
_ -> return [PGMany]
|
|
||||||
|
|
||||||
-- Turn a word into a PG pattern, but only if we can preserve
|
-- Turn a word into a PG pattern, but only if we can preserve
|
||||||
-- exact semantics.
|
-- exact semantics.
|
||||||
wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob]
|
wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob]
|
||||||
wordToExactPseudoGlob word =
|
wordToExactPseudoGlob = wordToPseudoGlob' True
|
||||||
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
|
|
||||||
|
wordToPseudoGlob' :: Bool -> Token -> Maybe [PseudoGlob]
|
||||||
|
wordToPseudoGlob' exact word =
|
||||||
|
simplifyPseudoGlob <$> toGlob word
|
||||||
where
|
where
|
||||||
|
toGlob :: Token -> Maybe [PseudoGlob]
|
||||||
|
toGlob word =
|
||||||
|
case word of
|
||||||
|
T_NormalWord _ (T_Literal _ ('~':str):rest) -> do
|
||||||
|
guard $ not exact
|
||||||
|
let this = (PGMany : (map PGChar $ dropWhile (/= '/') str))
|
||||||
|
tail <- concat <$> (mapM f $ concatMap getWordParts rest)
|
||||||
|
return $ this ++ tail
|
||||||
|
_ -> concat <$> (mapM f $ getWordParts word)
|
||||||
|
|
||||||
f x = case x of
|
f x = case x of
|
||||||
T_Literal _ s -> return $ map PGChar s
|
T_Literal _ s -> return $ map PGChar s
|
||||||
T_SingleQuoted _ s -> return $ map PGChar s
|
T_SingleQuoted _ s -> return $ map PGChar s
|
||||||
T_Glob _ "?" -> return [PGAny]
|
T_Glob _ "?" -> return [PGAny]
|
||||||
T_Glob _ "*" -> return [PGMany]
|
T_Glob _ "*" -> return [PGMany]
|
||||||
_ -> fail "Unknown token type"
|
T_Glob _ ('[':_) | not exact -> return [PGAny]
|
||||||
|
_ -> if exact then fail "" else return [PGMany]
|
||||||
|
|
||||||
|
|
||||||
-- Reorder a PseudoGlob for more efficient matching, e.g.
|
-- Reorder a PseudoGlob for more efficient matching, e.g.
|
||||||
-- f?*?**g -> f??*g
|
-- f?*?**g -> f??*g
|
||||||
|
@ -502,8 +728,7 @@ pseudoGlobIsSuperSetof = matchable
|
||||||
matchable (PGMany : rest) [] = matchable rest []
|
matchable (PGMany : rest) [] = matchable rest []
|
||||||
matchable _ _ = False
|
matchable _ _ = False
|
||||||
|
|
||||||
wordsCanBeEqual x y = fromMaybe True $
|
wordsCanBeEqual x y = pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
|
||||||
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
|
|
||||||
|
|
||||||
-- Is this an expansion that can be quoted,
|
-- Is this an expansion that can be quoted,
|
||||||
-- e.g. $(foo) `foo` $foo (but not {foo,})?
|
-- e.g. $(foo) `foo` $foo (but not {foo,})?
|
||||||
|
@ -517,6 +742,11 @@ isCommandSubstitution t = case t of
|
||||||
T_Backticked {} -> True
|
T_Backticked {} -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
|
-- Is this an expansion that results in a simple string?
|
||||||
|
isStringExpansion t = isCommandSubstitution t || case t of
|
||||||
|
T_DollarArithmetic {} -> True
|
||||||
|
T_DollarBraced {} -> not (isArrayExpansion t)
|
||||||
|
_ -> False
|
||||||
|
|
||||||
-- Is this a T_Annotation that ignores a specific code?
|
-- Is this a T_Annotation that ignores a specific code?
|
||||||
isAnnotationIgnoringCode code t =
|
isAnnotationIgnoringCode code t =
|
||||||
|
@ -524,5 +754,173 @@ isAnnotationIgnoringCode code t =
|
||||||
T_Annotation _ anns _ -> any hasNum anns
|
T_Annotation _ anns _ -> any hasNum anns
|
||||||
_ -> False
|
_ -> False
|
||||||
where
|
where
|
||||||
hasNum (DisableComment ts) = code == ts
|
hasNum (DisableComment from to) = code >= from && code < to
|
||||||
hasNum _ = False
|
hasNum _ = False
|
||||||
|
|
||||||
|
prop_executableFromShebang1 = executableFromShebang "/bin/sh" == "sh"
|
||||||
|
prop_executableFromShebang2 = executableFromShebang "/bin/bash" == "bash"
|
||||||
|
prop_executableFromShebang3 = executableFromShebang "/usr/bin/env ksh" == "ksh"
|
||||||
|
prop_executableFromShebang4 = executableFromShebang "/usr/bin/env -S foo=bar bash -x" == "bash"
|
||||||
|
prop_executableFromShebang5 = executableFromShebang "/usr/bin/env --split-string=bash -x" == "bash"
|
||||||
|
prop_executableFromShebang6 = executableFromShebang "/usr/bin/env --split-string=foo=bar bash -x" == "bash"
|
||||||
|
prop_executableFromShebang7 = executableFromShebang "/usr/bin/env --split-string bash -x" == "bash"
|
||||||
|
prop_executableFromShebang8 = executableFromShebang "/usr/bin/env --split-string foo=bar bash -x" == "bash"
|
||||||
|
prop_executableFromShebang9 = executableFromShebang "/usr/bin/env foo=bar dash" == "dash"
|
||||||
|
prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "busybox sh"
|
||||||
|
prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "busybox ash"
|
||||||
|
|
||||||
|
-- Get the shell executable from a string like '/usr/bin/env bash'
|
||||||
|
executableFromShebang :: String -> String
|
||||||
|
executableFromShebang = shellFor
|
||||||
|
where
|
||||||
|
re = mkRegex "/env +(-S|--split-string=?)? *(.*)"
|
||||||
|
shellFor s | s `matches` re =
|
||||||
|
case matchRegex re s of
|
||||||
|
Just [flag, shell] -> fromEnvArgs (words shell)
|
||||||
|
_ -> ""
|
||||||
|
shellFor sb =
|
||||||
|
case words sb of
|
||||||
|
[] -> ""
|
||||||
|
[x] -> basename x
|
||||||
|
(first:second:args) | basename first == "busybox" ->
|
||||||
|
case basename second of
|
||||||
|
"sh" -> "busybox sh"
|
||||||
|
"ash" -> "busybox ash"
|
||||||
|
x -> x
|
||||||
|
(first:args) | basename first == "env" ->
|
||||||
|
fromEnvArgs args
|
||||||
|
(first:_) -> basename first
|
||||||
|
|
||||||
|
fromEnvArgs args = fromMaybe "" $ find (notElem '=') $ skipFlags args
|
||||||
|
basename s = reverse . takeWhile (/= '/') . reverse $ s
|
||||||
|
skipFlags = dropWhile ("-" `isPrefixOf`)
|
||||||
|
|
||||||
|
|
||||||
|
-- Determining if a name is a variable
|
||||||
|
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
|
||||||
|
isVariableChar x = isVariableStartChar x || isDigit x
|
||||||
|
isSpecialVariableChar = (`elem` "*@#?-$!")
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
-- Get the variable name from an expansion like ${var:-foo}
|
||||||
|
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_getBracedReference11b = 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) | c `elem` "!#" = rest
|
||||||
|
dropPrefix cs = cs
|
||||||
|
takeName s = do
|
||||||
|
let name = takeWhile isVariableChar s
|
||||||
|
guard . not $ null name
|
||||||
|
return name
|
||||||
|
getSpecial (c:_) | isSpecialVariableChar c = return [c]
|
||||||
|
getSpecial _ = fail "empty or not special"
|
||||||
|
|
||||||
|
nameExpansion ('!':next:rest) = do -- e.g. ${!foo*bar*}
|
||||||
|
guard $ isVariableChar next -- e.g. ${!@}
|
||||||
|
first <- find (not . isVariableChar) rest
|
||||||
|
guard $ first `elem` "*?@"
|
||||||
|
return ""
|
||||||
|
nameExpansion _ = Nothing
|
||||||
|
|
||||||
|
-- Get the variable modifier like /a/b in ${var/a/b}
|
||||||
|
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
|
||||||
|
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
|
||||||
|
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
|
||||||
|
prop_getBracedModifier4 = getBracedModifier "foo[@]@Q" == "[@]@Q"
|
||||||
|
prop_getBracedModifier5 = getBracedModifier "@@Q" == "@Q"
|
||||||
|
getBracedModifier s = headOrDefault "" $ 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]
|
||||||
|
|
||||||
|
-- Get the variables from indices like ["x", "y"] in ${var[x+y+1]}
|
||||||
|
prop_getIndexReferences1 = getIndexReferences "var[x+y+1]" == ["x", "y"]
|
||||||
|
getIndexReferences s = fromMaybe [] $ do
|
||||||
|
index:_ <- matchRegex re s
|
||||||
|
return $ matchAllStrings variableNameRegex index
|
||||||
|
where
|
||||||
|
re = mkRegex "(\\[.*\\])"
|
||||||
|
|
||||||
|
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
|
||||||
|
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
|
||||||
|
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
|
||||||
|
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
|
||||||
|
getOffsetReferences mods = fromMaybe [] $ do
|
||||||
|
-- if mods start with [, then drop until ]
|
||||||
|
_:offsets:_ <- matchRegex re mods
|
||||||
|
return $ matchAllStrings variableNameRegex offsets
|
||||||
|
where
|
||||||
|
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
|
||||||
|
|
||||||
|
|
||||||
|
-- Returns whether a token is a parameter expansion without any modifiers.
|
||||||
|
-- True for $var ${var} $1 $#
|
||||||
|
-- False for ${#var} ${var[x]} ${var:-0}
|
||||||
|
isUnmodifiedParameterExpansion t =
|
||||||
|
case t of
|
||||||
|
T_DollarBraced _ False _ -> True
|
||||||
|
T_DollarBraced _ _ list ->
|
||||||
|
let str = concat $ oversimplify list
|
||||||
|
in getBracedReference str == str
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
-- Return the referenced variable if (and only if) it's an unmodified parameter expansion.
|
||||||
|
getUnmodifiedParameterExpansion t =
|
||||||
|
case t of
|
||||||
|
T_DollarBraced _ _ list -> do
|
||||||
|
let str = concat $ oversimplify list
|
||||||
|
guard $ getBracedReference str == str
|
||||||
|
return str
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
--- A list of the element and all its parents up to the root node.
|
||||||
|
getPath tree = NE.unfoldr $ \t -> (t, Map.lookup (getId t) tree)
|
||||||
|
|
||||||
|
isClosingFileOp op =
|
||||||
|
case op of
|
||||||
|
T_IoDuplicate _ (T_GREATAND _) "-" -> True
|
||||||
|
T_IoDuplicate _ (T_LESSAND _) "-" -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
getEnableDirectives root =
|
||||||
|
case root of
|
||||||
|
T_Annotation _ list _ -> [s | EnableComment s <- list]
|
||||||
|
_ -> []
|
||||||
|
|
||||||
|
getExtendedAnalysisDirective :: Token -> Maybe Bool
|
||||||
|
getExtendedAnalysisDirective root =
|
||||||
|
case root of
|
||||||
|
T_Annotation _ list _ -> listToMaybe $ [s | ExtendedAnalysis s <- list]
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
return []
|
||||||
|
runTests = $quickCheckAll
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
||||||
{-
|
{-
|
||||||
Copyright 2012-2019 Vidar Holen
|
Copyright 2012-2022 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
https://www.shellcheck.net
|
https://www.shellcheck.net
|
||||||
|
@ -25,6 +25,7 @@ import ShellCheck.Interface
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
import qualified ShellCheck.Checks.Commands
|
import qualified ShellCheck.Checks.Commands
|
||||||
|
import qualified ShellCheck.Checks.ControlFlow
|
||||||
import qualified ShellCheck.Checks.Custom
|
import qualified ShellCheck.Checks.Custom
|
||||||
import qualified ShellCheck.Checks.ShellSupport
|
import qualified ShellCheck.Checks.ShellSupport
|
||||||
|
|
||||||
|
@ -34,19 +35,21 @@ analyzeScript :: AnalysisSpec -> AnalysisResult
|
||||||
analyzeScript spec = newAnalysisResult {
|
analyzeScript spec = newAnalysisResult {
|
||||||
arComments =
|
arComments =
|
||||||
filterByAnnotation spec params . nub $
|
filterByAnnotation spec params . nub $
|
||||||
runAnalytics spec
|
runChecker params (checkers spec params)
|
||||||
++ runChecker params (checkers spec params)
|
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
params = makeParameters spec
|
params = makeParameters spec
|
||||||
|
|
||||||
checkers spec params = mconcat $ map ($ params) [
|
checkers spec params = mconcat $ map ($ params) [
|
||||||
|
ShellCheck.Analytics.checker spec,
|
||||||
ShellCheck.Checks.Commands.checker spec,
|
ShellCheck.Checks.Commands.checker spec,
|
||||||
|
ShellCheck.Checks.ControlFlow.checker spec,
|
||||||
ShellCheck.Checks.Custom.checker,
|
ShellCheck.Checks.Custom.checker,
|
||||||
ShellCheck.Checks.ShellSupport.checker
|
ShellCheck.Checks.ShellSupport.checker
|
||||||
]
|
]
|
||||||
|
|
||||||
optionalChecks = mconcat $ [
|
optionalChecks = mconcat $ [
|
||||||
ShellCheck.Analytics.optionalChecks,
|
ShellCheck.Analytics.optionalChecks,
|
||||||
ShellCheck.Checks.Commands.optionalChecks
|
ShellCheck.Checks.Commands.optionalChecks,
|
||||||
|
ShellCheck.Checks.ControlFlow.optionalChecks
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{-
|
{-
|
||||||
Copyright 2012-2019 Vidar Holen
|
Copyright 2012-2022 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
https://www.shellcheck.net
|
https://www.shellcheck.net
|
||||||
|
@ -23,13 +23,16 @@ module ShellCheck.AnalyzerLib where
|
||||||
|
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
import ShellCheck.ASTLib
|
import ShellCheck.ASTLib
|
||||||
|
import qualified ShellCheck.CFGAnalysis as CF
|
||||||
import ShellCheck.Data
|
import ShellCheck.Data
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Parser
|
import ShellCheck.Parser
|
||||||
|
import ShellCheck.Prelude
|
||||||
import ShellCheck.Regex
|
import ShellCheck.Regex
|
||||||
|
|
||||||
import Control.Arrow (first)
|
import Control.Arrow (first)
|
||||||
import Control.DeepSeq
|
import Control.DeepSeq
|
||||||
|
import Control.Monad
|
||||||
import Control.Monad.Identity
|
import Control.Monad.Identity
|
||||||
import Control.Monad.RWS
|
import Control.Monad.RWS
|
||||||
import Control.Monad.State
|
import Control.Monad.State
|
||||||
|
@ -38,6 +41,7 @@ import Data.Char
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import Data.Semigroup
|
import Data.Semigroup
|
||||||
|
import qualified Data.List.NonEmpty as NE
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
import Test.QuickCheck.All (forAllProperties)
|
import Test.QuickCheck.All (forAllProperties)
|
||||||
|
@ -79,10 +83,18 @@ composeAnalyzers f g x = f x >> g x
|
||||||
data Parameters = Parameters {
|
data Parameters = Parameters {
|
||||||
-- Whether this script has the 'lastpipe' option set/default.
|
-- Whether this script has the 'lastpipe' option set/default.
|
||||||
hasLastpipe :: Bool,
|
hasLastpipe :: Bool,
|
||||||
|
-- Whether this script has the 'inherit_errexit' option set/default.
|
||||||
|
hasInheritErrexit :: Bool,
|
||||||
-- Whether this script has 'set -e' anywhere.
|
-- Whether this script has 'set -e' anywhere.
|
||||||
hasSetE :: Bool,
|
hasSetE :: Bool,
|
||||||
|
-- Whether this script has 'set -o pipefail' anywhere.
|
||||||
|
hasPipefail :: Bool,
|
||||||
|
-- Whether this script has 'shopt -s execfail' anywhere.
|
||||||
|
hasExecfail :: Bool,
|
||||||
-- A linear (bad) analysis of data flow
|
-- A linear (bad) analysis of data flow
|
||||||
variableFlow :: [StackData],
|
variableFlow :: [StackData],
|
||||||
|
-- A map from Id to Token
|
||||||
|
idMap :: Map.Map Id Token,
|
||||||
-- A map from Id to parent Token
|
-- A map from Id to parent Token
|
||||||
parentMap :: Map.Map Id Token,
|
parentMap :: Map.Map Id Token,
|
||||||
-- The shell type, such as Bash or Ksh
|
-- The shell type, such as Bash or Ksh
|
||||||
|
@ -92,7 +104,9 @@ data Parameters = Parameters {
|
||||||
-- The root node of the AST
|
-- The root node of the AST
|
||||||
rootNode :: Token,
|
rootNode :: Token,
|
||||||
-- map from token id to start and end position
|
-- map from token id to start and end position
|
||||||
tokenPositions :: Map.Map Id (Position, Position)
|
tokenPositions :: Map.Map Id (Position, Position),
|
||||||
|
-- Result from Control Flow Graph analysis (including data flow analysis)
|
||||||
|
cfgAnalysis :: Maybe CF.CFGAnalysis
|
||||||
} deriving (Show)
|
} deriving (Show)
|
||||||
|
|
||||||
-- TODO: Cache results of common AST ops here
|
-- TODO: Cache results of common AST ops here
|
||||||
|
@ -142,7 +156,7 @@ producesComments c s = do
|
||||||
prRoot pr
|
prRoot pr
|
||||||
let spec = defaultSpec pr
|
let spec = defaultSpec pr
|
||||||
let params = makeParameters spec
|
let params = makeParameters spec
|
||||||
return . not . null $ runChecker params c
|
return . not . null $ filterByAnnotation spec params $ runChecker params c
|
||||||
|
|
||||||
makeComment :: Severity -> Id -> Code -> String -> TokenComment
|
makeComment :: Severity -> Id -> Code -> String -> TokenComment
|
||||||
makeComment severity id code note =
|
makeComment severity id code note =
|
||||||
|
@ -163,8 +177,12 @@ err id code str = addComment $ makeComment ErrorC id code str
|
||||||
info id code str = addComment $ makeComment InfoC id code str
|
info id code str = addComment $ makeComment InfoC id code str
|
||||||
style id code str = addComment $ makeComment StyleC id code str
|
style id code str = addComment $ makeComment StyleC id code str
|
||||||
|
|
||||||
|
errWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
|
||||||
|
errWithFix = addCommentWithFix ErrorC
|
||||||
warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
|
warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
|
||||||
warnWithFix = addCommentWithFix WarningC
|
warnWithFix = addCommentWithFix WarningC
|
||||||
|
infoWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
|
||||||
|
infoWithFix = addCommentWithFix InfoC
|
||||||
styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
|
styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
|
||||||
styleWithFix = addCommentWithFix StyleC
|
styleWithFix = addCommentWithFix StyleC
|
||||||
|
|
||||||
|
@ -176,28 +194,58 @@ makeCommentWithFix :: Severity -> Id -> Code -> String -> Fix -> TokenComment
|
||||||
makeCommentWithFix severity id code str fix =
|
makeCommentWithFix severity id code str fix =
|
||||||
let comment = makeComment severity id code str
|
let comment = makeComment severity id code str
|
||||||
withFix = comment {
|
withFix = comment {
|
||||||
tcFix = Just fix
|
-- If fix is empty, pretend it wasn't there.
|
||||||
|
tcFix = if null (fixReplacements fix) then Nothing else Just fix
|
||||||
}
|
}
|
||||||
in withFix `deepseq` withFix
|
in force withFix
|
||||||
|
|
||||||
makeParameters spec =
|
-- makeParameters :: CheckSpec -> Parameters
|
||||||
let params = Parameters {
|
makeParameters spec = params
|
||||||
|
where
|
||||||
|
extendedAnalysis = fromMaybe True $ msum [asExtendedAnalysis spec, getExtendedAnalysisDirective root]
|
||||||
|
params = Parameters {
|
||||||
rootNode = root,
|
rootNode = root,
|
||||||
shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec,
|
shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec,
|
||||||
hasSetE = containsSetE root,
|
hasSetE = containsSetE root,
|
||||||
hasLastpipe =
|
hasLastpipe =
|
||||||
case shellType params of
|
case shellType params of
|
||||||
Bash -> containsLastpipe root
|
Bash -> isOptionSet "lastpipe" root
|
||||||
Dash -> False
|
Dash -> False
|
||||||
|
BusyboxSh -> False
|
||||||
Sh -> False
|
Sh -> False
|
||||||
Ksh -> True,
|
Ksh -> True,
|
||||||
|
hasInheritErrexit =
|
||||||
|
case shellType params of
|
||||||
|
Bash -> isOptionSet "inherit_errexit" root
|
||||||
|
Dash -> True
|
||||||
|
BusyboxSh -> True
|
||||||
|
Sh -> True
|
||||||
|
Ksh -> False,
|
||||||
|
hasPipefail =
|
||||||
|
case shellType params of
|
||||||
|
Bash -> isOptionSet "pipefail" root
|
||||||
|
Dash -> True
|
||||||
|
BusyboxSh -> isOptionSet "pipefail" root
|
||||||
|
Sh -> True
|
||||||
|
Ksh -> isOptionSet "pipefail" root,
|
||||||
|
hasExecfail =
|
||||||
|
case shellType params of
|
||||||
|
Bash -> isOptionSet "execfail" root
|
||||||
|
_ -> False,
|
||||||
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
|
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
|
||||||
|
idMap = getTokenMap root,
|
||||||
parentMap = getParentTree root,
|
parentMap = getParentTree root,
|
||||||
variableFlow = getVariableFlow params root,
|
variableFlow = getVariableFlow params root,
|
||||||
tokenPositions = asTokenPositions spec
|
tokenPositions = asTokenPositions spec,
|
||||||
} in params
|
cfgAnalysis = do
|
||||||
where root = asScript spec
|
guard extendedAnalysis
|
||||||
|
return $ CF.analyzeControlFlow cfParams root
|
||||||
|
}
|
||||||
|
cfParams = CF.CFGParameters {
|
||||||
|
CF.cfLastpipe = hasLastpipe params,
|
||||||
|
CF.cfPipefail = hasPipefail params
|
||||||
|
}
|
||||||
|
root = asScript spec
|
||||||
|
|
||||||
|
|
||||||
-- Does this script mention 'set -e' anywhere?
|
-- Does this script mention 'set -e' anywhere?
|
||||||
|
@ -214,18 +262,30 @@ containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
|
||||||
_ -> False
|
_ -> False
|
||||||
re = mkRegex "[[:space:]]-[^-]*e"
|
re = mkRegex "[[:space:]]-[^-]*e"
|
||||||
|
|
||||||
-- Does this script mention 'shopt -s lastpipe' anywhere?
|
|
||||||
-- Also used as a hack.
|
containsSetOption opt root = isNothing $ doAnalysis (guard . not . isPipefail) root
|
||||||
containsLastpipe root =
|
where
|
||||||
|
isPipefail t =
|
||||||
|
case t of
|
||||||
|
T_SimpleCommand {} ->
|
||||||
|
t `isUnqualifiedCommand` "set" &&
|
||||||
|
(opt `elem` oversimplify t ||
|
||||||
|
"o" `elem` map snd (getAllFlags t))
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
containsShopt shopt root =
|
||||||
isNothing $ doAnalysis (guard . not . isShoptLastPipe) root
|
isNothing $ doAnalysis (guard . not . isShoptLastPipe) root
|
||||||
where
|
where
|
||||||
isShoptLastPipe t =
|
isShoptLastPipe t =
|
||||||
case t of
|
case t of
|
||||||
T_SimpleCommand {} ->
|
T_SimpleCommand {} ->
|
||||||
t `isUnqualifiedCommand` "shopt" &&
|
t `isUnqualifiedCommand` "shopt" &&
|
||||||
("lastpipe" `elem` oversimplify t)
|
(shopt `elem` oversimplify t)
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
|
-- Does this script mention 'shopt -s $opt' or 'set -o $opt' anywhere?
|
||||||
|
isOptionSet opt root = containsShopt opt root || containsSetOption opt root
|
||||||
|
|
||||||
|
|
||||||
prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh
|
prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh
|
||||||
prop_determineShell1 = determineShellTest "#!/usr/bin/env ksh" == Ksh
|
prop_determineShell1 = determineShellTest "#!/usr/bin/env ksh" == Ksh
|
||||||
|
@ -236,6 +296,10 @@ prop_determineShell5 = determineShellTest "#shellcheck shell=sh\nfoo" == Sh
|
||||||
prop_determineShell6 = determineShellTest "#! /bin/sh" == Sh
|
prop_determineShell6 = determineShellTest "#! /bin/sh" == Sh
|
||||||
prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
|
prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
|
||||||
prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh
|
prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh
|
||||||
|
prop_determineShell9 = determineShellTest "#!/bin/env -S dash -x" == Dash
|
||||||
|
prop_determineShell10 = determineShellTest "#!/bin/env --split-string= dash -x" == Dash
|
||||||
|
prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == BusyboxSh -- busybox sh is a specific shell, not posix sh
|
||||||
|
prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == BusyboxSh
|
||||||
|
|
||||||
determineShellTest = determineShellTest' Nothing
|
determineShellTest = determineShellTest' Nothing
|
||||||
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
|
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
|
||||||
|
@ -249,22 +313,11 @@ determineShell fallbackShell t = fromMaybe Bash $
|
||||||
headOrDefault (fromShebang s) [s | ShellOverride s <- annotations]
|
headOrDefault (fromShebang s) [s | ShellOverride s <- annotations]
|
||||||
fromShebang (T_Script _ (T_Literal _ s) _) = executableFromShebang s
|
fromShebang (T_Script _ (T_Literal _ s) _) = 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 = headOrDefault "" (drop 1 $ words s)
|
|
||||||
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
|
|
||||||
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Given a root node, make a map from Id to parent Token.
|
-- Given a root node, make a map from Id to parent Token.
|
||||||
-- This is used to populate parentMap in Parameters
|
-- This is used to populate parentMap in Parameters
|
||||||
getParentTree :: Token -> Map.Map Id Token
|
getParentTree :: Token -> Map.Map Id Token
|
||||||
getParentTree t =
|
getParentTree t =
|
||||||
snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
|
snd $ execState (doStackAnalysis pre post t) ([], Map.empty)
|
||||||
where
|
where
|
||||||
pre t = modify (first ((:) t))
|
pre t = modify (first ((:) t))
|
||||||
post t = do
|
post t = do
|
||||||
|
@ -292,16 +345,16 @@ isStrictlyQuoteFree = isQuoteFreeNode True
|
||||||
isQuoteFree = isQuoteFreeNode False
|
isQuoteFree = isQuoteFreeNode False
|
||||||
|
|
||||||
|
|
||||||
isQuoteFreeNode strict tree t =
|
isQuoteFreeNode strict shell tree t =
|
||||||
(isQuoteFreeElement t == Just True) ||
|
isQuoteFreeElement t ||
|
||||||
headOrDefault False (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t))
|
(fromMaybe False $ msum $ map isQuoteFreeContext $ NE.tail $ getPath tree t)
|
||||||
where
|
where
|
||||||
-- Is this node self-quoting in itself?
|
-- Is this node self-quoting in itself?
|
||||||
isQuoteFreeElement t =
|
isQuoteFreeElement t =
|
||||||
case t of
|
case t of
|
||||||
T_Assignment {} -> return True
|
T_Assignment id _ _ _ _ -> assignmentIsQuoting id
|
||||||
T_FdRedirect {} -> return True
|
T_FdRedirect {} -> True
|
||||||
_ -> Nothing
|
_ -> False
|
||||||
|
|
||||||
-- Are any subnodes inherently self-quoting?
|
-- Are any subnodes inherently self-quoting?
|
||||||
isQuoteFreeContext t =
|
isQuoteFreeContext t =
|
||||||
|
@ -311,7 +364,7 @@ isQuoteFreeNode strict tree t =
|
||||||
TC_Binary _ DoubleBracket _ _ _ -> return True
|
TC_Binary _ DoubleBracket _ _ _ -> return True
|
||||||
TA_Sequence {} -> return True
|
TA_Sequence {} -> return True
|
||||||
T_Arithmetic {} -> return True
|
T_Arithmetic {} -> return True
|
||||||
T_Assignment {} -> return True
|
T_Assignment id _ _ _ _ -> return $ assignmentIsQuoting id
|
||||||
T_Redirecting {} -> return False
|
T_Redirecting {} -> return False
|
||||||
T_DoubleQuoted _ _ -> return True
|
T_DoubleQuoted _ _ -> return True
|
||||||
T_DollarDoubleQuoted _ _ -> return True
|
T_DollarDoubleQuoted _ _ -> return True
|
||||||
|
@ -323,6 +376,18 @@ isQuoteFreeNode strict tree t =
|
||||||
T_SelectIn {} -> return (not strict)
|
T_SelectIn {} -> return (not strict)
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
|
-- Check whether this assignment is self-quoting due to being a recognized
|
||||||
|
-- assignment passed to a Declaration Utility. This will soon be required
|
||||||
|
-- by POSIX: https://austingroupbugs.net/view.php?id=351
|
||||||
|
assignmentIsQuoting id = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand id)
|
||||||
|
shellParsesParamsAsAssignments = shell /= Sh
|
||||||
|
|
||||||
|
-- Is this assignment a parameter to a command like export/typeset/etc?
|
||||||
|
isAssignmentParamToCommand id =
|
||||||
|
case Map.lookup id tree of
|
||||||
|
Just (T_SimpleCommand _ _ (_:args)) -> id `elem` (map getId args)
|
||||||
|
_ -> False
|
||||||
|
|
||||||
-- Check if a token is a parameter to a certain command by name:
|
-- Check if a token is a parameter to a certain command by name:
|
||||||
-- Example: isParamTo (parentMap params) "sed" t
|
-- Example: isParamTo (parentMap params) "sed" t
|
||||||
isParamTo :: Map.Map Id Token -> String -> Token -> Bool
|
isParamTo :: Map.Map Id Token -> String -> Token -> Bool
|
||||||
|
@ -344,7 +409,7 @@ isParamTo tree cmd =
|
||||||
-- Get the parent command (T_Redirecting) of a Token, if any.
|
-- Get the parent command (T_Redirecting) of a Token, if any.
|
||||||
getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token
|
getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token
|
||||||
getClosestCommand tree t =
|
getClosestCommand tree t =
|
||||||
findFirst findCommand $ getPath tree t
|
findFirst findCommand $ NE.toList $ getPath tree t
|
||||||
where
|
where
|
||||||
findCommand t =
|
findCommand t =
|
||||||
case t of
|
case t of
|
||||||
|
@ -354,48 +419,43 @@ getClosestCommand tree t =
|
||||||
|
|
||||||
-- Like above, if koala_man knew Haskell when starting this project.
|
-- Like above, if koala_man knew Haskell when starting this project.
|
||||||
getClosestCommandM t = do
|
getClosestCommandM t = do
|
||||||
tree <- asks parentMap
|
params <- ask
|
||||||
return $ getClosestCommand tree t
|
return $ getClosestCommand (parentMap params) t
|
||||||
|
|
||||||
-- Is the token used as a command name (the first word in a T_SimpleCommand)?
|
-- Is the token used as a command name (the first word in a T_SimpleCommand)?
|
||||||
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
usedAsCommandName tree token = go (getId token) (NE.tail $ getPath tree token)
|
||||||
where
|
where
|
||||||
go currentId (T_NormalWord id [word]:rest)
|
go currentId (T_NormalWord id [word]:rest)
|
||||||
| currentId == getId word = go id rest
|
| currentId == getId word = go id rest
|
||||||
go currentId (T_DoubleQuoted id [word]:rest)
|
go currentId (T_DoubleQuoted id [word]:rest)
|
||||||
| currentId == getId word = go id rest
|
| currentId == getId word = go id rest
|
||||||
go currentId (T_SimpleCommand _ _ (word:_):_)
|
go currentId (t@(T_SimpleCommand _ _ (word:_)):_) =
|
||||||
| currentId == getId word = True
|
getId word == currentId || getId (getCommandTokenOrThis t) == currentId
|
||||||
go _ _ = False
|
go _ _ = False
|
||||||
|
|
||||||
-- A list of the element and all its parents up to the root node.
|
|
||||||
getPath tree t = t :
|
|
||||||
case Map.lookup (getId t) tree of
|
|
||||||
Nothing -> []
|
|
||||||
Just parent -> getPath tree parent
|
|
||||||
|
|
||||||
-- Version of the above taking the map from the current context
|
-- Version of the above taking the map from the current context
|
||||||
-- Todo: give this the name "getPath"
|
-- Todo: give this the name "getPath"
|
||||||
getPathM t = do
|
getPathM t = do
|
||||||
map <- asks parentMap
|
params <- ask
|
||||||
return $ getPath map t
|
return $ getPath (parentMap params) t
|
||||||
|
|
||||||
isParentOf tree parent child =
|
isParentOf tree parent child =
|
||||||
elem (getId parent) . map getId $ getPath tree child
|
any (\t -> parentId == getId t) (getPath tree child)
|
||||||
|
where
|
||||||
|
parentId = getId parent
|
||||||
|
|
||||||
parents params = getPath (parentMap params)
|
parents params = getPath (parentMap params)
|
||||||
|
|
||||||
-- Find the first match in a list where the predicate is Just True.
|
-- Find the first match in a list where the predicate is Just True.
|
||||||
-- Stops if it's Just False and ignores Nothing.
|
-- Stops if it's Just False and ignores Nothing.
|
||||||
findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a
|
findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a
|
||||||
findFirst p l =
|
findFirst p = foldr go Nothing
|
||||||
case l of
|
where
|
||||||
[] -> Nothing
|
go x acc =
|
||||||
(x:xs) ->
|
|
||||||
case p x of
|
case p x of
|
||||||
Just True -> return x
|
Just True -> return x
|
||||||
Just False -> Nothing
|
Just False -> Nothing
|
||||||
Nothing -> findFirst p xs
|
Nothing -> acc
|
||||||
|
|
||||||
-- Check whether a word is entirely output from a single command
|
-- Check whether a word is entirely output from a single command
|
||||||
tokenIsJustCommandOutput t = case t of
|
tokenIsJustCommandOutput t = case t of
|
||||||
|
@ -410,8 +470,7 @@ tokenIsJustCommandOutput t = case t of
|
||||||
|
|
||||||
-- TODO: Replace this with a proper Control Flow Graph
|
-- TODO: Replace this with a proper Control Flow Graph
|
||||||
getVariableFlow params t =
|
getVariableFlow params t =
|
||||||
let (_, stack) = runState (doStackAnalysis startScope endScope t) []
|
reverse $ execState (doStackAnalysis startScope endScope t) []
|
||||||
in reverse stack
|
|
||||||
where
|
where
|
||||||
startScope t =
|
startScope t =
|
||||||
let scopeType = leadType params t
|
let scopeType = leadType params t
|
||||||
|
@ -462,83 +521,82 @@ leadType params t =
|
||||||
|
|
||||||
causesSubshell = do
|
causesSubshell = do
|
||||||
(T_Pipeline _ _ list) <- parentPipeline
|
(T_Pipeline _ _ list) <- parentPipeline
|
||||||
if length list <= 1
|
return $ case list of
|
||||||
then return False
|
_:_:_ -> not (hasLastpipe params) || getId (last list) /= getId t
|
||||||
else if not $ hasLastpipe params
|
_ -> False
|
||||||
then return True
|
|
||||||
else return . not $ (getId . head $ reverse list) == getId t
|
|
||||||
|
|
||||||
getModifiedVariables t =
|
getModifiedVariables t =
|
||||||
case t of
|
case t of
|
||||||
T_SimpleCommand _ vars [] ->
|
T_SimpleCommand _ vars [] ->
|
||||||
concatMap (\x -> case x of
|
[(x, x, name, dataTypeFrom DataString w) | x@(T_Assignment id _ name _ w) <- vars]
|
||||||
T_Assignment id _ name _ w ->
|
T_SimpleCommand {} ->
|
||||||
[(x, x, name, dataTypeFrom DataString w)]
|
getModifiedVariableCommand t
|
||||||
_ -> []
|
|
||||||
) vars
|
|
||||||
c@T_SimpleCommand {} ->
|
|
||||||
getModifiedVariableCommand c
|
|
||||||
|
|
||||||
TA_Unary _ "++|" v@(TA_Variable _ name _) ->
|
TA_Unary _ op v@(TA_Variable _ name _) | "--" `isInfixOf` op || "++" `isInfixOf` op ->
|
||||||
[(t, v, name, DataString $ SourceFrom [v])]
|
[(t, v, name, DataString SourceInteger)]
|
||||||
TA_Unary _ "|++" v@(TA_Variable _ name _) ->
|
TA_Assignment _ op (TA_Variable _ name _) rhs -> do
|
||||||
[(t, v, name, DataString $ SourceFrom [v])]
|
|
||||||
TA_Assignment _ op (TA_Variable _ name _) rhs -> maybeToList $ do
|
|
||||||
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
||||||
return (t, t, name, DataString $ SourceFrom [rhs])
|
return (t, t, name, DataString SourceInteger)
|
||||||
|
|
||||||
T_BatsTest {} -> [
|
T_BatsTest {} -> [
|
||||||
(t, t, "lines", DataArray SourceExternal),
|
(t, t, "lines", DataArray SourceExternal),
|
||||||
(t, t, "status", DataString SourceInteger),
|
(t, t, "status", DataString SourceInteger),
|
||||||
(t, t, "output", DataString SourceExternal)
|
(t, t, "output", DataString SourceExternal),
|
||||||
|
(t, t, "stderr", DataString SourceExternal),
|
||||||
|
(t, t, "stderr_lines", DataArray SourceExternal)
|
||||||
]
|
]
|
||||||
|
|
||||||
-- Count [[ -v foo ]] as an "assignment".
|
-- Count [[ -v foo ]] as an "assignment".
|
||||||
-- This is to prevent [ -v foo ] being unassigned or unused.
|
-- This is to prevent [ -v foo ] being unassigned or unused.
|
||||||
TC_Unary id _ "-v" token -> maybeToList $ do
|
TC_Unary id _ "-v" token -> maybeToList $ do
|
||||||
str <- fmap (takeWhile (/= '[')) $ -- Quoted index
|
str <- getVariableForTestDashV token
|
||||||
flip getLiteralStringExt token $ \x ->
|
|
||||||
case x of
|
|
||||||
T_Glob _ s -> return s -- Unquoted index
|
|
||||||
_ -> Nothing
|
|
||||||
|
|
||||||
guard . not . null $ str
|
|
||||||
return (t, token, str, DataString SourceChecked)
|
return (t, token, str, DataString SourceChecked)
|
||||||
|
|
||||||
|
TC_Unary _ _ "-n" token -> markAsChecked t token
|
||||||
|
TC_Unary _ _ "-z" token -> markAsChecked t token
|
||||||
|
TC_Nullary _ _ token -> markAsChecked t token
|
||||||
|
|
||||||
T_DollarBraced _ _ l -> maybeToList $ do
|
T_DollarBraced _ _ l -> maybeToList $ do
|
||||||
let string = bracedString t
|
let string = concat $ oversimplify l
|
||||||
let modifier = getBracedModifier string
|
let modifier = getBracedModifier string
|
||||||
guard $ any (`isPrefixOf` modifier) ["=", ":="]
|
guard $ any (`isPrefixOf` modifier) ["=", ":="]
|
||||||
return (t, t, getBracedReference string, DataString $ SourceFrom [l])
|
return (t, t, getBracedReference string, DataString $ SourceFrom [l])
|
||||||
|
|
||||||
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo
|
T_FdRedirect _ ('{':var) op -> -- {foo}>&2 modifies foo
|
||||||
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
|
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
|
||||||
|
|
||||||
t@(T_CoProc _ name _) ->
|
T_CoProc _ Nothing _ ->
|
||||||
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
|
[(t, t, "COPROC", DataArray SourceInteger)]
|
||||||
|
|
||||||
|
T_CoProc _ (Just token) _ -> do
|
||||||
|
name <- maybeToList $ getLiteralString token
|
||||||
|
[(t, t, name, DataArray SourceInteger)]
|
||||||
|
|
||||||
--Points to 'for' rather than variable
|
--Points to 'for' rather than variable
|
||||||
T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)]
|
T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)]
|
||||||
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
|
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
|
||||||
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
|
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
|
||||||
_ -> []
|
_ -> []
|
||||||
|
where
|
||||||
isClosingFileOp op =
|
markAsChecked place token = mapMaybe (f place) $ getWordParts token
|
||||||
case op of
|
f place t = case t of
|
||||||
T_IoDuplicate _ (T_GREATAND _) "-" -> True
|
T_DollarBraced _ _ l ->
|
||||||
T_IoDuplicate _ (T_LESSAND _) "-" -> True
|
let str = getBracedReference $ concat $ oversimplify l in do
|
||||||
_ -> False
|
guard $ isVariableName str
|
||||||
|
return (place, t, str, DataString SourceChecked)
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
|
||||||
-- Consider 'export/declare -x' a reference, since it makes the var available
|
-- Consider 'export/declare -x' a reference, since it makes the var available
|
||||||
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
||||||
case x of
|
case x of
|
||||||
|
"declare" -> forDeclare
|
||||||
|
"typeset" -> forDeclare
|
||||||
|
|
||||||
"export" -> if "f" `elem` flags
|
"export" -> if "f" `elem` flags
|
||||||
then []
|
then []
|
||||||
else concatMap getReference rest
|
else concatMap getReference rest
|
||||||
"declare" -> if
|
"local" -> if "x" `elem` flags
|
||||||
any (`elem` flags) ["x", "p"] &&
|
|
||||||
(not $ any (`elem` flags) ["f", "F"])
|
|
||||||
then concatMap getReference rest
|
then concatMap getReference rest
|
||||||
else []
|
else []
|
||||||
"trap" ->
|
"trap" ->
|
||||||
|
@ -548,6 +606,13 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
|
||||||
"alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token]
|
"alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token]
|
||||||
_ -> []
|
_ -> []
|
||||||
where
|
where
|
||||||
|
forDeclare =
|
||||||
|
if
|
||||||
|
any (`elem` flags) ["x", "p"] &&
|
||||||
|
(not $ any (`elem` flags) ["f", "F"])
|
||||||
|
then concatMap getReference rest
|
||||||
|
else []
|
||||||
|
|
||||||
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
|
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
|
||||||
getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
|
getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
|
||||||
getReference _ = []
|
getReference _ = []
|
||||||
|
@ -569,10 +634,14 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
|
||||||
"builtin" ->
|
"builtin" ->
|
||||||
getModifiedVariableCommand $ T_SimpleCommand id cmdPrefix rest
|
getModifiedVariableCommand $ T_SimpleCommand id cmdPrefix rest
|
||||||
"read" ->
|
"read" ->
|
||||||
let params = map getLiteral rest
|
let fallback = catMaybes $ takeWhile isJust (reverse $ map getLiteral rest)
|
||||||
readArrayVars = getReadArrayVariables rest
|
in fromMaybe fallback $ do
|
||||||
in
|
parsed <- getGnuOpts flagsForRead rest
|
||||||
catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params
|
case lookup "a" parsed of
|
||||||
|
Just (_, var) -> (:[]) <$> getLiteralArray var
|
||||||
|
Nothing -> return $ catMaybes $
|
||||||
|
map (getLiteral . snd . snd) $ filter (null . fst) parsed
|
||||||
|
|
||||||
"getopts" ->
|
"getopts" ->
|
||||||
case rest of
|
case rest of
|
||||||
opts:var:_ -> maybeToList $ getLiteral var
|
opts:var:_ -> maybeToList $ getLiteral var
|
||||||
|
@ -583,8 +652,8 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
|
||||||
"export" ->
|
"export" ->
|
||||||
if "f" `elem` flags then [] else concatMap getModifierParamString rest
|
if "f" `elem` flags then [] else concatMap getModifierParamString rest
|
||||||
|
|
||||||
"declare" -> if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
|
"declare" -> forDeclare
|
||||||
"typeset" -> declaredVars
|
"typeset" -> forDeclare
|
||||||
|
|
||||||
"local" -> concatMap getModifierParamString rest
|
"local" -> concatMap getModifierParamString rest
|
||||||
"readonly" ->
|
"readonly" ->
|
||||||
|
@ -596,6 +665,7 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
|
||||||
return (base, base, "@", DataString $ SourceFrom params)
|
return (base, base, "@", DataString $ SourceFrom params)
|
||||||
|
|
||||||
"printf" -> maybeToList $ getPrintfVariable rest
|
"printf" -> maybeToList $ getPrintfVariable rest
|
||||||
|
"wait" -> maybeToList $ getWaitVariable rest
|
||||||
|
|
||||||
"mapfile" -> maybeToList $ getMapfileArray base rest
|
"mapfile" -> maybeToList $ getMapfileArray base rest
|
||||||
"readarray" -> maybeToList $ getMapfileArray base rest
|
"readarray" -> maybeToList $ getMapfileArray base rest
|
||||||
|
@ -615,6 +685,8 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
|
||||||
T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
|
T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
|
||||||
stripEqualsFrom t = t
|
stripEqualsFrom t = t
|
||||||
|
|
||||||
|
forDeclare = if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
|
||||||
|
|
||||||
declaredVars = concatMap (getModifierParam defaultType) rest
|
declaredVars = concatMap (getModifierParam defaultType) rest
|
||||||
where
|
where
|
||||||
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
|
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
|
||||||
|
@ -653,36 +725,39 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
|
||||||
_ -> return (t:fromMaybe [] (getSetParams rest))
|
_ -> return (t:fromMaybe [] (getSetParams rest))
|
||||||
getSetParams [] = Nothing
|
getSetParams [] = Nothing
|
||||||
|
|
||||||
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
|
getPrintfVariable list = getFlagAssignedVariable "v" (SourceFrom list) $ getBsdOpts "v:" list
|
||||||
where
|
getWaitVariable list = getFlagAssignedVariable "p" SourceInteger $ return $ getGenericOpts list
|
||||||
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, varName, varType $ SourceFrom list)
|
|
||||||
where
|
getFlagAssignedVariable str dataSource maybeFlags = do
|
||||||
(varName, varType) = case elemIndex '[' var of
|
flags <- maybeFlags
|
||||||
Just i -> (take i var, DataArray)
|
(_, (flag, value)) <- find ((== str) . fst) flags
|
||||||
Nothing -> (var, DataString)
|
variableName <- getLiteralStringExt (const $ return "!") value
|
||||||
f (_:rest) = f rest
|
let (baseName, index) = span (/= '[') variableName
|
||||||
f [] = fail "not found"
|
return (base, value, baseName, (if null index then DataString else DataArray) dataSource)
|
||||||
|
|
||||||
-- mapfile has some curious syntax allowing flags plus 0..n variable names
|
-- 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
|
-- where only the first non-option one is used if any.
|
||||||
-- just get the last one, if it's a variable name.
|
getMapfileArray base rest = parseArgs `mplus` fallback
|
||||||
getMapfileArray base arguments = do
|
where
|
||||||
lastArg <- listToMaybe (reverse arguments)
|
parseArgs :: Maybe (Token, Token, String, DataType)
|
||||||
name <- getLiteralString lastArg
|
parseArgs = do
|
||||||
|
args <- getGnuOpts "d:n:O:s:u:C:c:t" rest
|
||||||
|
case [y | ("",(_,y)) <- args] of
|
||||||
|
[] ->
|
||||||
|
return (base, base, "MAPFILE", DataArray SourceExternal)
|
||||||
|
first:_ -> do
|
||||||
|
name <- getLiteralString first
|
||||||
guard $ isVariableName name
|
guard $ isVariableName name
|
||||||
return (base, lastArg, name, DataArray SourceExternal)
|
return (base, first, name, DataArray SourceExternal)
|
||||||
|
-- If arg parsing fails (due to bad or new flags), get the last variable name
|
||||||
-- get all the array variables used in read, e.g. read -a arr
|
fallback :: Maybe (Token, Token, String, DataType)
|
||||||
getReadArrayVariables args =
|
fallback = do
|
||||||
map (getLiteralArray . snd)
|
(name, token) <- listToMaybe . mapMaybe f $ reverse rest
|
||||||
(filter (isArrayFlag . fst) (zip args (tail args)))
|
return (base, token, name, DataArray SourceExternal)
|
||||||
|
f arg = do
|
||||||
isArrayFlag x = fromMaybe False $ do
|
name <- getLiteralString arg
|
||||||
str <- getLiteralString x
|
guard $ isVariableName name
|
||||||
return $ case str of
|
return (name, arg)
|
||||||
'-':'-':_ -> False
|
|
||||||
'-':str -> 'a' `elem` str
|
|
||||||
_ -> False
|
|
||||||
|
|
||||||
-- get the FLAGS_ variable created by a shflags DEFINE_ call
|
-- get the FLAGS_ variable created by a shflags DEFINE_ call
|
||||||
getFlagVariable (n:v:_) = do
|
getFlagVariable (n:v:_) = do
|
||||||
|
@ -692,28 +767,23 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
|
||||||
|
|
||||||
getModifiedVariableCommand _ = []
|
getModifiedVariableCommand _ = []
|
||||||
|
|
||||||
getIndexReferences s = fromMaybe [] $ do
|
-- Given a NormalWord like foo or foo[$bar], get foo.
|
||||||
match <- matchRegex re s
|
-- Primarily used to get references for [[ -v foo[bar] ]]
|
||||||
index <- match !!! 0
|
getVariableForTestDashV :: Token -> Maybe String
|
||||||
return $ matchAllStrings variableNameRegex index
|
getVariableForTestDashV t = do
|
||||||
|
str <- takeWhile ('[' /=) <$> getLiteralStringExt toStr t
|
||||||
|
guard $ isVariableName str
|
||||||
|
return str
|
||||||
where
|
where
|
||||||
re = mkRegex "(\\[.*\\])"
|
-- foo[bar] gets parsed with [bar] as a glob, so undo that
|
||||||
|
toStr (T_Glob _ s) = return s
|
||||||
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
|
-- Turn foo[$x] into foo[\0] so that we can get the constant array name
|
||||||
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
|
-- in a non-constant expression (while filtering out foo$x[$y])
|
||||||
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
|
toStr _ = return "\0"
|
||||||
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
|
|
||||||
getOffsetReferences mods = fromMaybe [] $ do
|
|
||||||
-- if mods start with [, then drop until ]
|
|
||||||
match <- matchRegex re mods
|
|
||||||
offsets <- match !!! 1
|
|
||||||
return $ matchAllStrings variableNameRegex offsets
|
|
||||||
where
|
|
||||||
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
|
|
||||||
|
|
||||||
getReferencedVariables parents t =
|
getReferencedVariables parents t =
|
||||||
case t of
|
case t of
|
||||||
T_DollarBraced id _ l -> let str = bracedString t in
|
T_DollarBraced id _ l -> let str = concat $ oversimplify l in
|
||||||
(t, t, getBracedReference str) :
|
(t, t, getBracedReference str) :
|
||||||
map (\x -> (l, l, x)) (
|
map (\x -> (l, l, x)) (
|
||||||
getIndexReferences str
|
getIndexReferences str
|
||||||
|
@ -728,7 +798,7 @@ getReferencedVariables parents t =
|
||||||
TC_Unary id _ "-v" token -> getIfReference t token
|
TC_Unary id _ "-v" token -> getIfReference t token
|
||||||
TC_Unary id _ "-R" token -> getIfReference t token
|
TC_Unary id _ "-R" token -> getIfReference t token
|
||||||
TC_Binary id DoubleBracket op lhs rhs ->
|
TC_Binary id DoubleBracket op lhs rhs ->
|
||||||
if isDereferencing op
|
if isDereferencingBinaryOp op
|
||||||
then concatMap (getIfReference t) [lhs, rhs]
|
then concatMap (getIfReference t) [lhs, rhs]
|
||||||
else []
|
else []
|
||||||
|
|
||||||
|
@ -738,7 +808,7 @@ getReferencedVariables parents t =
|
||||||
(t, t, "output")
|
(t, t, "output")
|
||||||
]
|
]
|
||||||
|
|
||||||
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
|
T_FdRedirect _ ('{':var) op -> -- {foo}>&- references and closes foo
|
||||||
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
|
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
|
||||||
x -> getReferencedVariableCommand x
|
x -> getReferencedVariableCommand x
|
||||||
where
|
where
|
||||||
|
@ -755,19 +825,18 @@ getReferencedVariables parents t =
|
||||||
|
|
||||||
literalizer t = case t of
|
literalizer t = case t of
|
||||||
T_Glob _ s -> return s -- Also when parsed as globs
|
T_Glob _ s -> return s -- Also when parsed as globs
|
||||||
_ -> Nothing
|
_ -> []
|
||||||
|
|
||||||
getIfReference context token = maybeToList $ do
|
getIfReference context token = maybeToList $ do
|
||||||
str@(h:_) <- getLiteralStringExt literalizer token
|
str <- getVariableForTestDashV token
|
||||||
when (isDigit h) $ fail "is a number"
|
|
||||||
return (context, token, getBracedReference str)
|
return (context, token, getBracedReference str)
|
||||||
|
|
||||||
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
|
|
||||||
|
|
||||||
isArithmeticAssignment t = case getPath parents t of
|
isArithmeticAssignment t = case getPath parents t of
|
||||||
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
|
this NE.:| TA_Assignment _ "=" lhs _ :_ -> lhs == t
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
|
isDereferencingBinaryOp = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
|
||||||
|
|
||||||
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
|
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
|
||||||
|
|
||||||
|
|
||||||
|
@ -790,16 +859,6 @@ isConfusedGlobRegex ('*':_) = True
|
||||||
isConfusedGlobRegex [x,'*'] | x `notElem` "\\." = True
|
isConfusedGlobRegex [x,'*'] | x `notElem` "\\." = True
|
||||||
isConfusedGlobRegex _ = False
|
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 =
|
getVariablesFromLiteralToken token =
|
||||||
getVariablesFromLiteral (getLiteralStringDef " " token)
|
getVariablesFromLiteral (getLiteralStringDef " " token)
|
||||||
|
|
||||||
|
@ -808,77 +867,15 @@ getVariablesFromLiteralToken token =
|
||||||
prop_getVariablesFromLiteral1 =
|
prop_getVariablesFromLiteral1 =
|
||||||
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
|
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
|
||||||
getVariablesFromLiteral string =
|
getVariablesFromLiteral string =
|
||||||
map (!! 0) $ matchAllSubgroups variableRegex string
|
map head $ matchAllSubgroups variableRegex string
|
||||||
where
|
where
|
||||||
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
|
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
|
||||||
|
|
||||||
-- Get the variable name from an expansion like ${var:-foo}
|
|
||||||
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 = headOrDefault "" $ 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.
|
|
||||||
|
|
||||||
-- Get element 0 or a default. Like `head` but safe.
|
|
||||||
headOrDefault _ (a:_) = a
|
|
||||||
headOrDefault def _ = def
|
|
||||||
|
|
||||||
--- Get element n of a list, or Nothing. Like `!!` but safe.
|
|
||||||
(!!!) list i =
|
|
||||||
case drop i list of
|
|
||||||
[] -> Nothing
|
|
||||||
(r:_) -> Just r
|
|
||||||
|
|
||||||
-- Run a command if the shell is in the given list
|
-- Run a command if the shell is in the given list
|
||||||
whenShell l c = do
|
whenShell l c = do
|
||||||
shell <- asks shellType
|
params <- ask
|
||||||
when (shell `elem` l ) c
|
when (shellType params `elem` l ) c
|
||||||
|
|
||||||
|
|
||||||
filterByAnnotation asSpec params =
|
filterByAnnotation asSpec params =
|
||||||
|
@ -907,54 +904,41 @@ isCountingReference _ = False
|
||||||
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
|
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
|
||||||
isQuotedAlternativeReference t =
|
isQuotedAlternativeReference t =
|
||||||
case t of
|
case t of
|
||||||
T_DollarBraced _ _ _ ->
|
T_DollarBraced _ _ l ->
|
||||||
getBracedModifier (bracedString t) `matches` re
|
getBracedModifier (concat $ oversimplify l) `matches` re
|
||||||
_ -> False
|
_ -> False
|
||||||
where
|
where
|
||||||
re = mkRegex "(^|\\]):?\\+"
|
re = mkRegex "(^|\\]):?\\+"
|
||||||
|
|
||||||
-- getGnuOpts "erd:u:" will parse a SimpleCommand like
|
supportsArrays Bash = True
|
||||||
-- read -re -d : -u 3 bar
|
supportsArrays Ksh = True
|
||||||
-- into
|
supportsArrays _ = False
|
||||||
-- Just [("r", -re), ("e", -re), ("d", :), ("u", 3), ("", bar)]
|
|
||||||
-- where flags with arguments map to arguments, while others map to themselves.
|
isTrueAssignmentSource c =
|
||||||
-- Any unrecognized flag will result in Nothing.
|
case c of
|
||||||
getGnuOpts str t = getOpts str $ getAllFlags t
|
DataString SourceChecked -> False
|
||||||
getBsdOpts str t = getOpts str $ getLeadingFlags t
|
DataString SourceDeclaration -> False
|
||||||
getOpts :: String -> [(Token, String)] -> Maybe [(String, Token)]
|
DataArray SourceChecked -> False
|
||||||
getOpts string flags = process flags
|
DataArray SourceDeclaration -> False
|
||||||
|
_ -> True
|
||||||
|
|
||||||
|
modifiesVariable params token name =
|
||||||
|
or $ map check flow
|
||||||
where
|
where
|
||||||
flagList (c:':':rest) = ([c], True) : flagList rest
|
flow = getVariableFlow params token
|
||||||
flagList (c:rest) = ([c], False) : flagList rest
|
check t =
|
||||||
flagList [] = []
|
case t of
|
||||||
flagMap = Map.fromList $ ("", False) : flagList string
|
Assignment (_, _, n, source) -> isTrueAssignmentSource source && n == name
|
||||||
|
_ -> False
|
||||||
|
|
||||||
process [] = return []
|
isTestCommand t =
|
||||||
process [(token, flag)] = do
|
case t of
|
||||||
takesArg <- Map.lookup flag flagMap
|
T_Condition {} -> True
|
||||||
guard $ not takesArg
|
T_SimpleCommand {} -> t `isCommand` "test"
|
||||||
return [(flag, token)]
|
T_Redirecting _ _ t -> isTestCommand t
|
||||||
process ((token1, flag1):rest2@((token2, flag2):rest)) = do
|
T_Annotation _ _ t -> isTestCommand t
|
||||||
takesArg <- Map.lookup flag1 flagMap
|
T_Pipeline _ _ [t] -> isTestCommand t
|
||||||
if takesArg
|
_ -> False
|
||||||
then do
|
|
||||||
guard $ null flag2
|
|
||||||
more <- process rest
|
|
||||||
return $ (flag1, token2) : more
|
|
||||||
else do
|
|
||||||
more <- process rest2
|
|
||||||
return $ (flag1, token1) : more
|
|
||||||
|
|
||||||
supportsArrays shell = shell == Bash || shell == Ksh
|
|
||||||
|
|
||||||
-- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh)
|
|
||||||
isBashLike :: Parameters -> Bool
|
|
||||||
isBashLike params =
|
|
||||||
case shellType params of
|
|
||||||
Bash -> True
|
|
||||||
Ksh -> True
|
|
||||||
Dash -> False
|
|
||||||
Sh -> False
|
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||||
|
|
1316
src/ShellCheck/CFG.hs
Normal file
1316
src/ShellCheck/CFG.hs
Normal file
File diff suppressed because it is too large
Load diff
1439
src/ShellCheck/CFGAnalysis.hs
Normal file
1439
src/ShellCheck/CFGAnalysis.hs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
||||||
{-
|
{-
|
||||||
Copyright 2012-2019 Vidar Holen
|
Copyright 2012-2022 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
https://www.shellcheck.net
|
https://www.shellcheck.net
|
||||||
|
@ -20,10 +20,12 @@
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
|
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
|
||||||
|
|
||||||
|
import ShellCheck.Analyzer
|
||||||
|
import ShellCheck.ASTLib
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Parser
|
import ShellCheck.Parser
|
||||||
import ShellCheck.Analyzer
|
|
||||||
|
|
||||||
|
import Debug.Trace -- DO NOT SUBMIT
|
||||||
import Data.Either
|
import Data.Either
|
||||||
import Data.Functor
|
import Data.Functor
|
||||||
import Data.List
|
import Data.List
|
||||||
|
@ -85,7 +87,8 @@ checkScript sys spec = do
|
||||||
asCheckSourced = csCheckSourced spec,
|
asCheckSourced = csCheckSourced spec,
|
||||||
asExecutionMode = Executed,
|
asExecutionMode = Executed,
|
||||||
asTokenPositions = tokenPositions,
|
asTokenPositions = tokenPositions,
|
||||||
asOptionalChecks = csOptionalChecks spec
|
asExtendedAnalysis = csExtendedAnalysis spec,
|
||||||
|
asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec
|
||||||
} where as = newAnalysisSpec root
|
} where as = newAnalysisSpec root
|
||||||
let analysisMessages =
|
let analysisMessages =
|
||||||
maybe []
|
maybe []
|
||||||
|
@ -156,6 +159,11 @@ checkWithIncludesAndSourcePath includes mapper = getErrors
|
||||||
siFindSource = mapper
|
siFindSource = mapper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkWithRcIncludesAndSourcePath rc includes mapper = getErrors
|
||||||
|
(mockRcFile rc $ mockedSystemInterface includes) {
|
||||||
|
siFindSource = mapper
|
||||||
|
}
|
||||||
|
|
||||||
prop_findsParseIssue = check "echo \"$12\"" == [1037]
|
prop_findsParseIssue = check "echo \"$12\"" == [1037]
|
||||||
|
|
||||||
prop_commentDisablesParseIssue1 =
|
prop_commentDisablesParseIssue1 =
|
||||||
|
@ -229,9 +237,18 @@ prop_cantSourceDynamic =
|
||||||
prop_cantSourceDynamic2 =
|
prop_cantSourceDynamic2 =
|
||||||
[1090] == checkWithIncludes [("lib", "")] "source ~/foo"
|
[1090] == checkWithIncludes [("lib", "")] "source ~/foo"
|
||||||
|
|
||||||
|
prop_canStripPrefixAndSource =
|
||||||
|
null $ checkWithIncludes [("./lib", "")] "source \"$MYDIR/lib\""
|
||||||
|
|
||||||
|
prop_canStripPrefixAndSource2 =
|
||||||
|
null $ checkWithIncludes [("./utils.sh", "")] "source \"$(dirname \"${BASH_SOURCE[0]}\")/utils.sh\""
|
||||||
|
|
||||||
prop_canSourceDynamicWhenRedirected =
|
prop_canSourceDynamicWhenRedirected =
|
||||||
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
|
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
|
||||||
|
|
||||||
|
prop_canRedirectWithSpaces =
|
||||||
|
null $ checkWithIncludes [("my file", "")] "#shellcheck source=\"my file\"\n. \"$1\""
|
||||||
|
|
||||||
prop_recursiveAnalysis =
|
prop_recursiveAnalysis =
|
||||||
[2086] == checkRecursive [("lib", "echo $1")] "source lib"
|
[2086] == checkRecursive [("lib", "echo $1")] "source lib"
|
||||||
|
|
||||||
|
@ -270,7 +287,7 @@ prop_filewideAnnotation8 = null $
|
||||||
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
|
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
|
||||||
|
|
||||||
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
|
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
|
||||||
2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
|
3046 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
|
||||||
|
|
||||||
prop_spinBug1413 = null $ check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n"
|
prop_spinBug1413 = null $ check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n"
|
||||||
|
|
||||||
|
@ -295,6 +312,20 @@ prop_canDisableShebangWarning = null $ result
|
||||||
csScript = "#shellcheck disable=SC2148\nfoo"
|
csScript = "#shellcheck disable=SC2148\nfoo"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prop_canDisableAllWarnings = result == [2086]
|
||||||
|
where
|
||||||
|
result = checkWithSpec [] emptyCheckSpec {
|
||||||
|
csFilename = "file.sh",
|
||||||
|
csScript = "#!/bin/sh\necho $1\n#shellcheck disable=all\necho `echo $1`"
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_canDisableParseErrors = null $ result
|
||||||
|
where
|
||||||
|
result = checkWithSpec [] emptyCheckSpec {
|
||||||
|
csFilename = "file.sh",
|
||||||
|
csScript = "#shellcheck disable=SC1073,SC1072,SC2148\n()"
|
||||||
|
}
|
||||||
|
|
||||||
prop_shExtensionDoesntMatter = result == [2148]
|
prop_shExtensionDoesntMatter = result == [2148]
|
||||||
where
|
where
|
||||||
result = checkWithSpec [] emptyCheckSpec {
|
result = checkWithSpec [] emptyCheckSpec {
|
||||||
|
@ -371,7 +402,7 @@ prop_canEnableOptionalsWithRc = result == [2244]
|
||||||
|
|
||||||
prop_sourcePathRedirectsName = result == [2086]
|
prop_sourcePathRedirectsName = result == [2086]
|
||||||
where
|
where
|
||||||
f "dir/myscript" _ "lib" = return "foo/lib"
|
f "dir/myscript" _ _ "lib" = return "foo/lib"
|
||||||
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
|
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
|
||||||
csScript = "#!/bin/bash\nsource lib",
|
csScript = "#!/bin/bash\nsource lib",
|
||||||
csFilename = "dir/myscript",
|
csFilename = "dir/myscript",
|
||||||
|
@ -380,22 +411,154 @@ prop_sourcePathRedirectsName = result == [2086]
|
||||||
|
|
||||||
prop_sourcePathAddsAnnotation = result == [2086]
|
prop_sourcePathAddsAnnotation = result == [2086]
|
||||||
where
|
where
|
||||||
f "dir/myscript" ["mypath"] "lib" = return "foo/lib"
|
f "dir/myscript" _ ["mypath"] "lib" = return "foo/lib"
|
||||||
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
|
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
|
||||||
csScript = "#!/bin/bash\n# shellcheck source-path=mypath\nsource lib",
|
csScript = "#!/bin/bash\n# shellcheck source-path=mypath\nsource lib",
|
||||||
csFilename = "dir/myscript",
|
csFilename = "dir/myscript",
|
||||||
csCheckSourced = True
|
csCheckSourced = True
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prop_sourcePathWorksWithSpaces = result == [2086]
|
||||||
|
where
|
||||||
|
f "dir/myscript" _ ["my path"] "lib" = return "foo/lib"
|
||||||
|
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
|
||||||
|
csScript = "#!/bin/bash\n# shellcheck source-path='my path'\nsource lib",
|
||||||
|
csFilename = "dir/myscript",
|
||||||
|
csCheckSourced = True
|
||||||
|
}
|
||||||
|
|
||||||
prop_sourcePathRedirectsDirective = result == [2086]
|
prop_sourcePathRedirectsDirective = result == [2086]
|
||||||
where
|
where
|
||||||
f "dir/myscript" _ "lib" = return "foo/lib"
|
f "dir/myscript" _ _ "lib" = return "foo/lib"
|
||||||
f _ _ _ = return "/dev/null"
|
f _ _ _ _ = return "/dev/null"
|
||||||
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
|
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
|
||||||
csScript = "#!/bin/bash\n# shellcheck source=lib\nsource kittens",
|
csScript = "#!/bin/bash\n# shellcheck source=lib\nsource kittens",
|
||||||
csFilename = "dir/myscript",
|
csFilename = "dir/myscript",
|
||||||
csCheckSourced = True
|
csCheckSourced = True
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prop_rcCanAllowExternalSources = result == [2086]
|
||||||
|
where
|
||||||
|
f "dir/myscript" (Just True) _ "mylib" = return "resolved/mylib"
|
||||||
|
f a b c d = error $ show ("Unexpected", a, b, c, d)
|
||||||
|
result = checkWithRcIncludesAndSourcePath "external-sources=true" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
|
||||||
|
csScript = "#!/bin/bash\nsource mylib",
|
||||||
|
csFilename = "dir/myscript",
|
||||||
|
csCheckSourced = True
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_rcCanDenyExternalSources = result == [2086]
|
||||||
|
where
|
||||||
|
f "dir/myscript" (Just False) _ "mylib" = return "resolved/mylib"
|
||||||
|
f a b c d = error $ show ("Unexpected", a, b, c, d)
|
||||||
|
result = checkWithRcIncludesAndSourcePath "external-sources=false" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
|
||||||
|
csScript = "#!/bin/bash\nsource mylib",
|
||||||
|
csFilename = "dir/myscript",
|
||||||
|
csCheckSourced = True
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_rcCanLeaveExternalSourcesUnspecified = result == [2086]
|
||||||
|
where
|
||||||
|
f "dir/myscript" Nothing _ "mylib" = return "resolved/mylib"
|
||||||
|
f a b c d = error $ show ("Unexpected", a, b, c, d)
|
||||||
|
result = checkWithRcIncludesAndSourcePath "" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
|
||||||
|
csScript = "#!/bin/bash\nsource mylib",
|
||||||
|
csFilename = "dir/myscript",
|
||||||
|
csCheckSourced = True
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_fileCanDisableExternalSources = result == [2006, 2086]
|
||||||
|
where
|
||||||
|
f "dir/myscript" (Just True) _ "withExternal" = return "withExternal"
|
||||||
|
f "dir/myscript" (Just False) _ "withoutExternal" = return "withoutExternal"
|
||||||
|
f a b c d = error $ show ("Unexpected", a, b, c, d)
|
||||||
|
result = checkWithRcIncludesAndSourcePath "external-sources=true" [("withExternal", "echo $1"), ("withoutExternal", "_=`foo`")] f emptyCheckSpec {
|
||||||
|
csScript = "#!/bin/bash\ntrue\nsource withExternal\n# shellcheck external-sources=false\nsource withoutExternal",
|
||||||
|
csFilename = "dir/myscript",
|
||||||
|
csCheckSourced = True
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_fileCannotEnableExternalSources = result == [1144]
|
||||||
|
where
|
||||||
|
f "dir/myscript" Nothing _ "foo" = return "foo"
|
||||||
|
f a b c d = error $ show ("Unexpected", a, b, c, d)
|
||||||
|
result = checkWithRcIncludesAndSourcePath "" [("foo", "true")] f emptyCheckSpec {
|
||||||
|
csScript = "#!/bin/bash\n# shellcheck external-sources=true\nsource foo",
|
||||||
|
csFilename = "dir/myscript",
|
||||||
|
csCheckSourced = True
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_fileCannotEnableExternalSources2 = result == [1144]
|
||||||
|
where
|
||||||
|
f "dir/myscript" (Just False) _ "foo" = return "foo"
|
||||||
|
f a b c d = error $ show ("Unexpected", a, b, c, d)
|
||||||
|
result = checkWithRcIncludesAndSourcePath "external-sources=false" [("foo", "true")] f emptyCheckSpec {
|
||||||
|
csScript = "#!/bin/bash\n# shellcheck external-sources=true\nsource foo",
|
||||||
|
csFilename = "dir/myscript",
|
||||||
|
csCheckSourced = True
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_rcCanSuppressEarlyProblems1 = null result
|
||||||
|
where
|
||||||
|
result = checkWithRc "disable=1071" emptyCheckSpec {
|
||||||
|
csScript = "#!/bin/zsh\necho $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_rcCanSuppressEarlyProblems2 = null result
|
||||||
|
where
|
||||||
|
result = checkWithRc "disable=1104" emptyCheckSpec {
|
||||||
|
csScript = "!/bin/bash\necho 'hello world'"
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_sourceWithHereDocWorks = null result
|
||||||
|
where
|
||||||
|
result = checkWithIncludes [("bar", "true\n")] "source bar << eof\nlol\neof"
|
||||||
|
|
||||||
|
prop_hereDocsAreParsedWithoutTrailingLinefeed = 1044 `elem` result
|
||||||
|
where
|
||||||
|
result = check "cat << eof"
|
||||||
|
|
||||||
|
prop_hereDocsWillHaveParsedIndices = null result
|
||||||
|
where
|
||||||
|
result = check "#!/bin/bash\nmy_array=(a b)\ncat <<EOF >> ./test\n $(( 1 + my_array[1] ))\nEOF"
|
||||||
|
|
||||||
|
prop_rcCanSuppressDfa = null result
|
||||||
|
where
|
||||||
|
result = checkWithRc "extended-analysis=false" emptyCheckSpec {
|
||||||
|
csScript = "#!/bin/sh\nexit; foo;"
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_fileCanSuppressDfa = null $ traceShowId result
|
||||||
|
where
|
||||||
|
result = checkWithRc "" emptyCheckSpec {
|
||||||
|
csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;"
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_fileWinsWhenSuppressingDfa1 = null result
|
||||||
|
where
|
||||||
|
result = checkWithRc "extended-analysis=true" emptyCheckSpec {
|
||||||
|
csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;"
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_fileWinsWhenSuppressingDfa2 = result == [2317]
|
||||||
|
where
|
||||||
|
result = checkWithRc "extended-analysis=false" emptyCheckSpec {
|
||||||
|
csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;"
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_flagWinsWhenSuppressingDfa1 = result == [2317]
|
||||||
|
where
|
||||||
|
result = checkWithRc "extended-analysis=false" emptyCheckSpec {
|
||||||
|
csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;",
|
||||||
|
csExtendedAnalysis = Just True
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_flagWinsWhenSuppressingDfa2 = null result
|
||||||
|
where
|
||||||
|
result = checkWithRc "extended-analysis=true" emptyCheckSpec {
|
||||||
|
csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;",
|
||||||
|
csExtendedAnalysis = Just False
|
||||||
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $quickCheckAll
|
runTests = $quickCheckAll
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{-
|
{-
|
||||||
Copyright 2012-2019 Vidar Holen
|
Copyright 2012-2022 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
https://www.shellcheck.net
|
https://www.shellcheck.net
|
||||||
|
@ -19,6 +19,8 @@
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE MultiWayIf #-}
|
||||||
|
{-# LANGUAGE PatternGuards #-}
|
||||||
|
|
||||||
-- This module contains checks that examine specific commands by name.
|
-- This module contains checks that examine specific commands by name.
|
||||||
module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where
|
module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where
|
||||||
|
@ -26,21 +28,29 @@ module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Co
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
import ShellCheck.ASTLib
|
import ShellCheck.ASTLib
|
||||||
import ShellCheck.AnalyzerLib
|
import ShellCheck.AnalyzerLib
|
||||||
|
import ShellCheck.CFG
|
||||||
|
import qualified ShellCheck.CFGAnalysis as CF
|
||||||
import ShellCheck.Data
|
import ShellCheck.Data
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Parser
|
import ShellCheck.Parser
|
||||||
|
import ShellCheck.Prelude
|
||||||
import ShellCheck.Regex
|
import ShellCheck.Regex
|
||||||
|
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Control.Monad.RWS
|
import Control.Monad.RWS
|
||||||
import Data.Char
|
import Data.Char
|
||||||
import Data.Functor.Identity
|
import Data.Functor.Identity
|
||||||
|
import qualified Data.Graph.Inductive.Graph as G
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import qualified Data.Map.Strict as Map
|
import qualified Data.List.NonEmpty as NE
|
||||||
|
import qualified Data.Map.Strict as M
|
||||||
|
import qualified Data.Set as S
|
||||||
import Test.QuickCheck.All (forAllProperties)
|
import Test.QuickCheck.All (forAllProperties)
|
||||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||||
|
|
||||||
|
import Debug.Trace -- STRIP
|
||||||
|
|
||||||
data CommandName = Exactly String | Basename String
|
data CommandName = Exactly String | Basename String
|
||||||
deriving (Eq, Ord)
|
deriving (Eq, Ord)
|
||||||
|
|
||||||
|
@ -52,13 +62,11 @@ verify :: CommandCheck -> String -> Bool
|
||||||
verify f s = producesComments (getChecker [f]) s == Just True
|
verify f s = producesComments (getChecker [f]) s == Just True
|
||||||
verifyNot f s = producesComments (getChecker [f]) s == Just False
|
verifyNot f s = producesComments (getChecker [f]) s == Just False
|
||||||
|
|
||||||
arguments (T_SimpleCommand _ _ (cmd:args)) = args
|
|
||||||
|
|
||||||
commandChecks :: [CommandCheck]
|
commandChecks :: [CommandCheck]
|
||||||
commandChecks = [
|
commandChecks = [
|
||||||
checkTr
|
checkTr
|
||||||
,checkFindNameGlob
|
,checkFindNameGlob
|
||||||
,checkNeedlessExpr
|
,checkExpr
|
||||||
,checkGrepRe
|
,checkGrepRe
|
||||||
,checkTrapQuotes
|
,checkTrapQuotes
|
||||||
,checkReturn
|
,checkReturn
|
||||||
|
@ -95,7 +103,15 @@ commandChecks = [
|
||||||
,checkSudoArgs
|
,checkSudoArgs
|
||||||
,checkSourceArgs
|
,checkSourceArgs
|
||||||
,checkChmodDashr
|
,checkChmodDashr
|
||||||
|
,checkXargsDashi
|
||||||
|
,checkUnquotedEchoSpaces
|
||||||
|
,checkEvalArray
|
||||||
]
|
]
|
||||||
|
++ map checkArgComparison ("alias" : declaringCommands)
|
||||||
|
++ map checkMaskedReturns declaringCommands
|
||||||
|
++ map checkMultipleDeclaring declaringCommands
|
||||||
|
++ map checkBackreferencingDeclaration declaringCommands
|
||||||
|
|
||||||
|
|
||||||
optionalChecks = map fst optionalCommandChecks
|
optionalChecks = map fst optionalCommandChecks
|
||||||
optionalCommandChecks :: [(CheckDescription, CommandCheck)]
|
optionalCommandChecks :: [(CheckDescription, CommandCheck)]
|
||||||
|
@ -107,7 +123,7 @@ optionalCommandChecks = [
|
||||||
cdNegative = "command -v javac"
|
cdNegative = "command -v javac"
|
||||||
}, checkWhich)
|
}, checkWhich)
|
||||||
]
|
]
|
||||||
optionalCheckMap = Map.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks
|
optionalCheckMap = M.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks
|
||||||
|
|
||||||
prop_verifyOptionalExamples = all check optionalCommandChecks
|
prop_verifyOptionalExamples = all check optionalCommandChecks
|
||||||
where
|
where
|
||||||
|
@ -115,27 +131,67 @@ prop_verifyOptionalExamples = all check optionalCommandChecks
|
||||||
verify check (cdPositive desc)
|
verify check (cdPositive desc)
|
||||||
&& verifyNot check (cdNegative desc)
|
&& verifyNot check (cdNegative desc)
|
||||||
|
|
||||||
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
-- Run a check against the getopt parser. If it fails, the lists are empty.
|
||||||
buildCommandMap = foldl' addCheck Map.empty
|
checkGetOpts str flags args f =
|
||||||
|
flags == actualFlags && args == actualArgs
|
||||||
|
where
|
||||||
|
toTokens = map (T_Literal (Id 0)) . words
|
||||||
|
opts = fromMaybe [] $ f (toTokens str)
|
||||||
|
actualFlags = filter (not . null) $ map fst opts
|
||||||
|
actualArgs = [onlyLiteralString x | ("", (_, x)) <- opts]
|
||||||
|
|
||||||
|
-- Short options
|
||||||
|
prop_checkGetOptsS1 = checkGetOpts "-f x" ["f"] [] $ getOpts (True, True) "f:" []
|
||||||
|
prop_checkGetOptsS2 = checkGetOpts "-fx" ["f"] [] $ getOpts (True, True) "f:" []
|
||||||
|
prop_checkGetOptsS3 = checkGetOpts "-f -x" ["f", "x"] [] $ getOpts (True, True) "fx" []
|
||||||
|
prop_checkGetOptsS4 = checkGetOpts "-f -x" ["f"] [] $ getOpts (True, True) "f:" []
|
||||||
|
prop_checkGetOptsS5 = checkGetOpts "-fx" [] [] $ getOpts (True, True) "fx:" []
|
||||||
|
|
||||||
|
prop_checkGenericOptsS1 = checkGetOpts "-f x" ["f"] [] $ return . getGenericOpts
|
||||||
|
prop_checkGenericOptsS2 = checkGetOpts "-abc x" ["a", "b", "c"] [] $ return . getGenericOpts
|
||||||
|
prop_checkGenericOptsS3 = checkGetOpts "-abc -x" ["a", "b", "c", "x"] [] $ return . getGenericOpts
|
||||||
|
prop_checkGenericOptsS4 = checkGetOpts "-x" ["x"] [] $ return . getGenericOpts
|
||||||
|
|
||||||
|
-- Long options
|
||||||
|
prop_checkGetOptsL1 = checkGetOpts "--foo=bar baz" ["foo"] ["baz"] $ getOpts (True, False) "" [("foo", True)]
|
||||||
|
prop_checkGetOptsL2 = checkGetOpts "--foo bar baz" ["foo"] ["baz"] $ getOpts (True, False) "" [("foo", True)]
|
||||||
|
prop_checkGetOptsL3 = checkGetOpts "--foo baz" ["foo"] ["baz"] $ getOpts (True, True) "" []
|
||||||
|
prop_checkGetOptsL4 = checkGetOpts "--foo baz" [] [] $ getOpts (True, False) "" []
|
||||||
|
|
||||||
|
prop_checkGenericOptsL1 = checkGetOpts "--foo=bar" ["foo"] [] $ return . getGenericOpts
|
||||||
|
prop_checkGenericOptsL2 = checkGetOpts "--foo bar" ["foo"] ["bar"] $ return . getGenericOpts
|
||||||
|
prop_checkGenericOptsL3 = checkGetOpts "-x --foo" ["x", "foo"] [] $ return . getGenericOpts
|
||||||
|
|
||||||
|
-- Know when to terminate
|
||||||
|
prop_checkGetOptsT1 = checkGetOpts "-a x -b" ["a", "b"] ["x"] $ getOpts (True, True) "ab" []
|
||||||
|
prop_checkGetOptsT2 = checkGetOpts "-a x -b" ["a"] ["x","-b"] $ getOpts (False, True) "ab" []
|
||||||
|
prop_checkGetOptsT3 = checkGetOpts "-a -- -b" ["a"] ["-b"] $ getOpts (True, True) "ab" []
|
||||||
|
prop_checkGetOptsT4 = checkGetOpts "-a -- -b" ["a", "b"] [] $ getOpts (True, True) "a:b" []
|
||||||
|
|
||||||
|
prop_checkGenericOptsT1 = checkGetOpts "-x -- -y" ["x"] ["-y"] $ return . getGenericOpts
|
||||||
|
prop_checkGenericOptsT2 = checkGetOpts "-xy --" ["x", "y"] [] $ return . getGenericOpts
|
||||||
|
|
||||||
|
|
||||||
|
buildCommandMap :: [CommandCheck] -> M.Map CommandName (Token -> Analysis)
|
||||||
|
buildCommandMap = foldl' addCheck M.empty
|
||||||
where
|
where
|
||||||
addCheck map (CommandCheck name function) =
|
addCheck map (CommandCheck name function) =
|
||||||
Map.insertWith composeAnalyzers name function map
|
M.insertWith composeAnalyzers name function map
|
||||||
|
|
||||||
|
|
||||||
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
|
checkCommand :: M.Map CommandName (Token -> Analysis) -> Token -> Analysis
|
||||||
checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do
|
checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do
|
||||||
name <- getLiteralString cmd
|
name <- getLiteralString cmd
|
||||||
return $
|
return $
|
||||||
if '/' `elem` name
|
if | '/' `elem` name ->
|
||||||
then
|
M.findWithDefault nullCheck (Basename $ basename name) map t
|
||||||
Map.findWithDefault nullCheck (Basename $ basename name) map t
|
| name == "builtin", (h:_) <- rest ->
|
||||||
else if name == "builtin" && not (null rest) then
|
|
||||||
let t' = T_SimpleCommand id cmdPrefix rest
|
let t' = T_SimpleCommand id cmdPrefix rest
|
||||||
selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest
|
selectedBuiltin = onlyLiteralString h
|
||||||
in Map.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
|
in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
|
||||||
else do
|
| otherwise -> do
|
||||||
Map.findWithDefault nullCheck (Exactly name) map t
|
M.findWithDefault nullCheck (Exactly name) map t
|
||||||
Map.findWithDefault nullCheck (Basename name) map t
|
M.findWithDefault nullCheck (Basename name) map t
|
||||||
|
|
||||||
where
|
where
|
||||||
basename = reverse . takeWhile (/= '/') . reverse
|
basename = reverse . takeWhile (/= '/') . reverse
|
||||||
|
@ -157,7 +213,7 @@ checker spec params = getChecker $ commandChecks ++ optionals
|
||||||
optionals =
|
optionals =
|
||||||
if "all" `elem` keys
|
if "all" `elem` keys
|
||||||
then map snd optionalCommandChecks
|
then map snd optionalCommandChecks
|
||||||
else mapMaybe (\x -> Map.lookup x optionalCheckMap) keys
|
else mapMaybe (\x -> M.lookup x optionalCheckMap) keys
|
||||||
|
|
||||||
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
|
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
|
||||||
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
|
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
|
||||||
|
@ -199,27 +255,81 @@ prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"
|
||||||
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
|
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
|
||||||
acceptsGlob s = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
|
acceptsGlob s = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
|
||||||
f [] = return ()
|
f [] = return ()
|
||||||
f (x:xs) = g x xs
|
f (x:xs) = foldr g (const $ return ()) xs x
|
||||||
g _ [] = return ()
|
g b acc a = do
|
||||||
g a (b:r) = do
|
|
||||||
forM_ (getLiteralString a) $ \s -> when (acceptsGlob s && isGlob b) $
|
forM_ (getLiteralString a) $ \s -> when (acceptsGlob s && isGlob b) $
|
||||||
warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."
|
warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."
|
||||||
g b r
|
acc b
|
||||||
|
|
||||||
|
|
||||||
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
|
prop_checkExpr = verify checkExpr "foo=$(expr 3 + 2)"
|
||||||
prop_checkNeedlessExpr2 = verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``"
|
prop_checkExpr2 = verify checkExpr "foo=`echo \\`expr 3 + 2\\``"
|
||||||
prop_checkNeedlessExpr3 = verifyNot checkNeedlessExpr "foo=$(expr foo : regex)"
|
prop_checkExpr3 = verifyNot checkExpr "foo=$(expr foo : regex)"
|
||||||
prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)"
|
prop_checkExpr4 = verifyNot checkExpr "foo=$(expr foo \\< regex)"
|
||||||
checkNeedlessExpr = CommandCheck (Basename "expr") f where
|
prop_checkExpr5 = verify checkExpr "# shellcheck disable=SC2003\nexpr match foo bar"
|
||||||
f t =
|
prop_checkExpr6 = verify checkExpr "# shellcheck disable=SC2003\nexpr foo : fo*"
|
||||||
|
prop_checkExpr7 = verify checkExpr "# shellcheck disable=SC2003\nexpr 5 -3"
|
||||||
|
prop_checkExpr8 = verifyNot checkExpr "# shellcheck disable=SC2003\nexpr \"$@\""
|
||||||
|
prop_checkExpr9 = verifyNot checkExpr "# shellcheck disable=SC2003\nexpr 5 $rest"
|
||||||
|
prop_checkExpr10 = verify checkExpr "# shellcheck disable=SC2003\nexpr length \"$var\""
|
||||||
|
prop_checkExpr11 = verify checkExpr "# shellcheck disable=SC2003\nexpr foo > bar"
|
||||||
|
prop_checkExpr12 = verify checkExpr "# shellcheck disable=SC2003\nexpr 1 | 2"
|
||||||
|
prop_checkExpr13 = verify checkExpr "# shellcheck disable=SC2003\nexpr 1 * 2"
|
||||||
|
prop_checkExpr14 = verify checkExpr "# shellcheck disable=SC2003\nexpr \"$x\" >= \"$y\""
|
||||||
|
|
||||||
|
checkExpr = CommandCheck (Basename "expr") f where
|
||||||
|
f t = do
|
||||||
when (all (`notElem` exceptions) (words $ arguments t)) $
|
when (all (`notElem` exceptions) (words $ arguments t)) $
|
||||||
style (getId $ getCommandTokenOrThis t) 2003
|
style (getId $ getCommandTokenOrThis t) 2003
|
||||||
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
|
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
|
||||||
|
|
||||||
|
case arguments t of
|
||||||
|
[lhs, op, rhs] -> do
|
||||||
|
checkOp lhs
|
||||||
|
case getWordParts op of
|
||||||
|
[T_Glob _ "*"] ->
|
||||||
|
err (getId op) 2304
|
||||||
|
"* must be escaped to multiply: \\*. Modern $((x * y)) avoids this issue."
|
||||||
|
[T_Literal _ ":"] | isGlob rhs ->
|
||||||
|
warn (getId rhs) 2305
|
||||||
|
"Quote regex argument to expr to avoid it expanding as a glob."
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
|
[single] | not (willSplit single) ->
|
||||||
|
warn (getId single) 2307
|
||||||
|
"'expr' expects 3+ arguments but sees 1. Make sure each operator/operand is a separate argument, and escape <>&|."
|
||||||
|
|
||||||
|
[first, second] |
|
||||||
|
onlyLiteralString first /= "length"
|
||||||
|
&& not (willSplit first || willSplit second) -> do
|
||||||
|
checkOp first
|
||||||
|
warn (getId t) 2307
|
||||||
|
"'expr' expects 3+ arguments, but sees 2. Make sure each operator/operand is a separate argument, and escape <>&|."
|
||||||
|
|
||||||
|
(first:rest) -> do
|
||||||
|
checkOp first
|
||||||
|
forM_ rest $ \t ->
|
||||||
|
-- We already find 95%+ of multiplication and regex earlier, so don't bother classifying this further.
|
||||||
|
when (isGlob t) $ warn (getId t) 2306 "Escape glob characters in arguments to expr to avoid pathname expansion."
|
||||||
|
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
-- These operators are hard to replicate in POSIX
|
-- These operators are hard to replicate in POSIX
|
||||||
exceptions = [ ":", "<", ">", "<=", ">=" ]
|
exceptions = [ ":", "<", ">", "<=", ">=",
|
||||||
|
-- We can offer better suggestions for these
|
||||||
|
"match", "length", "substr", "index"]
|
||||||
words = mapMaybe getLiteralString
|
words = mapMaybe getLiteralString
|
||||||
|
|
||||||
|
checkOp side =
|
||||||
|
case getLiteralString side of
|
||||||
|
Just "match" -> msg "'expr match' has unspecified results. Prefer 'expr str : regex'."
|
||||||
|
Just "length" -> msg "'expr length' has unspecified results. Prefer ${#var}."
|
||||||
|
Just "substr" -> msg "'expr substr' has unspecified results. Prefer 'cut' or ${var#???}."
|
||||||
|
Just "index" -> msg "'expr index' has unspecified results. Prefer x=${var%%[chars]*}; $((${#x}+1))."
|
||||||
|
_ -> return ()
|
||||||
|
where
|
||||||
|
msg = info (getId side) 2308
|
||||||
|
|
||||||
|
|
||||||
prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3"
|
prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3"
|
||||||
prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3"
|
prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3"
|
||||||
|
@ -283,15 +393,9 @@ checkGrepRe = CommandCheck (Basename "grep") check where
|
||||||
candidates =
|
candidates =
|
||||||
sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords
|
sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords
|
||||||
|
|
||||||
getSuspiciousRegexWildcard str =
|
getSuspiciousRegexWildcard str = case matchRegex suspicious str of
|
||||||
if not $ str `matches` contra
|
Just [[c]] | not (str `matches` contra) -> Just c
|
||||||
then do
|
_ -> fail "looks good"
|
||||||
match <- matchRegex suspicious str
|
|
||||||
str <- match !!! 0
|
|
||||||
str !!! 0
|
|
||||||
else
|
|
||||||
fail "looks good"
|
|
||||||
where
|
|
||||||
suspicious = mkRegex "([A-Za-z1-9])\\*"
|
suspicious = mkRegex "([A-Za-z1-9])\\*"
|
||||||
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
|
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
|
||||||
|
|
||||||
|
@ -378,9 +482,16 @@ prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\n
|
||||||
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
|
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
|
||||||
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
|
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
|
||||||
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
|
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
|
||||||
|
prop_checkUnusedEchoEscapes6 = verify checkUnusedEchoEscapes "echo '\\506'"
|
||||||
|
prop_checkUnusedEchoEscapes7 = verify checkUnusedEchoEscapes "echo '\\5a'"
|
||||||
|
prop_checkUnusedEchoEscapes8 = verifyNot checkUnusedEchoEscapes "echo '\\8a'"
|
||||||
|
prop_checkUnusedEchoEscapes9 = verifyNot checkUnusedEchoEscapes "echo '\\d5a'"
|
||||||
|
prop_checkUnusedEchoEscapes10 = verify checkUnusedEchoEscapes "echo '\\x4a'"
|
||||||
|
prop_checkUnusedEchoEscapes11 = verify checkUnusedEchoEscapes "echo '\\xat'"
|
||||||
|
prop_checkUnusedEchoEscapes12 = verifyNot checkUnusedEchoEscapes "echo '\\xth'"
|
||||||
checkUnusedEchoEscapes = CommandCheck (Basename "echo") f
|
checkUnusedEchoEscapes = CommandCheck (Basename "echo") f
|
||||||
where
|
where
|
||||||
hasEscapes = mkRegex "\\\\[rnt]"
|
hasEscapes = mkRegex "\\\\([rntabefv\\']|[0-7]{1,3}|x([0-9]|[A-F]|[a-f]){1,2})"
|
||||||
f cmd =
|
f cmd =
|
||||||
whenShell [Sh, Bash, Ksh] $
|
whenShell [Sh, Bash, Ksh] $
|
||||||
unless (cmd `hasFlag` "e") $
|
unless (cmd `hasFlag` "e") $
|
||||||
|
@ -462,8 +573,8 @@ checkMkdirDashPM = CommandCheck (Basename "mkdir") check
|
||||||
where
|
where
|
||||||
check t = sequence_ $ do
|
check t = sequence_ $ do
|
||||||
let flags = getAllFlags t
|
let flags = getAllFlags t
|
||||||
dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
|
dashP <- find (\(_,f) -> f == "p" || f == "parents") flags
|
||||||
dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
|
dashM <- find (\(_,f) -> f == "m" || f == "mode") flags
|
||||||
-- mkdir -pm 0700 dir is fine, so is ../dir, but dir/subdir is not.
|
-- mkdir -pm 0700 dir is fine, so is ../dir, but dir/subdir is not.
|
||||||
guard $ any couldHaveSubdirs (drop 1 $ arguments t)
|
guard $ any couldHaveSubdirs (drop 1 $ arguments t)
|
||||||
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
|
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
|
||||||
|
@ -483,7 +594,7 @@ prop_checkNonportableSignals7 = verifyNot checkNonportableSignals "trap 'stop' i
|
||||||
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
|
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
|
||||||
where
|
where
|
||||||
f args = case args of
|
f args = case args of
|
||||||
first:rest -> unless (isFlag first) $ mapM_ check rest
|
first:rest | not $ isFlag first -> mapM_ check rest
|
||||||
_ -> return ()
|
_ -> return ()
|
||||||
|
|
||||||
check param = sequence_ $ do
|
check param = sequence_ $ do
|
||||||
|
@ -520,9 +631,9 @@ checkInteractiveSu = CommandCheck (Basename "su") f
|
||||||
info (getId cmd) 2117
|
info (getId cmd) 2117
|
||||||
"To run commands as another user, use su -c or sudo."
|
"To run commands as another user, use su -c or sudo."
|
||||||
|
|
||||||
undirected (T_Pipeline _ _ l) = length l <= 1
|
undirected (T_Pipeline _ _ (_:_:_)) = False
|
||||||
-- This should really just be modifications to stdin, but meh
|
-- This should really just be modifications to stdin, but meh
|
||||||
undirected (T_Redirecting _ list _) = null list
|
undirected (T_Redirecting _ (_:_) _) = False
|
||||||
undirected _ = True
|
undirected _ = True
|
||||||
|
|
||||||
|
|
||||||
|
@ -539,9 +650,8 @@ checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
|
||||||
([], hostport:r@(_:_)) -> checkArg $ last r
|
([], hostport:r@(_:_)) -> checkArg $ last r
|
||||||
_ -> return ()
|
_ -> return ()
|
||||||
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
|
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
|
||||||
case filter (not . isConstant) parts of
|
forM_ (find (not . isConstant) parts) $
|
||||||
[] -> return ()
|
\x -> info (getId x) 2029
|
||||||
(x:_) -> info (getId x) 2029
|
|
||||||
"Note that, unescaped, this expands on the client side."
|
"Note that, unescaped, this expands on the client side."
|
||||||
checkArg _ = return ()
|
checkArg _ = return ()
|
||||||
|
|
||||||
|
@ -567,6 +677,8 @@ prop_checkPrintfVar18= verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
|
||||||
prop_checkPrintfVar19 = verifyNot checkPrintfVar "printf '%(%s)T'"
|
prop_checkPrintfVar19 = verifyNot checkPrintfVar "printf '%(%s)T'"
|
||||||
prop_checkPrintfVar20 = verifyNot checkPrintfVar "printf '%d %(%s)T' 42"
|
prop_checkPrintfVar20 = verifyNot checkPrintfVar "printf '%d %(%s)T' 42"
|
||||||
prop_checkPrintfVar21 = verify checkPrintfVar "printf '%d %(%s)T'"
|
prop_checkPrintfVar21 = verify checkPrintfVar "printf '%d %(%s)T'"
|
||||||
|
prop_checkPrintfVar22 = verify checkPrintfVar "printf '%s\n%s' foo"
|
||||||
|
|
||||||
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||||
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
|
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
|
||||||
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
|
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
|
||||||
|
@ -579,23 +691,24 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||||
let formats = getPrintfFormats string
|
let formats = getPrintfFormats string
|
||||||
let formatCount = length formats
|
let formatCount = length formats
|
||||||
let argCount = length more
|
let argCount = length more
|
||||||
|
let pluraliseIfMany word n = if n > 1 then word ++ "s" else word
|
||||||
|
|
||||||
return $
|
return $ if
|
||||||
case () of
|
| argCount == 0 && formatCount == 0 ->
|
||||||
() | argCount == 0 && formatCount == 0 ->
|
|
||||||
return () -- This is fine
|
return () -- This is fine
|
||||||
() | formatCount == 0 && argCount > 0 ->
|
| formatCount == 0 && argCount > 0 ->
|
||||||
err (getId format) 2182
|
err (getId format) 2182
|
||||||
"This printf format string has no variables. Other arguments are ignored."
|
"This printf format string has no variables. Other arguments are ignored."
|
||||||
() | any mayBecomeMultipleArgs more ->
|
| any mayBecomeMultipleArgs more ->
|
||||||
return () -- We don't know so trust the user
|
return () -- We don't know so trust the user
|
||||||
() | argCount < formatCount && onlyTrailingTs formats argCount ->
|
| argCount < formatCount && onlyTrailingTs formats argCount ->
|
||||||
return () -- Allow trailing %()Ts since they use the current time
|
return () -- Allow trailing %()Ts since they use the current time
|
||||||
() | argCount > 0 && argCount `mod` formatCount == 0 ->
|
| argCount > 0 && argCount `mod` formatCount == 0 ->
|
||||||
return () -- Great: a suitable number of arguments
|
return () -- Great: a suitable number of arguments
|
||||||
() ->
|
| otherwise ->
|
||||||
warn (getId format) 2183 $
|
warn (getId format) 2183 $
|
||||||
"This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments."
|
"This format string has " ++ show formatCount ++ " " ++ pluraliseIfMany "variable" formatCount ++
|
||||||
|
", but is passed " ++ show argCount ++ pluraliseIfMany " argument" argCount ++ "."
|
||||||
|
|
||||||
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
|
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
|
||||||
info (getId format) 2059
|
info (getId format) 2059
|
||||||
|
@ -610,6 +723,8 @@ prop_checkGetPrintfFormats2 = getPrintfFormats "%0*s" == "*s"
|
||||||
prop_checkGetPrintfFormats3 = getPrintfFormats "%(%s)T" == "T"
|
prop_checkGetPrintfFormats3 = getPrintfFormats "%(%s)T" == "T"
|
||||||
prop_checkGetPrintfFormats4 = getPrintfFormats "%d%%%(%s)T" == "dT"
|
prop_checkGetPrintfFormats4 = getPrintfFormats "%d%%%(%s)T" == "dT"
|
||||||
prop_checkGetPrintfFormats5 = getPrintfFormats "%bPassed: %d, %bFailed: %d%b, Skipped: %d, %bErrored: %d%b\\n" == "bdbdbdbdb"
|
prop_checkGetPrintfFormats5 = getPrintfFormats "%bPassed: %d, %bFailed: %d%b, Skipped: %d, %bErrored: %d%b\\n" == "bdbdbdbdb"
|
||||||
|
prop_checkGetPrintfFormats6 = getPrintfFormats "%s%s" == "ss"
|
||||||
|
prop_checkGetPrintfFormats7 = getPrintfFormats "%s\n%s" == "ss"
|
||||||
getPrintfFormats = getFormats
|
getPrintfFormats = getFormats
|
||||||
where
|
where
|
||||||
-- Get the arguments in the string as a string of type characters,
|
-- Get the arguments in the string as a string of type characters,
|
||||||
|
@ -628,18 +743,18 @@ getPrintfFormats = getFormats
|
||||||
|
|
||||||
regexBasedGetFormats rest =
|
regexBasedGetFormats rest =
|
||||||
case matchRegex re rest of
|
case matchRegex re rest of
|
||||||
Just [width, precision, typ, rest] ->
|
Just [width, precision, typ, rest, _] ->
|
||||||
(if width == "*" then "*" else "") ++
|
(if width == "*" then "*" else "") ++
|
||||||
(if precision == "*" then "*" else "") ++
|
(if precision == "*" then "*" else "") ++
|
||||||
typ ++ getFormats rest
|
typ ++ getFormats rest
|
||||||
Nothing -> take 1 rest ++ getFormats rest
|
Nothing -> take 1 rest ++ getFormats rest
|
||||||
where
|
where
|
||||||
-- constructed based on specifications in "man printf"
|
-- constructed based on specifications in "man printf"
|
||||||
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)([diouxXfFeEgGaAcsbq])(.*)"
|
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)([diouxXfFeEgGaAcsbq])((\n|.)*)"
|
||||||
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \ /
|
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \______ /
|
||||||
-- V V V V V
|
-- V V V V V
|
||||||
-- flags field width precision format character rest
|
-- flags field width precision format character rest
|
||||||
-- field width and precision can be specified with a '*' instead of a digit,
|
-- field width and precision can be specified with an '*' instead of a digit,
|
||||||
-- in which case printf will accept one more argument for each '*' used
|
-- in which case printf will accept one more argument for each '*' used
|
||||||
|
|
||||||
|
|
||||||
|
@ -663,17 +778,12 @@ prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'"
|
||||||
prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"
|
prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"
|
||||||
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
|
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
|
||||||
where
|
where
|
||||||
f (var:value:rest) =
|
f (var:rest)
|
||||||
let str = literal var in
|
| (not (null rest) && isVariableName str) || isAssignment str =
|
||||||
when (isVariableName str || isAssignment str) $
|
warn (getId var) 2121 "To assign a variable, use just 'var=value', no 'set ..'."
|
||||||
msg (getId var)
|
where str = literal var
|
||||||
f (var:_) =
|
|
||||||
when (isAssignment $ literal var) $
|
|
||||||
msg (getId var)
|
|
||||||
f _ = return ()
|
f _ = return ()
|
||||||
|
|
||||||
msg id = warn id 2121 "To assign a variable, use just 'var=value', no 'set ..'."
|
|
||||||
|
|
||||||
isAssignment str = '=' `elem` str
|
isAssignment str = '=' `elem` str
|
||||||
literal (T_NormalWord _ l) = concatMap literal l
|
literal (T_NormalWord _ l) = concatMap literal l
|
||||||
literal (T_Literal _ str) = str
|
literal (T_Literal _ str) = str
|
||||||
|
@ -687,8 +797,7 @@ prop_checkExportedExpansions4 = verifyNot checkExportedExpansions "export ${foo?
|
||||||
checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
|
checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
|
||||||
where
|
where
|
||||||
check t = sequence_ $ do
|
check t = sequence_ $ do
|
||||||
var <- getSingleUnmodifiedVariable t
|
name <- getSingleUnmodifiedBracedString t
|
||||||
let name = bracedString var
|
|
||||||
return . warn (getId t) 2163 $
|
return . warn (getId t) 2163 $
|
||||||
"This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
"This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
||||||
|
|
||||||
|
@ -700,30 +809,43 @@ prop_checkReadExpansions5 = verify checkReadExpansions "read \"$var\""
|
||||||
prop_checkReadExpansions6 = verify checkReadExpansions "read -a $var"
|
prop_checkReadExpansions6 = verify checkReadExpansions "read -a $var"
|
||||||
prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1"
|
prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1"
|
||||||
prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}"
|
prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}"
|
||||||
|
prop_checkReadExpansions9 = verify checkReadExpansions "read arr[val]"
|
||||||
checkReadExpansions = CommandCheck (Exactly "read") check
|
checkReadExpansions = CommandCheck (Exactly "read") check
|
||||||
where
|
where
|
||||||
options = getGnuOpts flagsForRead
|
options = getGnuOpts flagsForRead
|
||||||
getVars cmd = fromMaybe [] $ do
|
getVars cmd = fromMaybe [] $ do
|
||||||
opts <- options cmd
|
opts <- options $ arguments cmd
|
||||||
return [y | (x,y) <- opts, null x || x == "a"]
|
return [y | (x,(_, y)) <- opts, null x || x == "a"]
|
||||||
|
|
||||||
check cmd = mapM_ warning $ getVars cmd
|
check cmd = do
|
||||||
warning t = sequence_ $ do
|
mapM_ dollarWarning $ getVars cmd
|
||||||
var <- getSingleUnmodifiedVariable t
|
mapM_ arrayWarning $ arguments cmd
|
||||||
let name = bracedString var
|
|
||||||
|
dollarWarning t = sequence_ $ do
|
||||||
|
name <- getSingleUnmodifiedBracedString t
|
||||||
guard $ isVariableName name -- e.g. not $1
|
guard $ isVariableName name -- e.g. not $1
|
||||||
return . warn (getId t) 2229 $
|
return . warn (getId t) 2229 $
|
||||||
"This does not read '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
"This does not read '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
||||||
|
|
||||||
|
arrayWarning word =
|
||||||
|
when (any isUnquotedBracket $ getWordParts word) $
|
||||||
|
warn (getId word) 2313 $
|
||||||
|
"Quote array indices to avoid them expanding as globs."
|
||||||
|
|
||||||
|
isUnquotedBracket t =
|
||||||
|
case t of
|
||||||
|
T_Glob _ ('[':_) -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
-- Return the single variable expansion that makes up this word, if any.
|
-- Return the single variable expansion that makes up this word, if any.
|
||||||
-- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing
|
-- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing
|
||||||
getSingleUnmodifiedVariable :: Token -> Maybe Token
|
getSingleUnmodifiedBracedString :: Token -> Maybe String
|
||||||
getSingleUnmodifiedVariable word =
|
getSingleUnmodifiedBracedString word =
|
||||||
case getWordParts word of
|
case getWordParts word of
|
||||||
[t@(T_DollarBraced {})] ->
|
[T_DollarBraced _ _ l] ->
|
||||||
let contents = bracedString t
|
let contents = concat $ oversimplify l
|
||||||
name = getBracedReference contents
|
name = getBracedReference contents
|
||||||
in guard (contents == name) >> return t
|
in guard (contents == name) >> return contents
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
|
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
|
||||||
|
@ -754,6 +876,9 @@ checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
|
||||||
|
|
||||||
prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]"
|
prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]"
|
||||||
prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo"
|
prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo"
|
||||||
|
prop_checkUnsetGlobs3 = verify checkUnsetGlobs "unset foo[$i]"
|
||||||
|
prop_checkUnsetGlobs4 = verify checkUnsetGlobs "unset foo[x${i}y]"
|
||||||
|
prop_checkUnsetGlobs5 = verifyNot checkUnsetGlobs "unset foo]["
|
||||||
checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)
|
checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)
|
||||||
where
|
where
|
||||||
check arg =
|
check arg =
|
||||||
|
@ -806,7 +931,7 @@ prop_checkTimedCommand2 = verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar
|
||||||
prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1"
|
prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1"
|
||||||
checkTimedCommand = CommandCheck (Exactly "time") f where
|
checkTimedCommand = CommandCheck (Exactly "time") f where
|
||||||
f (T_SimpleCommand _ _ (c:args@(_:_))) =
|
f (T_SimpleCommand _ _ (c:args@(_:_))) =
|
||||||
whenShell [Sh, Dash] $ do
|
whenShell [Sh, Dash, BusyboxSh] $ do
|
||||||
let cmd = last args -- "time" is parsed with a command as argument
|
let cmd = last args -- "time" is parsed with a command as argument
|
||||||
when (isPiped cmd) $
|
when (isPiped cmd) $
|
||||||
warn (getId c) 2176 "'time' is undefined for pipelines. time single stage or bash -c instead."
|
warn (getId c) 2176 "'time' is undefined for pipelines. time single stage or bash -c instead."
|
||||||
|
@ -830,11 +955,27 @@ checkTimedCommand = CommandCheck (Exactly "time") f where
|
||||||
prop_checkLocalScope1 = verify checkLocalScope "local foo=3"
|
prop_checkLocalScope1 = verify checkLocalScope "local foo=3"
|
||||||
prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }"
|
prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }"
|
||||||
checkLocalScope = CommandCheck (Exactly "local") $ \t ->
|
checkLocalScope = CommandCheck (Exactly "local") $ \t ->
|
||||||
whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local
|
whenShell [Bash, Dash, BusyboxSh] $ do -- Ksh allows it, Sh doesn't support local
|
||||||
path <- getPathM t
|
path <- getPathM t
|
||||||
unless (any isFunctionLike path) $
|
unless (any isFunctionLike path) $
|
||||||
err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
|
err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
|
||||||
|
|
||||||
|
prop_checkMultipleDeclaring1 = verify (checkMultipleDeclaring "local") "q() { local readonly var=1; }"
|
||||||
|
prop_checkMultipleDeclaring2 = verifyNot (checkMultipleDeclaring "local") "q() { local var=1; }"
|
||||||
|
prop_checkMultipleDeclaring3 = verify (checkMultipleDeclaring "readonly") "readonly local foo=5"
|
||||||
|
prop_checkMultipleDeclaring4 = verify (checkMultipleDeclaring "export") "export readonly foo=5"
|
||||||
|
prop_checkMultipleDeclaring5 = verifyNot (checkMultipleDeclaring "local") "f() { local -r foo=5; }"
|
||||||
|
prop_checkMultipleDeclaring6 = verifyNot (checkMultipleDeclaring "declare") "declare -rx foo=5"
|
||||||
|
prop_checkMultipleDeclaring7 = verifyNot (checkMultipleDeclaring "readonly") "readonly 'local' foo=5"
|
||||||
|
checkMultipleDeclaring cmd = CommandCheck (Exactly cmd) (mapM_ check . arguments)
|
||||||
|
where
|
||||||
|
check t = sequence_ $ do
|
||||||
|
lit <- getUnquotedLiteral t
|
||||||
|
guard $ lit `elem` declaringCommands
|
||||||
|
return $ err (getId $ getCommandTokenOrThis t) 2316 $
|
||||||
|
"This applies " ++ cmd ++ " to the variable named " ++ lit ++
|
||||||
|
", which is probably not what you want. Use a separate command or the appropriate `declare` options instead."
|
||||||
|
|
||||||
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
|
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
|
||||||
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
|
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
|
||||||
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
|
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
|
||||||
|
@ -853,42 +994,56 @@ prop_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x
|
||||||
prop_checkWhileGetoptsCase3 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; b) bar;; *) :;esac; done"
|
prop_checkWhileGetoptsCase3 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; b) bar;; *) :;esac; done"
|
||||||
prop_checkWhileGetoptsCase4 = verifyNot checkWhileGetoptsCase "while getopts 'a:123' x; do case $x in a) foo;; [0-9]) bar;; esac; done"
|
prop_checkWhileGetoptsCase4 = verifyNot checkWhileGetoptsCase "while getopts 'a:123' x; do case $x in a) foo;; [0-9]) bar;; esac; done"
|
||||||
prop_checkWhileGetoptsCase5 = verifyNot checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; \\?) bar;; *) baz;; esac; done"
|
prop_checkWhileGetoptsCase5 = verifyNot checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; \\?) bar;; *) baz;; esac; done"
|
||||||
|
prop_checkWhileGetoptsCase6 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $y in a) foo;; esac; done"
|
||||||
|
prop_checkWhileGetoptsCase7 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case x$x in xa) foo;; xb) foo;; esac; done"
|
||||||
|
prop_checkWhileGetoptsCase8 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do x=a; case $x in a) foo;; esac; done"
|
||||||
checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
|
checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
|
||||||
where
|
where
|
||||||
f :: Token -> Analysis
|
f :: Token -> Analysis
|
||||||
f t@(T_SimpleCommand _ _ (cmd:arg1:_)) = do
|
f t@(T_SimpleCommand _ _ (cmd:arg1:name:_)) = do
|
||||||
path <- getPathM t
|
path <- getPathM t
|
||||||
|
params <- ask
|
||||||
sequence_ $ do
|
sequence_ $ do
|
||||||
options <- getLiteralString arg1
|
options <- getLiteralString arg1
|
||||||
(T_WhileExpression _ _ body) <- findFirst whileLoop path
|
getoptsVar <- getLiteralString name
|
||||||
caseCmd <- mapMaybe findCase body !!! 0
|
(T_WhileExpression _ _ body) <- findFirst whileLoop (NE.toList path)
|
||||||
return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) caseCmd
|
T_CaseExpression id var list <- mapMaybe findCase body !!! 0
|
||||||
|
|
||||||
|
-- Make sure getopts name and case variable matches
|
||||||
|
[T_DollarBraced _ _ bracedWord] <- return $ getWordParts var
|
||||||
|
[T_Literal _ caseVar] <- return $ getWordParts bracedWord
|
||||||
|
guard $ caseVar == getoptsVar
|
||||||
|
|
||||||
|
-- Make sure the variable isn't modified
|
||||||
|
guard . not $ modifiesVariable params (T_BraceGroup (Id 0) body) getoptsVar
|
||||||
|
|
||||||
|
return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) id list
|
||||||
f _ = return ()
|
f _ = return ()
|
||||||
|
|
||||||
check :: Id -> [String] -> Token -> Analysis
|
check :: Id -> [String] -> Id -> [(CaseType, [Token], [Token])] -> Analysis
|
||||||
check optId opts (T_CaseExpression id _ list) = do
|
check optId opts id list = do
|
||||||
unless (Nothing `Map.member` handledMap) $ do
|
unless (Nothing `M.member` handledMap) $ do
|
||||||
mapM_ (warnUnhandled optId id) $ catMaybes $ Map.keys notHandled
|
mapM_ (warnUnhandled optId id) $ catMaybes $ M.keys notHandled
|
||||||
|
|
||||||
unless (any (`Map.member` handledMap) [Just "*",Just "?"]) $
|
unless (any (`M.member` handledMap) [Just "*",Just "?"]) $
|
||||||
warn id 2220 "Invalid flags are not handled. Add a *) case."
|
warn id 2220 "Invalid flags are not handled. Add a *) case."
|
||||||
|
|
||||||
mapM_ warnRedundant $ Map.toList notRequested
|
mapM_ warnRedundant $ M.toList notRequested
|
||||||
|
|
||||||
where
|
where
|
||||||
handledMap = Map.fromList (concatMap getHandledStrings list)
|
handledMap = M.fromList (concatMap getHandledStrings list)
|
||||||
requestedMap = Map.fromList $ map (\x -> (Just x, ())) opts
|
requestedMap = M.fromList $ map (\x -> (Just x, ())) opts
|
||||||
|
|
||||||
notHandled = Map.difference requestedMap handledMap
|
notHandled = M.difference requestedMap handledMap
|
||||||
notRequested = Map.difference handledMap requestedMap
|
notRequested = M.difference handledMap requestedMap
|
||||||
|
|
||||||
warnUnhandled optId caseId str =
|
warnUnhandled optId caseId str =
|
||||||
warn caseId 2213 $ "getopts specified -" ++ str ++ ", but it's not handled by this 'case'."
|
warn caseId 2213 $ "getopts specified -" ++ (e4m str) ++ ", but it's not handled by this 'case'."
|
||||||
|
|
||||||
warnRedundant (key, expr) = sequence_ $ do
|
warnRedundant (Just str, expr)
|
||||||
str <- key
|
| str `notElem` ["*", ":", "?"] =
|
||||||
guard $ str `notElem` ["*", ":", "?"]
|
warn (getId expr) 2214 "This case is not specified by getopts."
|
||||||
return $ warn (getId expr) 2214 "This case is not specified by getopts."
|
warnRedundant _ = return ()
|
||||||
|
|
||||||
getHandledStrings (_, globs, _) =
|
getHandledStrings (_, globs, _) =
|
||||||
map (\x -> (literal x, x)) globs
|
map (\x -> (literal x, x)) globs
|
||||||
|
@ -899,7 +1054,7 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
|
||||||
|
|
||||||
fromGlob t =
|
fromGlob t =
|
||||||
case t of
|
case t of
|
||||||
T_Glob _ ('[':c:']':[]) -> return [c]
|
T_Glob _ ['[', c, ']'] -> return [c]
|
||||||
T_Glob _ "*" -> return "*"
|
T_Glob _ "*" -> return "*"
|
||||||
T_Glob _ "?" -> return "?"
|
T_Glob _ "?" -> return "?"
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
@ -934,7 +1089,7 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
|
||||||
when (isRecursive t) $
|
when (isRecursive t) $
|
||||||
mapM_ (mapM_ checkWord . braceExpand) $ arguments t
|
mapM_ (mapM_ checkWord . braceExpand) $ arguments t
|
||||||
where
|
where
|
||||||
isRecursive = any (`elem` ["r", "R", "recursive"]) . map snd . getAllFlags
|
isRecursive = any ((`elem` ["r", "R", "recursive"]) . snd) . getAllFlags
|
||||||
|
|
||||||
checkWord token =
|
checkWord token =
|
||||||
case getLiteralString token of
|
case getLiteralString token of
|
||||||
|
@ -966,9 +1121,9 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
|
||||||
f _ = return ""
|
f _ = return ""
|
||||||
|
|
||||||
stripTrailing c = reverse . dropWhile (== c) . reverse
|
stripTrailing c = reverse . dropWhile (== c) . reverse
|
||||||
skipRepeating c (a:b:rest) | a == b && b == c = skipRepeating c (b:rest)
|
skipRepeating c = foldr go []
|
||||||
skipRepeating c (a:r) = a:skipRepeating c r
|
where
|
||||||
skipRepeating _ [] = []
|
go a r = a : case r of b:rest | b == c && a == b -> rest; _ -> r
|
||||||
|
|
||||||
paths = [
|
paths = [
|
||||||
"", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",
|
"", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",
|
||||||
|
@ -1039,7 +1194,7 @@ checkFindRedirections = CommandCheck (Basename "find") f
|
||||||
|
|
||||||
prop_checkWhich = verify checkWhich "which '.+'"
|
prop_checkWhich = verify checkWhich "which '.+'"
|
||||||
checkWhich = CommandCheck (Basename "which") $
|
checkWhich = CommandCheck (Basename "which") $
|
||||||
\t -> info (getId $ getCommandTokenOrThis t) 2230 "which is non-standard. Use builtin 'command -v' instead."
|
\t -> info (getId $ getCommandTokenOrThis t) 2230 "'which' is non-standard. Use builtin 'command -v' instead."
|
||||||
|
|
||||||
prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
|
prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
|
||||||
prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
|
prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
|
||||||
|
@ -1081,9 +1236,8 @@ prop_checkSudoArgs7 = verifyNot checkSudoArgs "sudo docker export foo"
|
||||||
checkSudoArgs = CommandCheck (Basename "sudo") f
|
checkSudoArgs = CommandCheck (Basename "sudo") f
|
||||||
where
|
where
|
||||||
f t = sequence_ $ do
|
f t = sequence_ $ do
|
||||||
opts <- parseOpts t
|
opts <- parseOpts $ arguments t
|
||||||
let nonFlags = [x | ("",x) <- opts]
|
(_,(commandArg, _)) <- find (null . fst) opts
|
||||||
commandArg <- nonFlags !!! 0
|
|
||||||
command <- getLiteralString commandArg
|
command <- getLiteralString commandArg
|
||||||
guard $ command `elem` builtins
|
guard $ command `elem` builtins
|
||||||
return $ warn (getId t) 2232 $ "Can't use sudo with builtins like " ++ command ++ ". Did you want sudo sh -c .. instead?"
|
return $ warn (getId t) 2232 $ "Can't use sudo with builtins like " ++ command ++ ". Did you want sudo sh -c .. instead?"
|
||||||
|
@ -1113,5 +1267,206 @@ checkChmodDashr = CommandCheck (Basename "chmod") f
|
||||||
guard $ flag == "-r"
|
guard $ flag == "-r"
|
||||||
return $ warn (getId t) 2253 "Use -R to recurse, or explicitly a-r to remove read permissions."
|
return $ warn (getId t) 2253 "Use -R to recurse, or explicitly a-r to remove read permissions."
|
||||||
|
|
||||||
|
prop_checkXargsDashi1 = verify checkXargsDashi "xargs -i{} echo {}"
|
||||||
|
prop_checkXargsDashi2 = verifyNot checkXargsDashi "xargs -I{} echo {}"
|
||||||
|
prop_checkXargsDashi3 = verifyNot checkXargsDashi "xargs sed -i -e foo"
|
||||||
|
prop_checkXargsDashi4 = verify checkXargsDashi "xargs -e sed -i foo"
|
||||||
|
prop_checkXargsDashi5 = verifyNot checkXargsDashi "xargs -x sed -i foo"
|
||||||
|
checkXargsDashi = CommandCheck (Basename "xargs") f
|
||||||
|
where
|
||||||
|
f t = sequence_ $ do
|
||||||
|
opts <- parseOpts $ arguments t
|
||||||
|
(option, value) <- lookup "i" opts
|
||||||
|
return $ info (getId option) 2267 "GNU xargs -i is deprecated in favor of -I{}"
|
||||||
|
parseOpts = getBsdOpts "0oprtxadR:S:J:L:l:n:P:s:e:E:i:I:"
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkArgComparison1 = verify (checkArgComparison "declare") "declare a = b"
|
||||||
|
prop_checkArgComparison2 = verify (checkArgComparison "declare") "declare a =b"
|
||||||
|
prop_checkArgComparison3 = verifyNot (checkArgComparison "declare") "declare a=b"
|
||||||
|
prop_checkArgComparison4 = verify (checkArgComparison "export") "export a +=b"
|
||||||
|
prop_checkArgComparison7 = verifyNot (checkArgComparison "declare") "declare -a +i foo"
|
||||||
|
prop_checkArgComparison8 = verify (checkArgComparison "let") "let x = 0"
|
||||||
|
prop_checkArgComparison9 = verify (checkArgComparison "alias") "alias x =0"
|
||||||
|
-- This mirrors checkSecondArgIsComparison but for arguments to local/readonly/declare/export
|
||||||
|
checkArgComparison cmd = CommandCheck (Exactly cmd) wordsWithEqual
|
||||||
|
where
|
||||||
|
wordsWithEqual t = mapM_ check $ arguments t
|
||||||
|
check arg = do
|
||||||
|
sequence_ $ do
|
||||||
|
str <- getLeadingUnquotedString arg
|
||||||
|
case str of
|
||||||
|
'=':_ ->
|
||||||
|
return $ err (headId arg) 2290 $
|
||||||
|
"Remove spaces around = to assign."
|
||||||
|
'+':'=':_ ->
|
||||||
|
return $ err (headId arg) 2290 $
|
||||||
|
"Remove spaces around += to append."
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
-- 'let' is parsed as a sequence of arithmetic expansions,
|
||||||
|
-- so we want the additional warning for "x="
|
||||||
|
when (cmd == "let") $ sequence_ $ do
|
||||||
|
token <- getTrailingUnquotedLiteral arg
|
||||||
|
str <- getLiteralString token
|
||||||
|
guard $ "=" `isSuffixOf` str
|
||||||
|
return $ err (getId token) 2290 $
|
||||||
|
"Remove spaces around = to assign."
|
||||||
|
|
||||||
|
headId t =
|
||||||
|
case t of
|
||||||
|
T_NormalWord _ (x:_) -> getId x
|
||||||
|
_ -> getId t
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkMaskedReturns1 = verify (checkMaskedReturns "local") "f() { local a=$(false); }"
|
||||||
|
prop_checkMaskedReturns2 = verify (checkMaskedReturns "declare") "declare a=$(false)"
|
||||||
|
prop_checkMaskedReturns3 = verify (checkMaskedReturns "declare") "declare a=\"`false`\""
|
||||||
|
prop_checkMaskedReturns4 = verify (checkMaskedReturns "readonly") "readonly a=$(false)"
|
||||||
|
prop_checkMaskedReturns5 = verify (checkMaskedReturns "readonly") "readonly a=\"`false`\""
|
||||||
|
prop_checkMaskedReturns6 = verifyNot (checkMaskedReturns "declare") "declare a; a=$(false)"
|
||||||
|
prop_checkMaskedReturns7 = verifyNot (checkMaskedReturns "local") "f() { local -r a=$(false); }"
|
||||||
|
prop_checkMaskedReturns8 = verifyNot (checkMaskedReturns "readonly") "a=$(false); readonly a"
|
||||||
|
prop_checkMaskedReturns9 = verify (checkMaskedReturns "typeset") "#!/bin/ksh\n f() { typeset -r x=$(false); }"
|
||||||
|
prop_checkMaskedReturns10 = verifyNot (checkMaskedReturns "typeset") "#!/bin/ksh\n function f { typeset -r x=$(false); }"
|
||||||
|
prop_checkMaskedReturns11 = verifyNot (checkMaskedReturns "typeset") "#!/bin/bash\n f() { typeset -r x=$(false); }"
|
||||||
|
prop_checkMaskedReturns12 = verify (checkMaskedReturns "typeset") "typeset -r x=$(false);"
|
||||||
|
prop_checkMaskedReturns13 = verify (checkMaskedReturns "typeset") "f() { typeset -g x=$(false); }"
|
||||||
|
prop_checkMaskedReturns14 = verify (checkMaskedReturns "declare") "declare x=${ false; }"
|
||||||
|
prop_checkMaskedReturns15 = verify (checkMaskedReturns "declare") "f() { declare x=$(false); }"
|
||||||
|
checkMaskedReturns str = CommandCheck (Exactly str) checkCmd
|
||||||
|
where
|
||||||
|
checkCmd t = do
|
||||||
|
path <- getPathM t
|
||||||
|
shell <- asks shellType
|
||||||
|
sequence_ $ do
|
||||||
|
name <- getCommandName t
|
||||||
|
|
||||||
|
let flags = map snd (getAllFlags t)
|
||||||
|
let hasDashR = "r" `elem` flags
|
||||||
|
let hasDashG = "g" `elem` flags
|
||||||
|
let isInScopedFunction = any (isScopedFunction shell) path
|
||||||
|
|
||||||
|
let isLocal = not hasDashG && isLocalInFunction name && isInScopedFunction
|
||||||
|
let isReadOnly = name == "readonly" || hasDashR
|
||||||
|
|
||||||
|
-- Don't warn about local variables that are declared readonly,
|
||||||
|
-- because the workaround `local x; x=$(false); local -r x;` is annoying
|
||||||
|
guard . not $ isLocal && isReadOnly
|
||||||
|
|
||||||
|
return $ mapM_ checkArgs $ arguments t
|
||||||
|
|
||||||
|
checkArgs (T_Assignment id _ _ _ word) | any hasReturn $ getWordParts word =
|
||||||
|
warn id 2155 "Declare and assign separately to avoid masking return values."
|
||||||
|
checkArgs _ = return ()
|
||||||
|
|
||||||
|
isLocalInFunction = (`elem` ["local", "declare", "typeset"])
|
||||||
|
isScopedFunction shell t =
|
||||||
|
case t of
|
||||||
|
T_BatsTest {} -> True
|
||||||
|
-- In ksh, only functions declared with 'function' have their own scope
|
||||||
|
T_Function _ (FunctionKeyword hasFunction) _ _ _ -> shell /= Ksh || hasFunction
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
hasReturn t = case t of
|
||||||
|
T_Backticked {} -> True
|
||||||
|
T_DollarExpansion {} -> True
|
||||||
|
T_DollarBraceCommandExpansion {} -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkUnquotedEchoSpaces1 = verify checkUnquotedEchoSpaces "echo foo bar"
|
||||||
|
prop_checkUnquotedEchoSpaces2 = verifyNot checkUnquotedEchoSpaces "echo foo"
|
||||||
|
prop_checkUnquotedEchoSpaces3 = verifyNot checkUnquotedEchoSpaces "echo foo bar"
|
||||||
|
prop_checkUnquotedEchoSpaces4 = verifyNot checkUnquotedEchoSpaces "echo 'foo bar'"
|
||||||
|
prop_checkUnquotedEchoSpaces5 = verifyNot checkUnquotedEchoSpaces "echo a > myfile.txt b"
|
||||||
|
prop_checkUnquotedEchoSpaces6 = verifyNot checkUnquotedEchoSpaces " echo foo\\\n bar"
|
||||||
|
checkUnquotedEchoSpaces = CommandCheck (Basename "echo") check
|
||||||
|
where
|
||||||
|
check t = do
|
||||||
|
let args = arguments t
|
||||||
|
m <- asks tokenPositions
|
||||||
|
redir <- getClosestCommandM t
|
||||||
|
sequence_ $ do
|
||||||
|
let positions = mapMaybe (\c -> M.lookup (getId c) m) args
|
||||||
|
let pairs = zip positions (drop 1 positions)
|
||||||
|
(T_Redirecting _ redirTokens _) <- redir
|
||||||
|
let redirPositions = mapMaybe (\c -> fst <$> M.lookup (getId c) m) redirTokens
|
||||||
|
guard $ any (hasSpacesBetween redirPositions) pairs
|
||||||
|
return $ info (getId t) 2291 "Quote repeated spaces to avoid them collapsing into one."
|
||||||
|
|
||||||
|
hasSpacesBetween redirs ((a,b), (c,d)) =
|
||||||
|
posLine a == posLine d
|
||||||
|
&& ((posColumn c) - (posColumn b)) >= 4
|
||||||
|
&& not (any (\x -> b < x && x < c) redirs)
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkEvalArray1 = verify checkEvalArray "eval $@"
|
||||||
|
prop_checkEvalArray2 = verify checkEvalArray "eval \"${args[@]}\""
|
||||||
|
prop_checkEvalArray3 = verify checkEvalArray "eval \"${args[@]@Q}\""
|
||||||
|
prop_checkEvalArray4 = verifyNot checkEvalArray "eval \"${args[*]@Q}\""
|
||||||
|
prop_checkEvalArray5 = verifyNot checkEvalArray "eval \"$*\""
|
||||||
|
checkEvalArray = CommandCheck (Exactly "eval") (mapM_ check . concatMap getWordParts . arguments)
|
||||||
|
where
|
||||||
|
check t =
|
||||||
|
when (isArrayExpansion t) $
|
||||||
|
if isEscaped t
|
||||||
|
then style (getId t) 2293 "When eval'ing @Q-quoted words, use * rather than @ as the index."
|
||||||
|
else warn (getId t) 2294 "eval negates the benefit of arrays. Drop eval to preserve whitespace/symbols (or eval as string)."
|
||||||
|
|
||||||
|
isEscaped q =
|
||||||
|
case q of
|
||||||
|
-- Match ${arr[@]@Q} and ${@@Q} and such
|
||||||
|
T_DollarBraced _ _ l -> 'Q' `elem` getBracedModifier (concat $ oversimplify l)
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkBackreferencingDeclaration1 = verify (checkBackreferencingDeclaration "declare") "declare x=1 y=foo$x"
|
||||||
|
prop_checkBackreferencingDeclaration2 = verify (checkBackreferencingDeclaration "readonly") "readonly x=1 y=$((1+x))"
|
||||||
|
prop_checkBackreferencingDeclaration3 = verify (checkBackreferencingDeclaration "local") "local x=1 y=$(echo $x)"
|
||||||
|
prop_checkBackreferencingDeclaration4 = verify (checkBackreferencingDeclaration "local") "local x=1 y[$x]=z"
|
||||||
|
prop_checkBackreferencingDeclaration5 = verify (checkBackreferencingDeclaration "declare") "declare x=var $x=1"
|
||||||
|
prop_checkBackreferencingDeclaration6 = verify (checkBackreferencingDeclaration "declare") "declare x=var $x=1"
|
||||||
|
prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration "declare") "declare x=var $k=$x"
|
||||||
|
checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check
|
||||||
|
where
|
||||||
|
check t = do
|
||||||
|
maybeCfga <- asks cfgAnalysis
|
||||||
|
mapM_ (\cfga -> foldM_ (perArg cfga) M.empty $ arguments t) maybeCfga
|
||||||
|
|
||||||
|
perArg cfga leftArgs t =
|
||||||
|
case t of
|
||||||
|
T_Assignment id _ name idx t -> do
|
||||||
|
warnIfBackreferencing cfga leftArgs $ t:idx
|
||||||
|
return $ M.insert name id leftArgs
|
||||||
|
t -> do
|
||||||
|
warnIfBackreferencing cfga leftArgs [t]
|
||||||
|
return leftArgs
|
||||||
|
|
||||||
|
warnIfBackreferencing cfga backrefs l = do
|
||||||
|
references <- findReferences cfga l
|
||||||
|
let reused = M.intersection backrefs references
|
||||||
|
mapM msg $ M.toList reused
|
||||||
|
|
||||||
|
msg (name, id) = warn id 2318 $ "This assignment is used again in this '" ++ cmd ++ "', but won't have taken effect. Use two '" ++ cmd ++ "'s."
|
||||||
|
|
||||||
|
findReferences cfga list = do
|
||||||
|
let graph = CF.graph cfga
|
||||||
|
let nodesMap = CF.tokenToNodes cfga
|
||||||
|
let nodes = S.unions $ map (\id -> M.findWithDefault S.empty id nodesMap) $ map getId $ list
|
||||||
|
let labels = mapMaybe (G.lab graph) $ S.toList nodes
|
||||||
|
let references = M.fromList $ concatMap refFromLabel labels
|
||||||
|
return references
|
||||||
|
|
||||||
|
refFromLabel lab =
|
||||||
|
case lab of
|
||||||
|
CFApplyEffects effects -> mapMaybe refFromEffect effects
|
||||||
|
_ -> []
|
||||||
|
refFromEffect e =
|
||||||
|
case e of
|
||||||
|
IdTagged id (CFReadVariable name) -> return (name, id)
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||||
|
|
101
src/ShellCheck/Checks/ControlFlow.hs
Normal file
101
src/ShellCheck/Checks/ControlFlow.hs
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
{-
|
||||||
|
Copyright 2022 Vidar Holen
|
||||||
|
|
||||||
|
This file is part of ShellCheck.
|
||||||
|
https://www.shellcheck.net
|
||||||
|
|
||||||
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
ShellCheck is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-}
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
|
||||||
|
-- Checks that run on the Control Flow Graph (as opposed to the AST)
|
||||||
|
-- This is scaffolding for a work in progress.
|
||||||
|
|
||||||
|
module ShellCheck.Checks.ControlFlow (checker, optionalChecks, ShellCheck.Checks.ControlFlow.runTests) where
|
||||||
|
|
||||||
|
import ShellCheck.AST
|
||||||
|
import ShellCheck.ASTLib
|
||||||
|
import ShellCheck.CFG hiding (cfgAnalysis)
|
||||||
|
import ShellCheck.CFGAnalysis
|
||||||
|
import ShellCheck.AnalyzerLib
|
||||||
|
import ShellCheck.Data
|
||||||
|
import ShellCheck.Interface
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import Control.Monad.Reader
|
||||||
|
import Data.Graph.Inductive.Graph
|
||||||
|
import qualified Data.Map as M
|
||||||
|
import qualified Data.Set as S
|
||||||
|
import Data.List
|
||||||
|
import Data.Maybe
|
||||||
|
|
||||||
|
import Test.QuickCheck.All (forAllProperties)
|
||||||
|
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||||
|
|
||||||
|
|
||||||
|
optionalChecks :: [CheckDescription]
|
||||||
|
optionalChecks = []
|
||||||
|
|
||||||
|
-- A check that runs on the entire graph
|
||||||
|
type ControlFlowCheck = Analysis
|
||||||
|
-- A check invoked once per node, with its (pre,post) data
|
||||||
|
type ControlFlowNodeCheck = LNode CFNode -> (ProgramState, ProgramState) -> Analysis
|
||||||
|
-- A check invoked once per effect, with its node's (pre,post) data
|
||||||
|
type ControlFlowEffectCheck = IdTagged CFEffect -> Node -> (ProgramState, ProgramState) -> Analysis
|
||||||
|
|
||||||
|
|
||||||
|
checker :: AnalysisSpec -> Parameters -> Checker
|
||||||
|
checker spec params = Checker {
|
||||||
|
perScript = const $ sequence_ controlFlowChecks,
|
||||||
|
perToken = const $ return ()
|
||||||
|
}
|
||||||
|
|
||||||
|
controlFlowChecks :: [ControlFlowCheck]
|
||||||
|
controlFlowChecks = [
|
||||||
|
runNodeChecks controlFlowNodeChecks
|
||||||
|
]
|
||||||
|
|
||||||
|
controlFlowNodeChecks :: [ControlFlowNodeCheck]
|
||||||
|
controlFlowNodeChecks = [
|
||||||
|
runEffectChecks controlFlowEffectChecks
|
||||||
|
]
|
||||||
|
|
||||||
|
controlFlowEffectChecks :: [ControlFlowEffectCheck]
|
||||||
|
controlFlowEffectChecks = [
|
||||||
|
]
|
||||||
|
|
||||||
|
runNodeChecks :: [ControlFlowNodeCheck] -> ControlFlowCheck
|
||||||
|
runNodeChecks perNode = do
|
||||||
|
cfg <- asks cfgAnalysis
|
||||||
|
mapM_ runOnAll cfg
|
||||||
|
where
|
||||||
|
getData datas n@(node, label) = do
|
||||||
|
(pre, post) <- M.lookup node datas
|
||||||
|
return (n, (pre, post))
|
||||||
|
|
||||||
|
runOn :: (LNode CFNode, (ProgramState, ProgramState)) -> Analysis
|
||||||
|
runOn (node, prepost) = mapM_ (\c -> c node prepost) perNode
|
||||||
|
runOnAll cfg = mapM_ runOn $ mapMaybe (getData $ nodeToData cfg) $ labNodes (graph cfg)
|
||||||
|
|
||||||
|
runEffectChecks :: [ControlFlowEffectCheck] -> ControlFlowNodeCheck
|
||||||
|
runEffectChecks list = checkNode
|
||||||
|
where
|
||||||
|
checkNode (node, label) prepost =
|
||||||
|
case label of
|
||||||
|
CFApplyEffects effects -> mapM_ (\effect -> mapM_ (\c -> c effect node prepost) list) effects
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
|
|
||||||
|
return []
|
||||||
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
|
@ -1,5 +1,5 @@
|
||||||
{-
|
{-
|
||||||
Copyright 2012-2016 Vidar Holen
|
Copyright 2012-2020 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
https://www.shellcheck.net
|
https://www.shellcheck.net
|
||||||
|
@ -19,12 +19,14 @@
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE ViewPatterns #-}
|
||||||
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
|
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
|
||||||
|
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
import ShellCheck.ASTLib
|
import ShellCheck.ASTLib
|
||||||
import ShellCheck.AnalyzerLib
|
import ShellCheck.AnalyzerLib
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Prelude
|
||||||
import ShellCheck.Regex
|
import ShellCheck.Regex
|
||||||
|
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
|
@ -59,6 +61,9 @@ checks = [
|
||||||
,checkBraceExpansionVars
|
,checkBraceExpansionVars
|
||||||
,checkMultiDimensionalArrays
|
,checkMultiDimensionalArrays
|
||||||
,checkPS1Assignments
|
,checkPS1Assignments
|
||||||
|
,checkMultipleBangs
|
||||||
|
,checkBangAfterPipe
|
||||||
|
,checkNegatedUnaryOps
|
||||||
]
|
]
|
||||||
|
|
||||||
testChecker (ForShell _ t) =
|
testChecker (ForShell _ t) =
|
||||||
|
@ -72,22 +77,24 @@ verifyNot c s = producesComments (testChecker c) s == Just False
|
||||||
prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
|
prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
|
||||||
prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
|
prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
|
||||||
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
|
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
|
||||||
checkForDecimals = ForShell [Sh, Dash, Bash] f
|
checkForDecimals = ForShell [Sh, Dash, BusyboxSh, Bash] f
|
||||||
where
|
where
|
||||||
f t@(TA_Expansion id _) = sequence_ $ do
|
f t@(TA_Expansion id _) = sequence_ $ do
|
||||||
str <- getLiteralString t
|
first:rest <- getLiteralString t
|
||||||
first <- str !!! 0
|
guard $ isDigit first && '.' `elem` rest
|
||||||
guard $ isDigit first && '.' `elem` str
|
|
||||||
return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
|
return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
|
||||||
f _ = return ()
|
f _ = return ()
|
||||||
|
|
||||||
|
|
||||||
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
|
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
|
||||||
prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"
|
prop_checkBashisms2 = verifyNot checkBashisms "[ foo -nt bar ]"
|
||||||
prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
|
prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
|
||||||
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
|
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
|
||||||
prop_checkBashisms5 = verify checkBashisms "source file"
|
prop_checkBashisms5 = verify checkBashisms "source file"
|
||||||
prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
|
prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
|
||||||
|
prop_checkBashisms6b = verify checkBashisms "test \"$a\" == 42"
|
||||||
|
prop_checkBashisms6c = verify checkBashisms "[ foo =~ bar ]"
|
||||||
|
prop_checkBashisms6d = verify checkBashisms "test foo =~ bar"
|
||||||
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
|
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
|
||||||
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
|
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
|
||||||
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
|
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
|
||||||
|
@ -103,6 +110,7 @@ prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"
|
||||||
prop_checkBashisms19 = verify checkBashisms "foo > file*.txt"
|
prop_checkBashisms19 = verify checkBashisms "foo > file*.txt"
|
||||||
prop_checkBashisms20 = verify checkBashisms "read -ra foo"
|
prop_checkBashisms20 = verify checkBashisms "read -ra foo"
|
||||||
prop_checkBashisms21 = verify checkBashisms "[ -a foo ]"
|
prop_checkBashisms21 = verify checkBashisms "[ -a foo ]"
|
||||||
|
prop_checkBashisms21b = verify checkBashisms "test -a foo"
|
||||||
prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]"
|
prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]"
|
||||||
prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT"
|
prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT"
|
||||||
prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM"
|
prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM"
|
||||||
|
@ -134,6 +142,8 @@ prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
|
||||||
prop_checkBashisms50 = verify checkBashisms "#!/bin/sh\ncmd >& file"
|
prop_checkBashisms50 = verify checkBashisms "#!/bin/sh\ncmd >& file"
|
||||||
prop_checkBashisms51 = verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
prop_checkBashisms51 = verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
||||||
prop_checkBashisms52 = verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
prop_checkBashisms52 = verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
||||||
|
prop_checkBashisms52b = verifyNot checkBashisms "#!/bin/sh\ncmd >& $var"
|
||||||
|
prop_checkBashisms52c = verify checkBashisms "#!/bin/sh\ncmd >& $dir/$var"
|
||||||
prop_checkBashisms53 = verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
prop_checkBashisms53 = verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
||||||
prop_checkBashisms54 = verify checkBashisms "#!/bin/sh\nfoo+=bar"
|
prop_checkBashisms54 = verify checkBashisms "#!/bin/sh\nfoo+=bar"
|
||||||
prop_checkBashisms55 = verify checkBashisms "#!/bin/sh\necho ${@%foo}"
|
prop_checkBashisms55 = verify checkBashisms "#!/bin/sh\necho ${@%foo}"
|
||||||
|
@ -178,110 +188,153 @@ prop_checkBashisms93 = verify checkBashisms "#!/bin/sh\necho $(( 10#$(date +%m)
|
||||||
prop_checkBashisms94 = verify checkBashisms "#!/bin/sh\n[ -v var ]"
|
prop_checkBashisms94 = verify checkBashisms "#!/bin/sh\n[ -v var ]"
|
||||||
prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_"
|
prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_"
|
||||||
prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
|
prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
|
||||||
checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}"
|
||||||
|
prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}"
|
||||||
|
prop_checkBashisms99 = verify checkBashisms "#!/bin/dash\necho [^f]oo"
|
||||||
|
prop_checkBashisms100 = verify checkBashisms "read -r"
|
||||||
|
prop_checkBashisms101 = verify checkBashisms "read"
|
||||||
|
prop_checkBashisms102 = verifyNot checkBashisms "read -r foo"
|
||||||
|
prop_checkBashisms103 = verifyNot checkBashisms "read foo"
|
||||||
|
prop_checkBashisms104 = verifyNot checkBashisms "read ''"
|
||||||
|
prop_checkBashisms105 = verifyNot checkBashisms "#!/bin/busybox sh\nset -o pipefail"
|
||||||
|
prop_checkBashisms106 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[[ \"$x\" = \"$x\" ]]"
|
||||||
|
prop_checkBashisms107 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[ \"$x\" == \"$x\" ]"
|
||||||
|
prop_checkBashisms108 = verifyNot checkBashisms "#!/bin/busybox sh\necho magic &> /dev/null"
|
||||||
|
prop_checkBashisms109 = verifyNot checkBashisms "#!/bin/busybox sh\ntrap stop EXIT SIGTERM"
|
||||||
|
prop_checkBashisms110 = verifyNot checkBashisms "#!/bin/busybox sh\nsource /dev/null"
|
||||||
|
prop_checkBashisms111 = verify checkBashisms "#!/bin/dash\nx='test'\n${x:0:3}" -- SC3057
|
||||||
|
prop_checkBashisms112 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x:0:3}" -- SC3057
|
||||||
|
prop_checkBashisms113 = verify checkBashisms "#!/bin/dash\nx='test'\n${x/st/xt}" -- SC3060
|
||||||
|
prop_checkBashisms114 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x/st/xt}" -- SC3060
|
||||||
|
prop_checkBashisms115 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x}" -- SC3053
|
||||||
|
prop_checkBashisms116 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x[1]}" -- SC3054
|
||||||
|
prop_checkBashisms117 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x[@]}" -- SC3055
|
||||||
|
prop_checkBashisms118 = verify checkBashisms "#!/bin/busybox sh\nxyz=1\n${!x*}" -- SC3056
|
||||||
|
prop_checkBashisms119 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x^^[t]}" -- SC3059
|
||||||
|
prop_checkBashisms120 = verify checkBashisms "#!/bin/sh\n[ x == y ]"
|
||||||
|
prop_checkBashisms121 = verifyNot checkBashisms "#!/bin/sh\n# shellcheck shell=busybox\n[ x == y ]"
|
||||||
|
prop_checkBashisms122 = verify checkBashisms "#!/bin/dash\n$'a'"
|
||||||
|
prop_checkBashisms123 = verifyNot checkBashisms "#!/bin/busybox sh\n$'a'"
|
||||||
|
prop_checkBashisms124 = verify checkBashisms "#!/bin/dash\ntype -p test"
|
||||||
|
prop_checkBashisms125 = verifyNot checkBashisms "#!/bin/busybox sh\ntype -p test"
|
||||||
|
prop_checkBashisms126 = verifyNot checkBashisms "#!/bin/busybox sh\nread -p foo -r bar"
|
||||||
|
prop_checkBashisms127 = verifyNot checkBashisms "#!/bin/busybox sh\necho -ne foo"
|
||||||
|
prop_checkBashisms128 = verify checkBashisms "#!/bin/dash\ntype -p test"
|
||||||
|
prop_checkBashisms129 = verify checkBashisms "#!/bin/sh\n[ -k /tmp ]"
|
||||||
|
prop_checkBashisms130 = verifyNot checkBashisms "#!/bin/dash\ntest -k /tmp"
|
||||||
|
prop_checkBashisms131 = verify checkBashisms "#!/bin/sh\n[ -o errexit ]"
|
||||||
|
checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
|
||||||
params <- ask
|
params <- ask
|
||||||
kludge params t
|
kludge params t
|
||||||
where
|
where
|
||||||
-- This code was copy-pasted from Analytics where params was a variable
|
-- This code was copy-pasted from Analytics where params was a variable
|
||||||
kludge params = bashism
|
kludge params = bashism
|
||||||
where
|
where
|
||||||
isDash = shellType params == Dash
|
isBusyboxSh = shellType params == BusyboxSh
|
||||||
warnMsg id s =
|
isDash = shellType params == Dash || isBusyboxSh
|
||||||
|
warnMsg id code s =
|
||||||
if isDash
|
if isDash
|
||||||
then warn id 2169 $ "In dash, " ++ s ++ " not supported."
|
then err id code $ "In dash, " ++ s ++ " not supported."
|
||||||
else warn id 2039 $ "In POSIX sh, " ++ s ++ " undefined."
|
else warn id code $ "In POSIX sh, " ++ s ++ " undefined."
|
||||||
|
asStr = getLiteralString
|
||||||
|
|
||||||
|
bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is"
|
||||||
|
bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is"
|
||||||
|
bashism (T_DollarSingleQuoted id _) =
|
||||||
|
unless isBusyboxSh $ warnMsg id 3003 "$'..' is"
|
||||||
|
bashism (T_DollarDoubleQuoted id _) = warnMsg id 3004 "$\"..\" is"
|
||||||
|
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id 3005 "arithmetic for loops are"
|
||||||
|
bashism (T_Arithmetic id _) = warnMsg id 3006 "standalone ((..)) is"
|
||||||
|
bashism (T_DollarBracket id _) = warnMsg id 3007 "$[..] in place of $((..)) is"
|
||||||
|
bashism (T_SelectIn id _ _ _) = warnMsg id 3008 "select loops are"
|
||||||
|
bashism (T_BraceExpansion id _) = warnMsg id 3009 "brace expansion is"
|
||||||
|
bashism (T_Condition id DoubleBracket _) =
|
||||||
|
unless isBusyboxSh $ warnMsg id 3010 "[[ ]] is"
|
||||||
|
bashism (T_HereString id _) = warnMsg id 3011 "here-strings are"
|
||||||
|
|
||||||
|
bashism (TC_Binary id _ op _ _) =
|
||||||
|
checkTestOp bashismBinaryTestFlags op id
|
||||||
|
bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs]) =
|
||||||
|
checkTestOp bashismBinaryTestFlags op id
|
||||||
|
bashism (TC_Unary id _ op _) =
|
||||||
|
checkTestOp bashismUnaryTestFlags op id
|
||||||
|
bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just op, _]) =
|
||||||
|
checkTestOp bashismUnaryTestFlags op id
|
||||||
|
|
||||||
bashism (T_ProcSub id _ _) = warnMsg id "process substitution is"
|
|
||||||
bashism (T_Extglob id _ _) = warnMsg id "extglob is"
|
|
||||||
bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is"
|
|
||||||
bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\" is"
|
|
||||||
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id "arithmetic for loops are"
|
|
||||||
bashism (T_Arithmetic id _) = warnMsg id "standalone ((..)) is"
|
|
||||||
bashism (T_DollarBracket id _) = warnMsg id "$[..] in place of $((..)) is"
|
|
||||||
bashism (T_SelectIn id _ _ _) = warnMsg id "select loops are"
|
|
||||||
bashism (T_BraceExpansion id _) = warnMsg id "brace expansion is"
|
|
||||||
bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is"
|
|
||||||
bashism (T_HereString id _) = warnMsg id "here-strings are"
|
|
||||||
bashism (TC_Binary id SingleBracket op _ _)
|
|
||||||
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
|
|
||||||
unless isDash $ warnMsg id $ "lexicographical " ++ op ++ " is"
|
|
||||||
bashism (TC_Binary id SingleBracket op _ _)
|
|
||||||
| op `elem` [ "-ot", "-nt", "-ef" ] =
|
|
||||||
unless isDash $ warnMsg id $ op ++ " is"
|
|
||||||
bashism (TC_Binary id SingleBracket "==" _ _) =
|
|
||||||
warnMsg id "== in place of = is"
|
|
||||||
bashism (TC_Binary id SingleBracket "=~" _ _) =
|
|
||||||
warnMsg id "=~ regex matching is"
|
|
||||||
bashism (TC_Unary id SingleBracket "-v" _) =
|
|
||||||
warnMsg id "unary -v (in place of [ -n \"${var+x}\" ]) is"
|
|
||||||
bashism (TC_Unary id _ "-a" _) =
|
|
||||||
warnMsg id "unary -a in place of -e is"
|
|
||||||
bashism (TA_Unary id op _)
|
bashism (TA_Unary id op _)
|
||||||
| op `elem` [ "|++", "|--", "++|", "--|"] =
|
| op `elem` [ "|++", "|--", "++|", "--|"] =
|
||||||
warnMsg id $ filter (/= '|') op ++ " is"
|
warnMsg id 3018 $ filter (/= '|') op ++ " is"
|
||||||
bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are"
|
bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
|
||||||
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is"
|
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) =
|
||||||
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id ">& is"
|
unless isBusyboxSh $ warnMsg id 3020 "&> is"
|
||||||
bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are"
|
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) file)) =
|
||||||
|
unless (all isDigit $ onlyLiteralString file) $ warnMsg id 3021 ">& filename (as opposed to >& fd) is"
|
||||||
|
bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are"
|
||||||
bashism (T_FdRedirect id num _)
|
bashism (T_FdRedirect id num _)
|
||||||
| all isDigit num && length num > 1 = warnMsg id "FDs outside 0-9 are"
|
| all isDigit num && length num > 1 = warnMsg id 3023 "FDs outside 0-9 are"
|
||||||
bashism (T_Assignment id Append _ _ _) =
|
bashism (T_Assignment id Append _ _ _) =
|
||||||
warnMsg id "+= is"
|
warnMsg id 3024 "+= is"
|
||||||
bashism (T_IoFile id _ word) | isNetworked =
|
bashism (T_IoFile id _ word) | isNetworked =
|
||||||
warnMsg id "/dev/{tcp,udp} is"
|
warnMsg id 3025 "/dev/{tcp,udp} is"
|
||||||
where
|
where
|
||||||
file = onlyLiteralString word
|
file = onlyLiteralString word
|
||||||
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
|
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
|
||||||
bashism (T_Glob id str) | "[^" `isInfixOf` str =
|
bashism (T_Glob id str) | "[^" `isInfixOf` str =
|
||||||
warnMsg id "^ in place of ! in glob bracket expressions is"
|
warnMsg id 3026 "^ in place of ! in glob bracket expressions is"
|
||||||
|
|
||||||
bashism t@(TA_Variable id str _) | isBashVariable str =
|
bashism t@(TA_Variable id str _) | isBashVariable str =
|
||||||
warnMsg id $ str ++ " is"
|
warnMsg id 3028 $ str ++ " is"
|
||||||
|
|
||||||
bashism t@(T_DollarBraced id _ token) = do
|
bashism t@(T_DollarBraced id _ token) = do
|
||||||
mapM_ check expansion
|
unless isBusyboxSh $ mapM_ check simpleExpansions
|
||||||
|
mapM_ check advancedExpansions
|
||||||
when (isBashVariable var) $
|
when (isBashVariable var) $
|
||||||
warnMsg id $ var ++ " is"
|
warnMsg id 3028 $ var ++ " is"
|
||||||
where
|
where
|
||||||
str = bracedString t
|
str = concat $ oversimplify token
|
||||||
var = getBracedReference str
|
var = getBracedReference str
|
||||||
check (regex, feature) =
|
check (regex, code, feature) =
|
||||||
when (isJust $ matchRegex regex str) $ warnMsg id feature
|
when (isJust $ matchRegex regex str) $ warnMsg id code feature
|
||||||
|
|
||||||
bashism t@(T_Pipe id "|&") =
|
bashism t@(T_Pipe id "|&") =
|
||||||
warnMsg id "|& in place of 2>&1 | is"
|
warnMsg id 3029 "|& in place of 2>&1 | is"
|
||||||
bashism (T_Array id _) =
|
bashism (T_Array id _) =
|
||||||
warnMsg id "arrays are"
|
warnMsg id 3030 "arrays are"
|
||||||
bashism (T_IoFile id _ t) | isGlob t =
|
bashism (T_IoFile id _ t) | isGlob t =
|
||||||
warnMsg id "redirecting to/from globs is"
|
warnMsg id 3031 "redirecting to/from globs is"
|
||||||
bashism (T_CoProc id _ _) =
|
bashism (T_CoProc id _ _) =
|
||||||
warnMsg id "coproc is"
|
warnMsg id 3032 "coproc is"
|
||||||
|
|
||||||
bashism (T_Function id _ _ str _) | not (isVariableName str) =
|
bashism (T_Function id _ _ str _) | not (isVariableName str) =
|
||||||
warnMsg id "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
|
warnMsg id 3033 "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
|
||||||
|
|
||||||
bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
|
bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
|
||||||
warnMsg id "$(<file) to read files is"
|
warnMsg id 3034 "$(<file) to read files is"
|
||||||
bashism (T_Backticked id [x]) | isOnlyRedirection x =
|
bashism (T_Backticked id [x]) | isOnlyRedirection x =
|
||||||
warnMsg id "`<file` to read files is"
|
warnMsg id 3035 "`<file` to read files is"
|
||||||
|
|
||||||
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
||||||
| t `isCommand` "echo" && argString `matches` flagRegex =
|
| t `isCommand` "echo" && argString `matches` flagRegex =
|
||||||
if isDash
|
if isBusyboxSh
|
||||||
|
then
|
||||||
|
unless (argString `matches` busyboxFlagRegex) $
|
||||||
|
warnMsg (getId arg) 3036 "echo flags besides -n and -e"
|
||||||
|
else if isDash
|
||||||
then
|
then
|
||||||
when (argString /= "-n") $
|
when (argString /= "-n") $
|
||||||
warnMsg (getId arg) "echo flags besides -n"
|
warnMsg (getId arg) 3036 "echo flags besides -n"
|
||||||
else
|
else
|
||||||
warnMsg (getId arg) "echo flags are"
|
warnMsg (getId arg) 3037 "echo flags are"
|
||||||
where
|
where
|
||||||
argString = concat $ oversimplify arg
|
argString = concat $ oversimplify arg
|
||||||
flagRegex = mkRegex "^-[eEsn]+$"
|
flagRegex = mkRegex "^-[eEsn]+$"
|
||||||
|
busyboxFlagRegex = mkRegex "^-[en]+$"
|
||||||
|
|
||||||
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
||||||
| t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
|
| getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
|
||||||
warnMsg (getId arg) "exec flags are"
|
warnMsg (getId arg) 3038 "exec flags are"
|
||||||
bashism t@(T_SimpleCommand id _ _)
|
bashism t@(T_SimpleCommand id _ _)
|
||||||
| t `isCommand` "let" = warnMsg id "'let' is"
|
| t `isCommand` "let" = warnMsg id 3039 "'let' is"
|
||||||
bashism t@(T_SimpleCommand _ _ (cmd:args))
|
bashism t@(T_SimpleCommand _ _ (cmd:args))
|
||||||
| t `isCommand` "set" = unless isDash $
|
| t `isCommand` "set" = unless isDash $
|
||||||
checkOptions $ getLiteralArgs args
|
checkOptions $ getLiteralArgs args
|
||||||
|
@ -289,16 +342,17 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||||
-- Get the literal options from a list of arguments,
|
-- Get the literal options from a list of arguments,
|
||||||
-- up until the first non-literal one
|
-- up until the first non-literal one
|
||||||
getLiteralArgs :: [Token] -> [(Id, String)]
|
getLiteralArgs :: [Token] -> [(Id, String)]
|
||||||
getLiteralArgs (first:rest) = fromMaybe [] $ do
|
getLiteralArgs = foldr go []
|
||||||
str <- getLiteralString first
|
where
|
||||||
return $ (getId first, str) : getLiteralArgs rest
|
go first rest = case getLiteralString first of
|
||||||
getLiteralArgs [] = []
|
Just str -> (getId first, str) : rest
|
||||||
|
Nothing -> []
|
||||||
|
|
||||||
-- Check a flag-option pair (such as -o errexit)
|
-- Check a flag-option pair (such as -o errexit)
|
||||||
checkOptions (flag@(fid,flag') : opt@(oid,opt') : rest)
|
checkOptions (flag@(fid,flag') : opt@(oid,opt') : rest)
|
||||||
| flag' `matches` oFlagRegex = do
|
| flag' `matches` oFlagRegex = do
|
||||||
when (opt' `notElem` longOptions) $
|
when (opt' `notElem` longOptions) $
|
||||||
warnMsg oid $ "set option " <> opt' <> " is"
|
warnMsg oid 3040 $ "set option " <> opt' <> " is"
|
||||||
checkFlags (flag:rest)
|
checkFlags (flag:rest)
|
||||||
| otherwise = checkFlags (flag:opt:rest)
|
| otherwise = checkFlags (flag:opt:rest)
|
||||||
checkOptions (flag:rest) = checkFlags (flag:rest)
|
checkOptions (flag:rest) = checkFlags (flag:rest)
|
||||||
|
@ -311,10 +365,10 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||||
unless (flag' `matches` validFlagsRegex) $
|
unless (flag' `matches` validFlagsRegex) $
|
||||||
forM_ (tail flag') $ \letter ->
|
forM_ (tail flag') $ \letter ->
|
||||||
when (letter `notElem` optionsSet) $
|
when (letter `notElem` optionsSet) $
|
||||||
warnMsg fid $ "set flag " <> ('-':letter:" is")
|
warnMsg fid 3041 $ "set flag " <> ('-':letter:" is")
|
||||||
checkOptions rest
|
checkOptions rest
|
||||||
| beginsWithDoubleDash flag' = do
|
| beginsWithDoubleDash flag' = do
|
||||||
warnMsg fid $ "set flag " <> flag' <> " is"
|
warnMsg fid 3042 $ "set flag " <> flag' <> " is"
|
||||||
checkOptions rest
|
checkOptions rest
|
||||||
-- Either a word that doesn't start with a dash, or simply '--',
|
-- Either a word that doesn't start with a dash, or simply '--',
|
||||||
-- so stop checking.
|
-- so stop checking.
|
||||||
|
@ -336,16 +390,20 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||||
let name = fromMaybe "" $ getCommandName t
|
let name = fromMaybe "" $ getCommandName t
|
||||||
flags = getLeadingFlags t
|
flags = getLeadingFlags t
|
||||||
in do
|
in do
|
||||||
|
when (name == "local" && not isDash) $
|
||||||
|
-- This is so commonly accepted that we'll make it a special case
|
||||||
|
warnMsg id 3043 $ "'local' is"
|
||||||
when (name `elem` unsupportedCommands) $
|
when (name `elem` unsupportedCommands) $
|
||||||
warnMsg id $ "'" ++ name ++ "' is"
|
warnMsg id 3044 $ "'" ++ name ++ "' is"
|
||||||
sequence_ $ do
|
sequence_ $ do
|
||||||
allowed' <- Map.lookup name allowedFlags
|
allowed' <- Map.lookup name allowedFlags
|
||||||
allowed <- allowed'
|
allowed <- allowed'
|
||||||
(word, flag) <- find
|
(word, flag) <- find
|
||||||
(\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
|
(\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
|
||||||
return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"
|
return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is"
|
||||||
|
|
||||||
when (name == "source") $ warnMsg id "'source' in place of '.' is"
|
when (name == "source" && not isBusyboxSh) $
|
||||||
|
warnMsg id 3046 "'source' in place of '.' is"
|
||||||
when (name == "trap") $
|
when (name == "trap") $
|
||||||
let
|
let
|
||||||
check token = sequence_ $ do
|
check token = sequence_ $ do
|
||||||
|
@ -353,12 +411,12 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||||
let upper = map toUpper str
|
let upper = map toUpper str
|
||||||
return $ do
|
return $ do
|
||||||
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
|
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
|
||||||
warnMsg (getId token) $ "trapping " ++ str ++ " is"
|
warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is"
|
||||||
when ("SIG" `isPrefixOf` upper) $
|
when (not isBusyboxSh && "SIG" `isPrefixOf` upper) $
|
||||||
warnMsg (getId token)
|
warnMsg (getId token) 3048
|
||||||
"prefixing signal names with 'SIG' is"
|
"prefixing signal names with 'SIG' is"
|
||||||
when (not isDash && upper /= str) $
|
when (not isDash && upper /= str) $
|
||||||
warnMsg (getId token)
|
warnMsg (getId token) 3049
|
||||||
"using lower/mixed case for signal names is"
|
"using lower/mixed case for signal names is"
|
||||||
in
|
in
|
||||||
mapM_ check (drop 1 rest)
|
mapM_ check (drop 1 rest)
|
||||||
|
@ -367,13 +425,16 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||||
format <- rest !!! 0 -- flags are covered by allowedFlags
|
format <- rest !!! 0 -- flags are covered by allowedFlags
|
||||||
let literal = onlyLiteralString format
|
let literal = onlyLiteralString format
|
||||||
guard $ "%q" `isInfixOf` literal
|
guard $ "%q" `isInfixOf` literal
|
||||||
return $ warnMsg (getId format) "printf %q is"
|
return $ warnMsg (getId format) 3050 "printf %q is"
|
||||||
|
|
||||||
|
when (name == "read" && all isFlag rest) $
|
||||||
|
warnMsg (getId cmd) 3061 "read without a variable is"
|
||||||
where
|
where
|
||||||
unsupportedCommands = [
|
unsupportedCommands = [
|
||||||
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
||||||
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
|
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
|
||||||
"typeset"
|
"typeset"
|
||||||
] ++ if not isDash then ["local"] else []
|
]
|
||||||
allowedFlags = Map.fromList [
|
allowedFlags = Map.fromList [
|
||||||
("cd", Just ["L", "P"]),
|
("cd", Just ["L", "P"]),
|
||||||
("exec", Just []),
|
("exec", Just []),
|
||||||
|
@ -381,38 +442,48 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||||
("hash", Just $ if isDash then ["r", "v"] else ["r"]),
|
("hash", Just $ if isDash then ["r", "v"] else ["r"]),
|
||||||
("jobs", Just ["l", "p"]),
|
("jobs", Just ["l", "p"]),
|
||||||
("printf", Just []),
|
("printf", Just []),
|
||||||
("read", Just $ if isDash then ["r", "p"] else ["r"]),
|
("read", Just $ if isDash || isBusyboxSh then ["r", "p"] else ["r"]),
|
||||||
("readonly", Just ["p"]),
|
("readonly", Just ["p"]),
|
||||||
("trap", Just []),
|
("trap", Just []),
|
||||||
("type", Just []),
|
("type", Just $ if isBusyboxSh then ["p"] else []),
|
||||||
("ulimit", if isDash then Nothing else Just ["f"]),
|
("ulimit", if isDash then Nothing else Just ["f"]),
|
||||||
("umask", Just ["S"]),
|
("umask", Just ["S"]),
|
||||||
("unset", Just ["f", "v"]),
|
("unset", Just ["f", "v"]),
|
||||||
("wait", Just [])
|
("wait", Just [])
|
||||||
]
|
]
|
||||||
bashism t@(T_SourceCommand id src _) =
|
bashism t@(T_SourceCommand id src _)
|
||||||
let name = fromMaybe "" $ getCommandName src
|
| getCommandName src == Just "source" =
|
||||||
in when (name == "source") $ warnMsg id "'source' in place of '.' is"
|
unless isBusyboxSh $
|
||||||
bashism (TA_Expansion _ (T_Literal id str : _)) | str `matches` radix =
|
warnMsg id 3051 "'source' in place of '.' is"
|
||||||
when (str `matches` radix) $ warnMsg id "arithmetic base conversion is"
|
bashism (TA_Expansion _ (T_Literal id str : _))
|
||||||
|
| str `matches` radix = warnMsg id 3052 "arithmetic base conversion is"
|
||||||
where
|
where
|
||||||
radix = mkRegex "^[0-9]+#"
|
radix = mkRegex "^[0-9]+#"
|
||||||
bashism _ = return ()
|
bashism _ = return ()
|
||||||
|
|
||||||
varChars="_0-9a-zA-Z"
|
varChars="_0-9a-zA-Z"
|
||||||
expansion = let re = mkRegex in [
|
advancedExpansions = let re = mkRegex in [
|
||||||
(re $ "^![" ++ varChars ++ "]", "indirect expansion is"),
|
(re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"),
|
||||||
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
|
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"),
|
||||||
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
|
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"),
|
||||||
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
|
(re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"),
|
||||||
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", "string indexing is"),
|
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is")
|
||||||
(re $ "^([*@][%#]|#[@*])", "string operations on $@/$* are"),
|
]
|
||||||
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")
|
simpleExpansions = let re = mkRegex in [
|
||||||
|
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", 3057, "string indexing is"),
|
||||||
|
(re $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"),
|
||||||
|
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is")
|
||||||
]
|
]
|
||||||
bashVars = [
|
bashVars = [
|
||||||
|
-- This list deliberately excludes $BASH_VERSION as it's often used
|
||||||
|
-- for shell identification.
|
||||||
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
||||||
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS",
|
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS",
|
||||||
"_"
|
"_", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
|
||||||
|
"BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND",
|
||||||
|
"BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE",
|
||||||
|
"BASH_SUBSHELL", "BASH_VERSINFO", "EPOCHREALTIME", "EPOCHSECONDS",
|
||||||
|
"FUNCNAME", "GROUPS", "MACHTYPE", "MAPFILE"
|
||||||
]
|
]
|
||||||
bashDynamicVars = [ "RANDOM", "SECONDS" ]
|
bashDynamicVars = [ "RANDOM", "SECONDS" ]
|
||||||
dashVars = [ "_" ]
|
dashVars = [ "_" ]
|
||||||
|
@ -426,6 +497,50 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||||
Assignment (_, _, name, _) -> name == var
|
Assignment (_, _, name, _) -> name == var
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
|
checkTestOp table op id = sequence_ $ do
|
||||||
|
(code, shells, msg) <- Map.lookup op table
|
||||||
|
guard . not $ shellType params `elem` shells
|
||||||
|
return $ warnMsg id code (msg op)
|
||||||
|
|
||||||
|
|
||||||
|
buildTestFlagMap list = Map.fromList $ concatMap (\(x,y) -> map (\c -> (c,y)) x) list
|
||||||
|
bashismBinaryTestFlags = buildTestFlagMap [
|
||||||
|
-- ([list of applicable flags],
|
||||||
|
-- (error code, exempt shells, message builder :: String -> String)),
|
||||||
|
--
|
||||||
|
-- Distinct error codes allow the wiki to give more helpful, targeted
|
||||||
|
-- information.
|
||||||
|
(["<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="],
|
||||||
|
(3012, [Dash, BusyboxSh], \op -> "lexicographical " ++ op ++ " is")),
|
||||||
|
(["=="],
|
||||||
|
(3014, [BusyboxSh], \op -> op ++ " in place of = is")),
|
||||||
|
(["=~"],
|
||||||
|
(3015, [], \op -> op ++ " regex matching is")),
|
||||||
|
|
||||||
|
([], (0,[],const ""))
|
||||||
|
]
|
||||||
|
bashismUnaryTestFlags = buildTestFlagMap [
|
||||||
|
(["-v"],
|
||||||
|
(3016, [], \op -> "test " ++ op ++ " (in place of [ -n \"${var+x}\" ]) is")),
|
||||||
|
(["-a"],
|
||||||
|
(3017, [], \op -> "unary " ++ op ++ " in place of -e is")),
|
||||||
|
(["-o"],
|
||||||
|
(3062, [], \op -> "test " ++ op ++ " to check options is")),
|
||||||
|
(["-R"],
|
||||||
|
(3063, [], \op -> "test " ++ op ++ " and namerefs in general are")),
|
||||||
|
(["-N"],
|
||||||
|
(3064, [], \op -> "test " ++ op ++ " is")),
|
||||||
|
(["-k"],
|
||||||
|
(3065, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")),
|
||||||
|
(["-G"],
|
||||||
|
(3066, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")),
|
||||||
|
(["-O"],
|
||||||
|
(3067, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")),
|
||||||
|
|
||||||
|
([], (0,[],const ""))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
|
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
|
||||||
prop_checkEchoSed1b = verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")"
|
prop_checkEchoSed1b = verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")"
|
||||||
prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
|
prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
|
||||||
|
@ -506,13 +621,13 @@ checkMultiDimensionalArrays = ForShell [Bash] f
|
||||||
case token of
|
case token of
|
||||||
T_Assignment _ _ name (first:second:_) _ -> about second
|
T_Assignment _ _ name (first:second:_) _ -> about second
|
||||||
T_IndexedElement _ (first:second:_) _ -> about second
|
T_IndexedElement _ (first:second:_) _ -> about second
|
||||||
T_DollarBraced {} ->
|
T_DollarBraced _ _ l ->
|
||||||
when (isMultiDim token) $ about token
|
when (isMultiDim l) $ about token
|
||||||
_ -> return ()
|
_ -> return ()
|
||||||
about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays."
|
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
|
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
|
||||||
isMultiDim t = getBracedModifier (bracedString t) `matches` re
|
isMultiDim l = getBracedModifier (concat $ oversimplify l) `matches` re
|
||||||
|
|
||||||
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
|
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
|
||||||
prop_checkPS11a = verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
|
prop_checkPS11a = verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
|
||||||
|
@ -541,5 +656,46 @@ checkPS1Assignments = ForShell [Bash] f
|
||||||
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
|
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkMultipleBangs1 = verify checkMultipleBangs "! ! true"
|
||||||
|
prop_checkMultipleBangs2 = verifyNot checkMultipleBangs "! true"
|
||||||
|
checkMultipleBangs = ForShell [Dash, BusyboxSh, Sh] f
|
||||||
|
where
|
||||||
|
f token = case token of
|
||||||
|
T_Banged id (T_Banged _ _) ->
|
||||||
|
err id 2325 "Multiple ! in front of pipelines are a bash/ksh extension. Use only 0 or 1."
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkBangAfterPipe1 = verify checkBangAfterPipe "true | ! true"
|
||||||
|
prop_checkBangAfterPipe2 = verifyNot checkBangAfterPipe "true | ( ! true )"
|
||||||
|
prop_checkBangAfterPipe3 = verifyNot checkBangAfterPipe "! ! true | true"
|
||||||
|
checkBangAfterPipe = ForShell [Dash, BusyboxSh, Sh, Bash] f
|
||||||
|
where
|
||||||
|
f token = case token of
|
||||||
|
T_Pipeline _ _ cmds -> mapM_ check cmds
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
|
check token = case token of
|
||||||
|
T_Banged id _ ->
|
||||||
|
err id 2326 "! is not allowed in the middle of pipelines. Use command group as in cmd | { ! cmd; } if necessary."
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkNegatedUnaryOps1 = verify checkNegatedUnaryOps "[ ! -o braceexpand ]"
|
||||||
|
prop_checkNegatedUnaryOps2 = verifyNot checkNegatedUnaryOps "[ -o braceexpand ]"
|
||||||
|
prop_checkNegatedUnaryOps3 = verifyNot checkNegatedUnaryOps "[[ ! -o braceexpand ]]"
|
||||||
|
prop_checkNegatedUnaryOps4 = verifyNot checkNegatedUnaryOps "! [ -o braceexpand ]"
|
||||||
|
prop_checkNegatedUnaryOps5 = verify checkNegatedUnaryOps "[ ! -a file ]"
|
||||||
|
checkNegatedUnaryOps = ForShell [Bash] f
|
||||||
|
where
|
||||||
|
f token = case token of
|
||||||
|
TC_Unary id SingleBracket "!" (TC_Unary _ _ op _) | op `elem` ["-a", "-o"] ->
|
||||||
|
err id 2332 $ msg op
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
|
msg "-o" = "[ ! -o opt ] is always true because -o becomes logical OR. Use [[ ]] or ! [ -o opt ]."
|
||||||
|
msg "-a" = "[ ! -a file ] is always true because -a becomes logical AND. Use -e instead."
|
||||||
|
msg _ = pleaseReport "unhandled negated unary message"
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||||
|
|
|
@ -2,33 +2,55 @@ module ShellCheck.Data where
|
||||||
|
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import Data.Version (showVersion)
|
import Data.Version (showVersion)
|
||||||
import Paths_ShellCheck (version)
|
|
||||||
|
|
||||||
|
|
||||||
|
{-
|
||||||
|
If you are here because you saw an error about Paths_ShellCheck in this file,
|
||||||
|
simply comment out the import below and define the version as a constant string.
|
||||||
|
|
||||||
|
Instead of:
|
||||||
|
|
||||||
|
import Paths_ShellCheck (version)
|
||||||
shellcheckVersion = showVersion version
|
shellcheckVersion = showVersion version
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
-- import Paths_ShellCheck (version)
|
||||||
|
shellcheckVersion = "kludge"
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Paths_ShellCheck (version)
|
||||||
|
shellcheckVersion = showVersion version -- VERSIONSTRING
|
||||||
|
|
||||||
|
|
||||||
internalVariables = [
|
internalVariables = [
|
||||||
-- Generic
|
-- Generic
|
||||||
"", "_", "rest", "REST",
|
"", "_", "rest", "REST",
|
||||||
|
|
||||||
-- Bash
|
-- Bash
|
||||||
"BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
|
"BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
|
||||||
"BASH_ARGV", "BASH_CMDS", "BASH_COMMAND", "BASH_EXECUTION_STRING",
|
"BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND",
|
||||||
"BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL",
|
"BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_LOADABLES_PATH",
|
||||||
"BASH_VERSINFO", "BASH_VERSION", "COMP_CWORD", "COMP_KEY",
|
"BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL", "BASH_VERSINFO",
|
||||||
"COMP_LINE", "COMP_POINT", "COMP_TYPE", "COMP_WORDBREAKS",
|
"BASH_VERSION", "COMP_CWORD", "COMP_KEY", "COMP_LINE", "COMP_POINT",
|
||||||
"COMP_WORDS", "COPROC", "DIRSTACK", "EUID", "FUNCNAME", "GROUPS",
|
"COMP_TYPE", "COMP_WORDBREAKS", "COMP_WORDS", "COPROC", "DIRSTACK",
|
||||||
"HISTCMD", "HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE",
|
"EPOCHREALTIME", "EPOCHSECONDS", "EUID", "FUNCNAME", "GROUPS", "HISTCMD",
|
||||||
"OLDPWD", "OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD",
|
"HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE", "OLDPWD",
|
||||||
"RANDOM", "READLINE_LINE", "READLINE_POINT", "REPLY", "SECONDS",
|
"OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD", "RANDOM",
|
||||||
"SHELLOPTS", "SHLVL", "UID", "BASH_ENV", "BASH_XTRACEFD", "CDPATH",
|
"READLINE_ARGUMENT", "READLINE_LINE", "READLINE_MARK", "READLINE_POINT",
|
||||||
"COLUMNS", "COMPREPLY", "EMACS", "ENV", "FCEDIT", "FIGNORE",
|
"REPLY", "SECONDS", "SHELLOPTS", "SHLVL", "SRANDOM", "UID", "BASH_COMPAT",
|
||||||
|
"BASH_ENV", "BASH_XTRACEFD", "CDPATH", "CHILD_MAX", "COLUMNS",
|
||||||
|
"COMPREPLY", "EMACS", "ENV", "EXECIGNORE", "FCEDIT", "FIGNORE",
|
||||||
"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", "INSIDE_EMACS", "LANG", "LC_ALL", "LC_COLLATE",
|
||||||
"LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LINES", "MAIL",
|
"LC_CTYPE", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME",
|
||||||
"MAILCHECK", "MAILPATH", "OPTERR", "PATH", "POSIXLY_CORRECT",
|
"LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH",
|
||||||
"PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL",
|
"POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1",
|
||||||
"TIMEFORMAT", "TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC",
|
"PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR",
|
||||||
|
"BASH_MONOSECONDS", "BASH_TRAPSIG", "GLOBSORT",
|
||||||
|
"auto_resume", "histchars",
|
||||||
|
|
||||||
-- Other
|
-- Other
|
||||||
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
|
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
|
||||||
|
@ -41,15 +63,23 @@ internalVariables = [
|
||||||
, "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP",
|
, "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP",
|
||||||
"FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION",
|
"FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION",
|
||||||
"flags_error", "flags_return"
|
"flags_error", "flags_return"
|
||||||
|
|
||||||
|
-- Bats
|
||||||
|
,"stderr", "stderr_lines"
|
||||||
]
|
]
|
||||||
|
|
||||||
specialVariablesWithoutSpaces = [
|
specialIntegerVariables = [
|
||||||
"$", "-", "?", "!", "#"
|
"$", "?", "!", "#"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
specialVariablesWithoutSpaces = "-" : specialIntegerVariables
|
||||||
|
|
||||||
variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
|
variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
|
||||||
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
|
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID",
|
||||||
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
|
"EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM",
|
||||||
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
|
"READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS",
|
||||||
|
"SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE",
|
||||||
|
"HISTSIZE", "LINES", "BASH_MONOSECONDS", "BASH_TRAPSIG"
|
||||||
|
|
||||||
-- shflags
|
-- shflags
|
||||||
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
|
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
|
||||||
|
@ -95,10 +125,10 @@ commonCommands = [
|
||||||
|
|
||||||
nonReadingCommands = [
|
nonReadingCommands = [
|
||||||
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
|
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
|
||||||
"cp", "du", "echo", "export", "false", "fg", "fuser", "getconf",
|
"cp", "du", "echo", "export", "fg", "fuser", "getconf",
|
||||||
"getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
|
"getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
|
||||||
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
|
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
|
||||||
"set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname"
|
"set", "sleep", "touch", "trap", "ulimit", "unalias", "uname"
|
||||||
]
|
]
|
||||||
|
|
||||||
sampleWords = [
|
sampleWords = [
|
||||||
|
@ -130,11 +160,18 @@ shellForExecutable name =
|
||||||
"sh" -> return Sh
|
"sh" -> return Sh
|
||||||
"bash" -> return Bash
|
"bash" -> return Bash
|
||||||
"bats" -> return Bash
|
"bats" -> return Bash
|
||||||
|
"busybox" -> return BusyboxSh -- Used for directives and --shell=busybox
|
||||||
|
"busybox sh" -> return BusyboxSh
|
||||||
|
"busybox ash" -> return BusyboxSh
|
||||||
"dash" -> return Dash
|
"dash" -> return Dash
|
||||||
"ash" -> return Dash -- There's also a warning for this.
|
"ash" -> return Dash -- There's also a warning for this.
|
||||||
"ksh" -> return Ksh
|
"ksh" -> return Ksh
|
||||||
"ksh88" -> return Ksh
|
"ksh88" -> return Ksh
|
||||||
"ksh93" -> return Ksh
|
"ksh93" -> return Ksh
|
||||||
|
"oksh" -> return Ksh
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
flagsForRead = "sreu:n:N:i:p:a:t:"
|
flagsForRead = "sreu:n:N:i:p:a:t:"
|
||||||
|
flagsForMapfile = "d:n:O:s:u:C:c:t"
|
||||||
|
|
||||||
|
declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"]
|
||||||
|
|
313
src/ShellCheck/Debug.hs
Normal file
313
src/ShellCheck/Debug.hs
Normal file
|
@ -0,0 +1,313 @@
|
||||||
|
{-
|
||||||
|
|
||||||
|
This file contains useful functions for debugging and developing ShellCheck.
|
||||||
|
|
||||||
|
To invoke them interactively, run:
|
||||||
|
|
||||||
|
cabal repl
|
||||||
|
|
||||||
|
At the ghci prompt, enter:
|
||||||
|
|
||||||
|
:load ShellCheck.Debug
|
||||||
|
|
||||||
|
You can now invoke the functions. Here are some examples:
|
||||||
|
|
||||||
|
shellcheckString "echo $1"
|
||||||
|
stringToAst "(( x+1 ))"
|
||||||
|
stringToCfg "if foo; then bar; else baz; fi"
|
||||||
|
writeFile "/tmp/test.dot" $ stringToCfgViz "while foo; do bar; done"
|
||||||
|
|
||||||
|
The latter file can be rendered to png with GraphViz:
|
||||||
|
|
||||||
|
dot -Tpng /tmp/test.dot > /tmp/test.png
|
||||||
|
|
||||||
|
To run all unit tests in a module:
|
||||||
|
|
||||||
|
ShellCheck.Parser.runTests
|
||||||
|
ShellCheck.Analytics.runTests
|
||||||
|
|
||||||
|
To run a specific test:
|
||||||
|
|
||||||
|
:load ShellCheck.Analytics
|
||||||
|
prop_checkUuoc3
|
||||||
|
|
||||||
|
If you make code changes, reload in seconds at any time with:
|
||||||
|
|
||||||
|
:r
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
Crash course in printf debugging in Haskell:
|
||||||
|
|
||||||
|
import Debug.Trace
|
||||||
|
|
||||||
|
greet 0 = return ()
|
||||||
|
-- Print when a function is invoked
|
||||||
|
greet n | trace ("calling greet " ++ show n) False = undefined
|
||||||
|
greet n = do
|
||||||
|
putStrLn "Enter name"
|
||||||
|
name <- getLine
|
||||||
|
-- Print at some point in any monadic function
|
||||||
|
traceM $ "user entered " ++ name
|
||||||
|
putStrLn $ "Hello " ++ name
|
||||||
|
-- Print a value before passing it on
|
||||||
|
greet $ traceShowId (n - 1)
|
||||||
|
|
||||||
|
|
||||||
|
===========================================================================
|
||||||
|
|
||||||
|
If you want to invoke `ghci` directly, such as on `shellcheck.hs`, to
|
||||||
|
debug all of ShellCheck including I/O, you may see an error like this:
|
||||||
|
|
||||||
|
src/ShellCheck/Data.hs:5:1: error:
|
||||||
|
Could not load module ‘Paths_ShellCheck’
|
||||||
|
it is a hidden module in the package ‘ShellCheck-0.8.0’
|
||||||
|
|
||||||
|
This can easily be circumvented by running `./setgitversion` or manually
|
||||||
|
editing src/ShellCheck/Data.hs to replace the auto-deduced version number
|
||||||
|
with a constant string as indicated.
|
||||||
|
|
||||||
|
Afterwards, you can run the ShellCheck tool, as if from the shell, with:
|
||||||
|
|
||||||
|
$ ghci shellcheck.hs
|
||||||
|
ghci> runMain ["-x", "file.sh"]
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
module ShellCheck.Debug () where
|
||||||
|
|
||||||
|
import ShellCheck.Analyzer
|
||||||
|
import ShellCheck.AST
|
||||||
|
import ShellCheck.CFG
|
||||||
|
import ShellCheck.Checker
|
||||||
|
import ShellCheck.CFGAnalysis as CF
|
||||||
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Parser
|
||||||
|
import ShellCheck.Prelude
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import Control.Monad.Identity
|
||||||
|
import Control.Monad.RWS
|
||||||
|
import Control.Monad.Writer
|
||||||
|
import Data.Graph.Inductive.Graph as G
|
||||||
|
import Data.List
|
||||||
|
import Data.Maybe
|
||||||
|
import qualified Data.Map as M
|
||||||
|
import qualified Data.Set as S
|
||||||
|
|
||||||
|
|
||||||
|
-- Run all of ShellCheck (minus output formatters)
|
||||||
|
shellcheckString :: String -> CheckResult
|
||||||
|
shellcheckString scriptString =
|
||||||
|
runIdentity $ checkScript dummySystemInterface checkSpec
|
||||||
|
where
|
||||||
|
checkSpec :: CheckSpec
|
||||||
|
checkSpec = emptyCheckSpec {
|
||||||
|
csScript = scriptString
|
||||||
|
}
|
||||||
|
|
||||||
|
dummySystemInterface :: SystemInterface Identity
|
||||||
|
dummySystemInterface = mockedSystemInterface [
|
||||||
|
-- A tiny, fake filesystem for sourced files
|
||||||
|
("lib/mylib1.sh", "foo=$(cat $1 | wc -l)"),
|
||||||
|
("lib/mylib2.sh", "bar=42")
|
||||||
|
]
|
||||||
|
|
||||||
|
-- Parameters used when generating Control Flow Graphs
|
||||||
|
cfgParams :: CFGParameters
|
||||||
|
cfgParams = CFGParameters {
|
||||||
|
cfLastpipe = False,
|
||||||
|
cfPipefail = False
|
||||||
|
}
|
||||||
|
|
||||||
|
-- An example script to play with
|
||||||
|
exampleScript :: String
|
||||||
|
exampleScript = unlines [
|
||||||
|
"#!/bin/sh",
|
||||||
|
"count=0",
|
||||||
|
"for file in *",
|
||||||
|
"do",
|
||||||
|
" (( count++ ))",
|
||||||
|
"done",
|
||||||
|
"echo $count"
|
||||||
|
]
|
||||||
|
|
||||||
|
-- Parse the script string into ShellCheck's ParseResult
|
||||||
|
parseScriptString :: String -> ParseResult
|
||||||
|
parseScriptString scriptString =
|
||||||
|
runIdentity $ parseScript dummySystemInterface parseSpec
|
||||||
|
where
|
||||||
|
parseSpec :: ParseSpec
|
||||||
|
parseSpec = newParseSpec {
|
||||||
|
psFilename = "myscript",
|
||||||
|
psScript = scriptString
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
-- Parse the script string into an Abstract Syntax Tree
|
||||||
|
stringToAst :: String -> Token
|
||||||
|
stringToAst scriptString =
|
||||||
|
case maybeRoot of
|
||||||
|
Just root -> root
|
||||||
|
Nothing -> error $ "Script failed to parse: " ++ show parserWarnings
|
||||||
|
where
|
||||||
|
parseResult :: ParseResult
|
||||||
|
parseResult = parseScriptString scriptString
|
||||||
|
|
||||||
|
maybeRoot :: Maybe Token
|
||||||
|
maybeRoot = prRoot parseResult
|
||||||
|
|
||||||
|
parserWarnings :: [PositionedComment]
|
||||||
|
parserWarnings = prComments parseResult
|
||||||
|
|
||||||
|
|
||||||
|
astToCfgResult :: Token -> CFGResult
|
||||||
|
astToCfgResult = buildGraph cfgParams
|
||||||
|
|
||||||
|
astToDfa :: Token -> CFGAnalysis
|
||||||
|
astToDfa = analyzeControlFlow cfgParams
|
||||||
|
|
||||||
|
astToCfg :: Token -> CFGraph
|
||||||
|
astToCfg = cfGraph . astToCfgResult
|
||||||
|
|
||||||
|
stringToCfg :: String -> CFGraph
|
||||||
|
stringToCfg = astToCfg . stringToAst
|
||||||
|
|
||||||
|
stringToDfa :: String -> CFGAnalysis
|
||||||
|
stringToDfa = astToDfa . stringToAst
|
||||||
|
|
||||||
|
cfgToGraphViz :: CFGraph -> String
|
||||||
|
cfgToGraphViz = cfgToGraphVizWith show
|
||||||
|
|
||||||
|
stringToCfgViz :: String -> String
|
||||||
|
stringToCfgViz = cfgToGraphViz . stringToCfg
|
||||||
|
|
||||||
|
stringToDfaViz :: String -> String
|
||||||
|
stringToDfaViz = dfaToGraphViz . stringToDfa
|
||||||
|
|
||||||
|
-- Dump a Control Flow Graph as GraphViz with extended information
|
||||||
|
stringToDetailedCfgViz :: String -> String
|
||||||
|
stringToDetailedCfgViz scriptString = cfgToGraphVizWith nodeLabel graph
|
||||||
|
where
|
||||||
|
ast :: Token
|
||||||
|
ast = stringToAst scriptString
|
||||||
|
|
||||||
|
cfgResult :: CFGResult
|
||||||
|
cfgResult = astToCfgResult ast
|
||||||
|
|
||||||
|
graph :: CFGraph
|
||||||
|
graph = cfGraph cfgResult
|
||||||
|
|
||||||
|
idToToken :: M.Map Id Token
|
||||||
|
idToToken = M.fromList $ execWriter $ doAnalysis (\c -> tell [(getId c, c)]) ast
|
||||||
|
|
||||||
|
idToNode :: M.Map Id (Node, Node)
|
||||||
|
idToNode = cfIdToRange cfgResult
|
||||||
|
|
||||||
|
nodeToStartIds :: M.Map Node (S.Set Id)
|
||||||
|
nodeToStartIds =
|
||||||
|
M.fromListWith S.union $
|
||||||
|
map (\(id, (start, _)) -> (start, S.singleton id)) $
|
||||||
|
M.toList idToNode
|
||||||
|
|
||||||
|
nodeToEndIds :: M.Map Node (S.Set Id)
|
||||||
|
nodeToEndIds =
|
||||||
|
M.fromListWith S.union $
|
||||||
|
map (\(id, (_, end)) -> (end, S.singleton id)) $
|
||||||
|
M.toList idToNode
|
||||||
|
|
||||||
|
formatId :: Id -> String
|
||||||
|
formatId id = fromMaybe ("Unknown " ++ show id) $ do
|
||||||
|
(OuterToken _ token) <- M.lookup id idToToken
|
||||||
|
firstWord <- words (show token) !!! 0
|
||||||
|
-- Strip off "Inner_"
|
||||||
|
(_ : tokenName) <- return $ dropWhile (/= '_') firstWord
|
||||||
|
return $ tokenName ++ " " ++ show id
|
||||||
|
|
||||||
|
formatGroup :: S.Set Id -> String
|
||||||
|
formatGroup set = intercalate ", " $ map formatId $ S.toList set
|
||||||
|
|
||||||
|
nodeLabel (node, label) = unlines [
|
||||||
|
show node ++ ". " ++ show label,
|
||||||
|
"Begin: " ++ formatGroup (M.findWithDefault S.empty node nodeToStartIds),
|
||||||
|
"End: " ++ formatGroup (M.findWithDefault S.empty node nodeToEndIds)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
-- Dump a Control Flow Graph with Data Flow Analysis as GraphViz
|
||||||
|
dfaToGraphViz :: CF.CFGAnalysis -> String
|
||||||
|
dfaToGraphViz analysis = cfgToGraphVizWith label $ CF.graph analysis
|
||||||
|
where
|
||||||
|
label (node, label) =
|
||||||
|
let
|
||||||
|
desc = show node ++ ". " ++ show label
|
||||||
|
in
|
||||||
|
fromMaybe ("No DFA available\n\n" ++ desc) $ do
|
||||||
|
(pre, post) <- M.lookup node $ CF.nodeToData analysis
|
||||||
|
return $ unlines [
|
||||||
|
"Precondition: " ++ show pre,
|
||||||
|
"",
|
||||||
|
desc,
|
||||||
|
"",
|
||||||
|
"Postcondition: " ++ show post
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
-- Dump an Control Flow Graph to GraphViz with a given node formatter
|
||||||
|
cfgToGraphVizWith :: (LNode CFNode -> String) -> CFGraph -> String
|
||||||
|
cfgToGraphVizWith nodeLabel graph = concat [
|
||||||
|
"digraph {\n",
|
||||||
|
concatMap dumpNode (labNodes graph),
|
||||||
|
concatMap dumpLink (labEdges graph),
|
||||||
|
tagVizEntries graph,
|
||||||
|
"}\n"
|
||||||
|
]
|
||||||
|
where
|
||||||
|
dumpNode l@(node, label) = show node ++ " [label=" ++ quoteViz (nodeLabel l) ++ "]\n"
|
||||||
|
dumpLink (from, to, typ) = show from ++ " -> " ++ show to ++ " [style=" ++ quoteViz (edgeStyle typ) ++ "]\n"
|
||||||
|
edgeStyle CFEFlow = "solid"
|
||||||
|
edgeStyle CFEExit = "bold"
|
||||||
|
edgeStyle CFEFalseFlow = "dotted"
|
||||||
|
|
||||||
|
quoteViz str = "\"" ++ escapeViz str ++ "\""
|
||||||
|
escapeViz [] = []
|
||||||
|
escapeViz (c:rest) =
|
||||||
|
case c of
|
||||||
|
'\"' -> '\\' : '\"' : escapeViz rest
|
||||||
|
'\n' -> '\\' : 'l' : escapeViz rest
|
||||||
|
'\\' -> '\\' : '\\' : escapeViz rest
|
||||||
|
_ -> c : escapeViz rest
|
||||||
|
|
||||||
|
|
||||||
|
-- Dump an Abstract Syntax Tree (or branch thereof) to GraphViz format
|
||||||
|
astToGraphViz :: Token -> String
|
||||||
|
astToGraphViz token = concat [
|
||||||
|
"digraph {\n",
|
||||||
|
formatTree token,
|
||||||
|
"}\n"
|
||||||
|
]
|
||||||
|
where
|
||||||
|
formatTree :: Token -> String
|
||||||
|
formatTree t = snd $ execRWS (doStackAnalysis push pop t) () []
|
||||||
|
|
||||||
|
push :: Token -> RWS () String [Int] ()
|
||||||
|
push (OuterToken (Id n) inner) = do
|
||||||
|
stack <- get
|
||||||
|
put (n : stack)
|
||||||
|
case stack of
|
||||||
|
[] -> return ()
|
||||||
|
(top:_) -> tell $ show top ++ " -> " ++ show n ++ "\n"
|
||||||
|
tell $ show n ++ " [label=" ++ quoteViz (show n ++ ": " ++ take 32 (show inner)) ++ "]\n"
|
||||||
|
|
||||||
|
pop :: Token -> RWS () String [Int] ()
|
||||||
|
pop _ = modify tail
|
||||||
|
|
||||||
|
|
||||||
|
-- For each entry point, set the rank so that they'll align in the graph
|
||||||
|
tagVizEntries :: CFGraph -> String
|
||||||
|
tagVizEntries graph = "{ rank=same " ++ rank ++ " }"
|
||||||
|
where
|
||||||
|
entries = mapMaybe find $ labNodes graph
|
||||||
|
find (node, CFEntryPoint name) = return (node, name)
|
||||||
|
find _ = Nothing
|
||||||
|
rank = unwords $ map (\(c, _) -> show c) entries
|
|
@ -22,6 +22,8 @@
|
||||||
module ShellCheck.Fixer (applyFix, removeTabStops, mapPositions, Ranged(..), runTests) where
|
module ShellCheck.Fixer (applyFix, removeTabStops, mapPositions, Ranged(..), runTests) where
|
||||||
|
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Prelude
|
||||||
|
import Control.Monad
|
||||||
import Control.Monad.State
|
import Control.Monad.State
|
||||||
import Data.Array
|
import Data.Array
|
||||||
import Data.List
|
import Data.List
|
||||||
|
@ -35,7 +37,7 @@ class Ranged a where
|
||||||
end :: a -> Position
|
end :: a -> Position
|
||||||
overlap :: a -> a -> Bool
|
overlap :: a -> a -> Bool
|
||||||
overlap x y =
|
overlap x y =
|
||||||
(yStart >= xStart && yStart < xEnd) || (yStart < xStart && yEnd > xStart)
|
xEnd > yStart && yEnd > xStart
|
||||||
where
|
where
|
||||||
yStart = start y
|
yStart = start y
|
||||||
yEnd = end y
|
yEnd = end y
|
||||||
|
@ -86,6 +88,7 @@ instance Ranged Replacement where
|
||||||
instance Monoid Fix where
|
instance Monoid Fix where
|
||||||
mempty = newFix
|
mempty = newFix
|
||||||
mappend = (<>)
|
mappend = (<>)
|
||||||
|
mconcat = foldl mappend mempty -- fold left to right since <> discards right on overlap
|
||||||
|
|
||||||
instance Semigroup Fix where
|
instance Semigroup Fix where
|
||||||
f1 <> f2 =
|
f1 <> f2 =
|
||||||
|
@ -228,7 +231,7 @@ applyReplacement2 rep string = do
|
||||||
|
|
||||||
let (l1, l2) = tmap posLine originalPos in
|
let (l1, l2) = tmap posLine originalPos in
|
||||||
when (l1 /= 1 || l2 /= 1) $
|
when (l1 /= 1 || l2 /= 1) $
|
||||||
error "ShellCheck internal error, please report: bad cross-line fix"
|
error $ pleaseReport "bad cross-line fix"
|
||||||
|
|
||||||
let replacer = repString rep
|
let replacer = repString rep
|
||||||
let shift = (length replacer) - (oldEnd - oldStart)
|
let shift = (length replacer) - (oldEnd - oldStart)
|
||||||
|
@ -273,10 +276,10 @@ getPrefixSum = f 0
|
||||||
where
|
where
|
||||||
f sum _ PSLeaf = sum
|
f sum _ PSLeaf = sum
|
||||||
f sum target (PSBranch pivot left right cumulative) =
|
f sum target (PSBranch pivot left right cumulative) =
|
||||||
case () of
|
case target `compare` pivot of
|
||||||
_ | target < pivot -> f sum target left
|
LT -> f sum target left
|
||||||
_ | target > pivot -> f (sum+cumulative) target right
|
GT -> f (sum+cumulative) target right
|
||||||
_ -> sum+cumulative
|
EQ -> sum+cumulative
|
||||||
|
|
||||||
-- Add a value to the Prefix Sum tree at the given index.
|
-- Add a value to the Prefix Sum tree at the given index.
|
||||||
-- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5
|
-- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5
|
||||||
|
@ -285,10 +288,10 @@ addPSValue key value tree = if value == 0 then tree else f tree
|
||||||
where
|
where
|
||||||
f PSLeaf = PSBranch key PSLeaf PSLeaf value
|
f PSLeaf = PSBranch key PSLeaf PSLeaf value
|
||||||
f (PSBranch pivot left right sum) =
|
f (PSBranch pivot left right sum) =
|
||||||
case () of
|
case key `compare` pivot of
|
||||||
_ | key < pivot -> PSBranch pivot (f left) right (sum + value)
|
LT -> PSBranch pivot (f left) right (sum + value)
|
||||||
_ | key > pivot -> PSBranch pivot left (f right) sum
|
GT -> PSBranch pivot left (f right) sum
|
||||||
_ -> PSBranch pivot left right (sum + value)
|
EQ -> PSBranch pivot left right (sum + value)
|
||||||
|
|
||||||
prop_pstreeSumsCorrectly kvs targets =
|
prop_pstreeSumsCorrectly kvs targets =
|
||||||
let
|
let
|
||||||
|
|
|
@ -24,8 +24,8 @@ import ShellCheck.Formatter.Format
|
||||||
|
|
||||||
import Data.Char
|
import Data.Char
|
||||||
import Data.List
|
import Data.List
|
||||||
import GHC.Exts
|
|
||||||
import System.IO
|
import System.IO
|
||||||
|
import qualified Data.List.NonEmpty as NE
|
||||||
|
|
||||||
format :: IO Formatter
|
format :: IO Formatter
|
||||||
format = return Formatter {
|
format = return Formatter {
|
||||||
|
@ -45,12 +45,12 @@ outputResults cr sys =
|
||||||
else mapM_ outputGroup fileGroups
|
else mapM_ outputGroup fileGroups
|
||||||
where
|
where
|
||||||
comments = crComments cr
|
comments = crComments cr
|
||||||
fileGroups = groupWith sourceFile comments
|
fileGroups = NE.groupWith sourceFile comments
|
||||||
outputGroup group = do
|
outputGroup group = do
|
||||||
let filename = sourceFile (head group)
|
let filename = sourceFile (NE.head group)
|
||||||
result <- (siReadFile sys) filename
|
result <- siReadFile sys (Just True) filename
|
||||||
let contents = either (const "") id result
|
let contents = either (const "") id result
|
||||||
outputFile filename contents group
|
outputFile filename contents (NE.toList group)
|
||||||
|
|
||||||
outputFile filename contents warnings = do
|
outputFile filename contents warnings = do
|
||||||
let comments = makeNonVirtual warnings contents
|
let comments = makeNonVirtual warnings contents
|
||||||
|
|
|
@ -38,9 +38,6 @@ import System.FilePath
|
||||||
|
|
||||||
import Test.QuickCheck
|
import Test.QuickCheck
|
||||||
|
|
||||||
import Debug.Trace
|
|
||||||
ltt x = trace (show x) x
|
|
||||||
|
|
||||||
format :: FormatterOptions -> IO Formatter
|
format :: FormatterOptions -> IO Formatter
|
||||||
format options = do
|
format options = do
|
||||||
foundIssues <- newIORef False
|
foundIssues <- newIORef False
|
||||||
|
@ -90,7 +87,7 @@ reportResult foundIssues reportedIssues color result sys = do
|
||||||
mapM_ output $ M.toList fixmap
|
mapM_ output $ M.toList fixmap
|
||||||
where
|
where
|
||||||
output (name, fix) = do
|
output (name, fix) = do
|
||||||
file <- (siReadFile sys) name
|
file <- siReadFile sys (Just True) name
|
||||||
case file of
|
case file of
|
||||||
Right contents -> do
|
Right contents -> do
|
||||||
putStrLn $ formatDoc color $ makeDiff name contents fix
|
putStrLn $ formatDoc color $ makeDiff name contents fix
|
||||||
|
@ -206,10 +203,9 @@ formatDoc color (DiffDoc name lf regions) =
|
||||||
buildFixMap :: [Fix] -> M.Map String Fix
|
buildFixMap :: [Fix] -> M.Map String Fix
|
||||||
buildFixMap fixes = perFile
|
buildFixMap fixes = perFile
|
||||||
where
|
where
|
||||||
splitFixes = concatMap splitFixByFile fixes
|
splitFixes = splitFixByFile $ mconcat fixes
|
||||||
perFile = groupByMap (posFile . repStartPos . head . fixReplacements) splitFixes
|
perFile = groupByMap (posFile . repStartPos . head . fixReplacements) splitFixes
|
||||||
|
|
||||||
-- There are currently no multi-file fixes, but let's handle it anyways
|
|
||||||
splitFixByFile :: Fix -> [Fix]
|
splitFixByFile :: Fix -> [Fix]
|
||||||
splitFixByFile fix = map makeFix $ groupBy sameFile (fixReplacements fix)
|
splitFixByFile fix = map makeFix $ groupBy sameFile (fixReplacements fix)
|
||||||
where
|
where
|
||||||
|
|
|
@ -28,6 +28,7 @@ import Data.Array
|
||||||
import Data.List
|
import Data.List
|
||||||
import System.IO
|
import System.IO
|
||||||
import System.Info
|
import System.Info
|
||||||
|
import System.Environment
|
||||||
|
|
||||||
-- A formatter that carries along an arbitrary piece of data
|
-- A formatter that carries along an arbitrary piece of data
|
||||||
data Formatter = Formatter {
|
data Formatter = Formatter {
|
||||||
|
@ -68,12 +69,14 @@ makeNonVirtual comments contents =
|
||||||
|
|
||||||
|
|
||||||
shouldOutputColor :: ColorOption -> IO Bool
|
shouldOutputColor :: ColorOption -> IO Bool
|
||||||
shouldOutputColor colorOption = do
|
shouldOutputColor colorOption =
|
||||||
term <- hIsTerminalDevice stdout
|
case colorOption of
|
||||||
|
ColorAlways -> return True
|
||||||
|
ColorNever -> return False
|
||||||
|
ColorAuto -> do
|
||||||
|
isTerminal <- hIsTerminalDevice stdout
|
||||||
|
term <- lookupEnv "TERM"
|
||||||
let windows = "mingw" `isPrefixOf` os
|
let windows = "mingw" `isPrefixOf` os
|
||||||
let isUsableTty = term && not windows
|
let dumbTerm = term `elem` [Just "dumb", Just "", Nothing]
|
||||||
let useColor = case colorOption of
|
let isUsableTty = isTerminal && not windows && not dumbTerm
|
||||||
ColorAlways -> True
|
return isUsableTty
|
||||||
ColorNever -> False
|
|
||||||
ColorAuto -> isUsableTty
|
|
||||||
return useColor
|
|
||||||
|
|
|
@ -23,8 +23,8 @@ import ShellCheck.Interface
|
||||||
import ShellCheck.Formatter.Format
|
import ShellCheck.Formatter.Format
|
||||||
|
|
||||||
import Data.List
|
import Data.List
|
||||||
import GHC.Exts
|
|
||||||
import System.IO
|
import System.IO
|
||||||
|
import qualified Data.List.NonEmpty as NE
|
||||||
|
|
||||||
format :: IO Formatter
|
format :: IO Formatter
|
||||||
format = return Formatter {
|
format = return Formatter {
|
||||||
|
@ -39,13 +39,13 @@ outputError file error = hPutStrLn stderr $ file ++ ": " ++ error
|
||||||
outputAll cr sys = mapM_ f groups
|
outputAll cr sys = mapM_ f groups
|
||||||
where
|
where
|
||||||
comments = crComments cr
|
comments = crComments cr
|
||||||
groups = groupWith sourceFile comments
|
groups = NE.groupWith sourceFile comments
|
||||||
f :: [PositionedComment] -> IO ()
|
f :: NE.NonEmpty PositionedComment -> IO ()
|
||||||
f group = do
|
f group = do
|
||||||
let filename = sourceFile (head group)
|
let filename = sourceFile (NE.head group)
|
||||||
result <- (siReadFile sys) filename
|
result <- siReadFile sys (Just True) filename
|
||||||
let contents = either (const "") id result
|
let contents = either (const "") id result
|
||||||
outputResult filename contents group
|
outputResult filename contents (NE.toList group)
|
||||||
|
|
||||||
outputResult filename contents warnings = do
|
outputResult filename contents warnings = do
|
||||||
let comments = makeNonVirtual warnings contents
|
let comments = makeNonVirtual warnings contents
|
||||||
|
|
|
@ -23,6 +23,7 @@ module ShellCheck.Formatter.JSON (format) where
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Formatter.Format
|
import ShellCheck.Formatter.Format
|
||||||
|
|
||||||
|
import Control.DeepSeq
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import Data.IORef
|
import Data.IORef
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
|
@ -103,7 +104,7 @@ collectResult ref cr sys = mapM_ f groups
|
||||||
comments = crComments cr
|
comments = crComments cr
|
||||||
groups = groupWith sourceFile comments
|
groups = groupWith sourceFile comments
|
||||||
f :: [PositionedComment] -> IO ()
|
f :: [PositionedComment] -> IO ()
|
||||||
f group = modifyIORef ref (\x -> comments ++ x)
|
f group = deepseq comments $ modifyIORef ref (\x -> comments ++ x)
|
||||||
|
|
||||||
finish ref = do
|
finish ref = do
|
||||||
list <- readIORef ref
|
list <- readIORef ref
|
||||||
|
|
|
@ -23,12 +23,13 @@ module ShellCheck.Formatter.JSON1 (format) where
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Formatter.Format
|
import ShellCheck.Formatter.Format
|
||||||
|
|
||||||
|
import Control.DeepSeq
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import Data.IORef
|
import Data.IORef
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
import GHC.Exts
|
|
||||||
import System.IO
|
import System.IO
|
||||||
import qualified Data.ByteString.Lazy.Char8 as BL
|
import qualified Data.ByteString.Lazy.Char8 as BL
|
||||||
|
import qualified Data.List.NonEmpty as NE
|
||||||
|
|
||||||
format :: IO Formatter
|
format :: IO Formatter
|
||||||
format = do
|
format = do
|
||||||
|
@ -113,14 +114,14 @@ outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
|
||||||
collectResult ref cr sys = mapM_ f groups
|
collectResult ref cr sys = mapM_ f groups
|
||||||
where
|
where
|
||||||
comments = crComments cr
|
comments = crComments cr
|
||||||
groups = groupWith sourceFile comments
|
groups = NE.groupWith sourceFile comments
|
||||||
f :: [PositionedComment] -> IO ()
|
f :: NE.NonEmpty PositionedComment -> IO ()
|
||||||
f group = do
|
f group = do
|
||||||
let filename = sourceFile (head group)
|
let filename = sourceFile (NE.head group)
|
||||||
result <- siReadFile sys filename
|
result <- siReadFile sys (Just True) filename
|
||||||
let contents = either (const "") id result
|
let contents = either (const "") id result
|
||||||
let comments' = makeNonVirtual comments contents
|
let comments' = makeNonVirtual comments contents
|
||||||
modifyIORef ref (\x -> comments' ++ x)
|
deepseq comments' $ modifyIORef ref (\x -> comments' ++ x)
|
||||||
|
|
||||||
finish ref = do
|
finish ref = do
|
||||||
list <- readIORef ref
|
list <- readIORef ref
|
||||||
|
|
|
@ -23,6 +23,7 @@ import ShellCheck.Fixer
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Formatter.Format
|
import ShellCheck.Formatter.Format
|
||||||
|
|
||||||
|
import Control.DeepSeq
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Data.Array
|
import Data.Array
|
||||||
import Data.Foldable
|
import Data.Foldable
|
||||||
|
@ -30,9 +31,9 @@ import Data.Ord
|
||||||
import Data.IORef
|
import Data.IORef
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import GHC.Exts
|
|
||||||
import System.IO
|
import System.IO
|
||||||
import System.Info
|
import System.Info
|
||||||
|
import qualified Data.List.NonEmpty as NE
|
||||||
|
|
||||||
wikiLink = "https://www.shellcheck.net/wiki/"
|
wikiLink = "https://www.shellcheck.net/wiki/"
|
||||||
|
|
||||||
|
@ -88,7 +89,7 @@ rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err)
|
||||||
appendComments errRef comments max = do
|
appendComments errRef comments max = do
|
||||||
previous <- readIORef errRef
|
previous <- readIORef errRef
|
||||||
let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments
|
let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments
|
||||||
writeIORef errRef . take max . nubBy equal . sort $ previous ++ current
|
writeIORef errRef $! force . take max . nubBy equal . sort $ previous ++ current
|
||||||
where
|
where
|
||||||
fst3 (x,_,_) = x
|
fst3 (x,_,_) = x
|
||||||
equal x y = fst3 x == fst3 y
|
equal x y = fst3 x == fst3 y
|
||||||
|
@ -116,19 +117,19 @@ outputResult options ref result sys = do
|
||||||
color <- getColorFunc $ foColorOption options
|
color <- getColorFunc $ foColorOption options
|
||||||
let comments = crComments result
|
let comments = crComments result
|
||||||
appendComments ref comments (fromIntegral $ foWikiLinkCount options)
|
appendComments ref comments (fromIntegral $ foWikiLinkCount options)
|
||||||
let fileGroups = groupWith sourceFile comments
|
let fileGroups = NE.groupWith sourceFile comments
|
||||||
mapM_ (outputForFile color sys) fileGroups
|
mapM_ (outputForFile color sys) fileGroups
|
||||||
|
|
||||||
outputForFile color sys comments = do
|
outputForFile color sys comments = do
|
||||||
let fileName = sourceFile (head comments)
|
let fileName = sourceFile (NE.head comments)
|
||||||
result <- (siReadFile sys) fileName
|
result <- siReadFile sys (Just True) fileName
|
||||||
let contents = either (const "") id result
|
let contents = either (const "") id result
|
||||||
let fileLinesList = lines contents
|
let fileLinesList = lines contents
|
||||||
let lineCount = length fileLinesList
|
let lineCount = length fileLinesList
|
||||||
let fileLines = listArray (1, lineCount) fileLinesList
|
let fileLines = listArray (1, lineCount) fileLinesList
|
||||||
let groups = groupWith lineNo comments
|
let groups = NE.groupWith lineNo comments
|
||||||
mapM_ (\commentsForLine -> do
|
forM_ groups $ \commentsForLine -> do
|
||||||
let lineNum = fromIntegral $ lineNo (head commentsForLine)
|
let lineNum = fromIntegral $ lineNo (NE.head commentsForLine)
|
||||||
let line = if lineNum < 1 || lineNum > lineCount
|
let line = if lineNum < 1 || lineNum > lineCount
|
||||||
then ""
|
then ""
|
||||||
else fileLines ! fromIntegral lineNum
|
else fileLines ! fromIntegral lineNum
|
||||||
|
@ -136,10 +137,9 @@ outputForFile color sys comments = do
|
||||||
putStrLn $ color "message" $
|
putStrLn $ color "message" $
|
||||||
"In " ++ fileName ++" line " ++ show lineNum ++ ":"
|
"In " ++ fileName ++" line " ++ show lineNum ++ ":"
|
||||||
putStrLn (color "source" line)
|
putStrLn (color "source" line)
|
||||||
mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) commentsForLine
|
forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c
|
||||||
putStrLn ""
|
putStrLn ""
|
||||||
showFixedString color commentsForLine (fromIntegral lineNum) fileLines
|
showFixedString color (toList commentsForLine) (fromIntegral lineNum) fileLines
|
||||||
) groups
|
|
||||||
|
|
||||||
-- Pick out only the lines necessary to show a fix in action
|
-- Pick out only the lines necessary to show a fix in action
|
||||||
sliceFile :: Fix -> Array Int String -> (Fix, Array Int String)
|
sliceFile :: Fix -> Array Int String -> (Fix, Array Int String)
|
||||||
|
@ -175,7 +175,7 @@ showFixedString color comments lineNum fileLines =
|
||||||
cuteIndent :: PositionedComment -> String
|
cuteIndent :: PositionedComment -> String
|
||||||
cuteIndent comment =
|
cuteIndent comment =
|
||||||
replicate (fromIntegral $ colNo comment - 1) ' ' ++
|
replicate (fromIntegral $ colNo comment - 1) ' ' ++
|
||||||
makeArrow ++ " " ++ code (codeNo comment) ++ ": " ++ messageText comment
|
makeArrow ++ " " ++ code (codeNo comment) ++ " (" ++ severityText comment ++ "): " ++ messageText comment
|
||||||
where
|
where
|
||||||
arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^"
|
arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^"
|
||||||
makeArrow =
|
makeArrow =
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{-
|
{-
|
||||||
Copyright 2012-2019 Vidar Holen
|
Copyright 2012-2024 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
https://www.shellcheck.net
|
https://www.shellcheck.net
|
||||||
|
@ -21,14 +21,14 @@
|
||||||
module ShellCheck.Interface
|
module ShellCheck.Interface
|
||||||
(
|
(
|
||||||
SystemInterface(..)
|
SystemInterface(..)
|
||||||
, CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csOptionalChecks)
|
, CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csExtendedAnalysis, csOptionalChecks)
|
||||||
, CheckResult(crFilename, crComments)
|
, CheckResult(crFilename, crComments)
|
||||||
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
|
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
|
||||||
, ParseResult(prComments, prTokenPositions, prRoot)
|
, ParseResult(prComments, prTokenPositions, prRoot)
|
||||||
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asOptionalChecks)
|
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asExtendedAnalysis, asOptionalChecks)
|
||||||
, AnalysisResult(arComments)
|
, AnalysisResult(arComments)
|
||||||
, FormatterOptions(foColorOption, foWikiLinkCount)
|
, FormatterOptions(foColorOption, foWikiLinkCount)
|
||||||
, Shell(Ksh, Sh, Bash, Dash)
|
, Shell(Ksh, Sh, Bash, Dash, BusyboxSh)
|
||||||
, ExecutionMode(Executed, Sourced)
|
, ExecutionMode(Executed, Sourced)
|
||||||
, ErrorMessage
|
, ErrorMessage
|
||||||
, Code
|
, Code
|
||||||
|
@ -39,11 +39,12 @@ module ShellCheck.Interface
|
||||||
, ColorOption(ColorAuto, ColorAlways, ColorNever)
|
, ColorOption(ColorAuto, ColorAlways, ColorNever)
|
||||||
, TokenComment(tcId, tcComment, tcFix)
|
, TokenComment(tcId, tcComment, tcFix)
|
||||||
, emptyCheckResult
|
, emptyCheckResult
|
||||||
, newParseResult
|
|
||||||
, newAnalysisSpec
|
|
||||||
, newAnalysisResult
|
, newAnalysisResult
|
||||||
|
, newAnalysisSpec
|
||||||
, newFormatterOptions
|
, newFormatterOptions
|
||||||
|
, newParseResult
|
||||||
, newPosition
|
, newPosition
|
||||||
|
, newSystemInterface
|
||||||
, newTokenComment
|
, newTokenComment
|
||||||
, mockedSystemInterface
|
, mockedSystemInterface
|
||||||
, mockRcFile
|
, mockRcFile
|
||||||
|
@ -73,15 +74,19 @@ import qualified Data.Map as Map
|
||||||
|
|
||||||
|
|
||||||
data SystemInterface m = SystemInterface {
|
data SystemInterface m = SystemInterface {
|
||||||
-- Read a file by filename, or return an error
|
-- | Given:
|
||||||
siReadFile :: String -> m (Either ErrorMessage String),
|
-- What annotations say about including external files (if anything)
|
||||||
-- Given:
|
-- A resolved filename from siFindSource
|
||||||
|
-- Read the file or return an error
|
||||||
|
siReadFile :: Maybe Bool -> String -> m (Either ErrorMessage String),
|
||||||
|
-- | Given:
|
||||||
-- the current script,
|
-- the current script,
|
||||||
|
-- what annotations say about including external files (if anything)
|
||||||
-- a list of source-path annotations in effect,
|
-- a list of source-path annotations in effect,
|
||||||
-- and a sourced file,
|
-- and a sourced file,
|
||||||
-- find the sourced file
|
-- find the sourced file
|
||||||
siFindSource :: String -> [String] -> String -> m FilePath,
|
siFindSource :: String -> Maybe Bool -> [String] -> String -> m FilePath,
|
||||||
-- Get the configuration file (name, contents) for a filename
|
-- | Get the configuration file (name, contents) for a filename
|
||||||
siGetConfig :: String -> m (Maybe (FilePath, String))
|
siGetConfig :: String -> m (Maybe (FilePath, String))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,6 +100,7 @@ data CheckSpec = CheckSpec {
|
||||||
csIncludedWarnings :: Maybe [Integer],
|
csIncludedWarnings :: Maybe [Integer],
|
||||||
csShellTypeOverride :: Maybe Shell,
|
csShellTypeOverride :: Maybe Shell,
|
||||||
csMinSeverity :: Severity,
|
csMinSeverity :: Severity,
|
||||||
|
csExtendedAnalysis :: Maybe Bool,
|
||||||
csOptionalChecks :: [String]
|
csOptionalChecks :: [String]
|
||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
@ -119,6 +125,7 @@ emptyCheckSpec = CheckSpec {
|
||||||
csIncludedWarnings = Nothing,
|
csIncludedWarnings = Nothing,
|
||||||
csShellTypeOverride = Nothing,
|
csShellTypeOverride = Nothing,
|
||||||
csMinSeverity = StyleC,
|
csMinSeverity = StyleC,
|
||||||
|
csExtendedAnalysis = Nothing,
|
||||||
csOptionalChecks = []
|
csOptionalChecks = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,6 +138,14 @@ newParseSpec = ParseSpec {
|
||||||
psShellTypeOverride = Nothing
|
psShellTypeOverride = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newSystemInterface :: Monad m => SystemInterface m
|
||||||
|
newSystemInterface =
|
||||||
|
SystemInterface {
|
||||||
|
siReadFile = \_ _ -> return $ Left "Not implemented",
|
||||||
|
siFindSource = \_ _ _ name -> return name,
|
||||||
|
siGetConfig = \_ -> return Nothing
|
||||||
|
}
|
||||||
|
|
||||||
-- Parser input and output
|
-- Parser input and output
|
||||||
data ParseSpec = ParseSpec {
|
data ParseSpec = ParseSpec {
|
||||||
psFilename :: String,
|
psFilename :: String,
|
||||||
|
@ -161,6 +176,7 @@ data AnalysisSpec = AnalysisSpec {
|
||||||
asExecutionMode :: ExecutionMode,
|
asExecutionMode :: ExecutionMode,
|
||||||
asCheckSourced :: Bool,
|
asCheckSourced :: Bool,
|
||||||
asOptionalChecks :: [String],
|
asOptionalChecks :: [String],
|
||||||
|
asExtendedAnalysis :: Maybe Bool,
|
||||||
asTokenPositions :: Map.Map Id (Position, Position)
|
asTokenPositions :: Map.Map Id (Position, Position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,6 +187,7 @@ newAnalysisSpec token = AnalysisSpec {
|
||||||
asExecutionMode = Executed,
|
asExecutionMode = Executed,
|
||||||
asCheckSourced = False,
|
asCheckSourced = False,
|
||||||
asOptionalChecks = [],
|
asOptionalChecks = [],
|
||||||
|
asExtendedAnalysis = Nothing,
|
||||||
asTokenPositions = Map.empty
|
asTokenPositions = Map.empty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,7 +225,7 @@ newCheckDescription = CheckDescription {
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Supporting data types
|
-- Supporting data types
|
||||||
data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
|
data Shell = Ksh | Sh | Bash | Dash | BusyboxSh deriving (Show, Eq)
|
||||||
data ExecutionMode = Executed | Sourced deriving (Show, Eq)
|
data ExecutionMode = Executed | Sourced deriving (Show, Eq)
|
||||||
|
|
||||||
type ErrorMessage = String
|
type ErrorMessage = String
|
||||||
|
@ -307,19 +324,18 @@ data ColorOption =
|
||||||
|
|
||||||
-- For testing
|
-- For testing
|
||||||
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
|
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
|
||||||
mockedSystemInterface files = SystemInterface {
|
mockedSystemInterface files = (newSystemInterface :: SystemInterface Identity) {
|
||||||
siReadFile = rf,
|
siReadFile = rf,
|
||||||
siFindSource = fs,
|
siFindSource = fs,
|
||||||
siGetConfig = const $ return Nothing
|
siGetConfig = const $ return Nothing
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
rf file = return $
|
rf _ file = return $
|
||||||
case find ((== file) . fst) files of
|
case find ((== file) . fst) files of
|
||||||
Nothing -> Left "File not included in mock."
|
Nothing -> Left "File not included in mock."
|
||||||
Just (_, contents) -> Right contents
|
Just (_, contents) -> Right contents
|
||||||
fs _ _ file = return file
|
fs _ _ _ file = return file
|
||||||
|
|
||||||
mockRcFile rcfile mock = mock {
|
mockRcFile rcfile mock = mock {
|
||||||
siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
|
siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
51
src/ShellCheck/Prelude.hs
Normal file
51
src/ShellCheck/Prelude.hs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
{-
|
||||||
|
Copyright 2022 Vidar Holen
|
||||||
|
|
||||||
|
This file is part of ShellCheck.
|
||||||
|
https://www.shellcheck.net
|
||||||
|
|
||||||
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
ShellCheck is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-}
|
||||||
|
|
||||||
|
-- Generic basic utility functions
|
||||||
|
module ShellCheck.Prelude where
|
||||||
|
|
||||||
|
import Data.Semigroup
|
||||||
|
|
||||||
|
|
||||||
|
-- Get element 0 or a default. Like `head` but safe.
|
||||||
|
headOrDefault _ (a:_) = a
|
||||||
|
headOrDefault def _ = def
|
||||||
|
|
||||||
|
-- Get the last element or a default. Like `last` but safe.
|
||||||
|
lastOrDefault def [] = def
|
||||||
|
lastOrDefault _ list = last list
|
||||||
|
|
||||||
|
--- Get element n of a list, or Nothing. Like `!!` but safe.
|
||||||
|
(!!!) list i =
|
||||||
|
case drop i list of
|
||||||
|
[] -> Nothing
|
||||||
|
(r:_) -> Just r
|
||||||
|
|
||||||
|
|
||||||
|
-- Like mconcat but for Semigroups
|
||||||
|
sconcat1 :: (Semigroup t) => [t] -> t
|
||||||
|
sconcat1 [x] = x
|
||||||
|
sconcat1 (x:xs) = x <> sconcat1 xs
|
||||||
|
|
||||||
|
sconcatOrDefault def [] = def
|
||||||
|
sconcatOrDefault _ list = sconcat1 list
|
||||||
|
|
||||||
|
-- For more actionable "impossible" errors
|
||||||
|
pleaseReport str = "ShellCheck internal error, please report: " ++ str
|
|
@ -2,7 +2,7 @@
|
||||||
# For more information, see: https://docs.haskellstack.org/en/stable/yaml_configuration/
|
# For more information, see: https://docs.haskellstack.org/en/stable/yaml_configuration/
|
||||||
|
|
||||||
# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
|
# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
|
||||||
resolver: lts-13.26
|
resolver: lts-18.15
|
||||||
|
|
||||||
# Local packages, usually specified by relative directory name
|
# Local packages, usually specified by relative directory name
|
||||||
packages:
|
packages:
|
||||||
|
|
|
@ -29,6 +29,7 @@ detestify() {
|
||||||
state = 0;
|
state = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/STRIP/ { next; }
|
||||||
/LANGUAGE TemplateHaskell/ { next; }
|
/LANGUAGE TemplateHaskell/ { next; }
|
||||||
/^import.*Test\./ { next; }
|
/^import.*Test\./ { next; }
|
||||||
|
|
||||||
|
@ -75,4 +76,3 @@ find . -name '.git' -prune -o -type f -name '*.hs' -print |
|
||||||
do
|
do
|
||||||
modify "$file" detestify
|
modify "$file" detestify
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ fi
|
||||||
|
|
||||||
cabal install --dependencies-only --enable-tests "${flags[@]}" ||
|
cabal install --dependencies-only --enable-tests "${flags[@]}" ||
|
||||||
cabal install --dependencies-only "${flags[@]}" ||
|
cabal install --dependencies-only "${flags[@]}" ||
|
||||||
|
cabal install --dependencies-only --max-backjumps -1 "${flags[@]}" ||
|
||||||
die "can't install dependencies"
|
die "can't install dependencies"
|
||||||
cabal configure --enable-tests "${flags[@]}" ||
|
cabal configure --enable-tests "${flags[@]}" ||
|
||||||
die "configure failed"
|
die "configure failed"
|
||||||
|
@ -29,6 +30,8 @@ cabal build ||
|
||||||
die "build failed"
|
die "build failed"
|
||||||
cabal test ||
|
cabal test ||
|
||||||
die "test failed"
|
die "test failed"
|
||||||
|
cabal haddock ||
|
||||||
|
die "haddock failed"
|
||||||
|
|
||||||
sc="$(find . -name shellcheck -type f -perm -111)"
|
sc="$(find . -name shellcheck -type f -perm -111)"
|
||||||
[ -x "$sc" ] || die "Can't find executable"
|
[ -x "$sc" ] || die "Can't find executable"
|
||||||
|
|
|
@ -9,7 +9,18 @@ fail() {
|
||||||
|
|
||||||
if git diff | grep -q ""
|
if git diff | grep -q ""
|
||||||
then
|
then
|
||||||
fail "There are uncommited changes"
|
fail "There are uncommitted changes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
version=${current#v}
|
||||||
|
if ! grep "Version:" ShellCheck.cabal | grep -qFw "$version"
|
||||||
|
then
|
||||||
|
fail "The cabal file does not match tag version $version"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -qF "## $current" CHANGELOG.md
|
||||||
|
then
|
||||||
|
fail "CHANGELOG.md does not contain '## $current'"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
current=$(git tag --points-at)
|
current=$(git tag --points-at)
|
||||||
|
@ -34,41 +45,38 @@ then
|
||||||
fail "You are not on master"
|
fail "You are not on master"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
version=${current#v}
|
|
||||||
if ! grep "Version:" ShellCheck.cabal | grep -qFw "$version"
|
|
||||||
then
|
|
||||||
fail "The cabal file does not match tag version $version"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! grep -qF "## $current" CHANGELOG.md
|
|
||||||
then
|
|
||||||
fail "CHANGELOG.md does not contain '## $current'"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $(git log -1 --pretty=%B) != "Stable version "* ]]
|
if [[ $(git log -1 --pretty=%B) != "Stable version "* ]]
|
||||||
then
|
then
|
||||||
fail "Expected git log message to be 'Stable version ...'"
|
fail "Expected git log message to be 'Stable version ...'"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ $(git log -1 --pretty=%B) != *"CHANGELOG"* ]]
|
||||||
|
then
|
||||||
|
fail "Expected git log message to contain CHANGELOG"
|
||||||
|
fi
|
||||||
|
|
||||||
i=1 j=1
|
i=1 j=1
|
||||||
cat << EOF
|
cat << EOF
|
||||||
|
|
||||||
Manual Checklist
|
Manual Checklist
|
||||||
|
|
||||||
$((i++)). Make sure none of the automated checks above failed
|
$((i++)). Make sure none of the automated checks above failed
|
||||||
$((i++)). Make sure Travis build currently passes: https://travis-ci.org/koalaman/shellcheck
|
$((i++)). Run \`build/build_builder build/*/\` to update all builder images.
|
||||||
$((i++)). Make sure SnapCraft build currently works: https://build.snapcraft.io/user/koalaman
|
$((j++)). \`build/run_builder dist-newstyle/sdist/ShellCheck-*.tar.gz build/*/\` to verify that they work.
|
||||||
|
$((j++)). \`for f in \$(cat build/*/tag); do docker push "\$f"; done\` to upload them.
|
||||||
$((i++)). Run test/distrotest to ensure that most distros can build OOTB.
|
$((i++)). Run test/distrotest to ensure that most distros can build OOTB.
|
||||||
|
$((i++)). Make sure GitHub Build currently passes: https://github.com/koalaman/shellcheck/actions
|
||||||
|
$((i++)). Make sure SnapCraft build currently works: https://build.snapcraft.io/user/koalaman
|
||||||
$((i++)). Format and read over the manual for bad formatting and outdated info.
|
$((i++)). Format and read over the manual for bad formatting and outdated info.
|
||||||
$((i++)). Make sure the Hackage package builds, so that all files are
|
$((i++)). Make sure the Hackage package builds locally.
|
||||||
|
|
||||||
Release Steps
|
Release Steps
|
||||||
|
|
||||||
$((j++)). \`cabal sdist\` to generate a Hackage package
|
$((j++)). \`cabal sdist\` to generate a Hackage package
|
||||||
$((j++)). \`git push --follow-tags\` to push commit
|
$((j++)). \`git push --follow-tags\` to push commit
|
||||||
$((j++)). Wait for Travis to build
|
$((j++)). Wait for GitHub Actions to build.
|
||||||
$((j++)). Verify release:
|
$((j++)). Verify release:
|
||||||
a. Check that the new versions are uploaded: https://shellcheck.storage.googleapis.com/index.html
|
a. Check that the new versions are uploaded: https://github.com/koalaman/shellcheck/tags
|
||||||
b. Check that the docker images have version tags: https://hub.docker.com/u/koalaman
|
b. Check that the docker images have version tags: https://hub.docker.com/u/koalaman
|
||||||
$((j++)). If no disaster, upload to Hackage: http://hackage.haskell.org/upload
|
$((j++)). If no disaster, upload to Hackage: http://hackage.haskell.org/upload
|
||||||
$((j++)). Push a new commit that updates CHANGELOG.md
|
$((j++)). Push a new commit that updates CHANGELOG.md
|
||||||
|
|
|
@ -17,13 +17,20 @@ and is still highly experimental.
|
||||||
Make sure you're plugged in and have screen/tmux in place,
|
Make sure you're plugged in and have screen/tmux in place,
|
||||||
then re-run with $0 --run to continue.
|
then re-run with $0 --run to continue.
|
||||||
|
|
||||||
Also note that dist* will be deleted.
|
Also note that dist*/ and .stack-work/ will be deleted.
|
||||||
EOF
|
EOF
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "Deleting 'dist' and 'dist-newstyle'..."
|
echo "Deleting 'dist', 'dist-newstyle', and '.stack-work'..."
|
||||||
rm -rf dist dist-newstyle
|
rm -rf dist dist-newstyle .stack-work
|
||||||
|
|
||||||
|
execs=$(find . -name shellcheck)
|
||||||
|
|
||||||
|
if [ -n "$execs" ]
|
||||||
|
then
|
||||||
|
die "Found unexpected executables. Remove and try again: $execs"
|
||||||
|
fi
|
||||||
|
|
||||||
log=$(mktemp) || die "Can't create temp file"
|
log=$(mktemp) || die "Can't create temp file"
|
||||||
date >> "$log" || die "Can't write to log"
|
date >> "$log" || die "Can't write to log"
|
||||||
|
@ -63,14 +70,16 @@ debian:testing apt-get update && apt-get install -y cabal-install
|
||||||
ubuntu:latest apt-get update && apt-get install -y cabal-install
|
ubuntu:latest apt-get update && apt-get install -y cabal-install
|
||||||
haskell:latest true
|
haskell:latest true
|
||||||
opensuse/leap:latest zypper install -y cabal-install ghc
|
opensuse/leap:latest zypper install -y cabal-install ghc
|
||||||
fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils
|
fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils libstdc++-static gcc-c++
|
||||||
archlinux/base:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
|
archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
|
||||||
|
|
||||||
# Other versions we want to support
|
# Ubuntu LTS
|
||||||
ubuntu:18.04 apt-get update && apt-get install -y cabal-install
|
ubuntu:24.04 apt-get update && apt-get install -y cabal-install
|
||||||
|
ubuntu:22.04 apt-get update && apt-get install -y cabal-install
|
||||||
|
ubuntu:20.04 apt-get update && apt-get install -y cabal-install
|
||||||
|
|
||||||
# Misc Haskell including current and latest Stack build
|
# Stack on Ubuntu LTS
|
||||||
ubuntu:18.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
|
ubuntu:24.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
exit "$final"
|
exit "$final"
|
||||||
|
|
|
@ -4,8 +4,12 @@ import Control.Monad
|
||||||
import System.Exit
|
import System.Exit
|
||||||
import qualified ShellCheck.Analytics
|
import qualified ShellCheck.Analytics
|
||||||
import qualified ShellCheck.AnalyzerLib
|
import qualified ShellCheck.AnalyzerLib
|
||||||
|
import qualified ShellCheck.ASTLib
|
||||||
|
import qualified ShellCheck.CFG
|
||||||
|
import qualified ShellCheck.CFGAnalysis
|
||||||
import qualified ShellCheck.Checker
|
import qualified ShellCheck.Checker
|
||||||
import qualified ShellCheck.Checks.Commands
|
import qualified ShellCheck.Checks.Commands
|
||||||
|
import qualified ShellCheck.Checks.ControlFlow
|
||||||
import qualified ShellCheck.Checks.Custom
|
import qualified ShellCheck.Checks.Custom
|
||||||
import qualified ShellCheck.Checks.ShellSupport
|
import qualified ShellCheck.Checks.ShellSupport
|
||||||
import qualified ShellCheck.Fixer
|
import qualified ShellCheck.Fixer
|
||||||
|
@ -14,17 +18,24 @@ import qualified ShellCheck.Parser
|
||||||
|
|
||||||
main = do
|
main = do
|
||||||
putStrLn "Running ShellCheck tests..."
|
putStrLn "Running ShellCheck tests..."
|
||||||
results <- sequence [
|
failures <- filter (not . snd) <$> mapM sequenceA tests
|
||||||
ShellCheck.Analytics.runTests
|
if null failures then exitSuccess else do
|
||||||
,ShellCheck.AnalyzerLib.runTests
|
putStrLn "Tests failed for the following module(s):"
|
||||||
,ShellCheck.Checker.runTests
|
mapM (putStrLn . ("- ShellCheck." ++) . fst) failures
|
||||||
,ShellCheck.Checks.Commands.runTests
|
exitFailure
|
||||||
,ShellCheck.Checks.Custom.runTests
|
where
|
||||||
,ShellCheck.Checks.ShellSupport.runTests
|
tests =
|
||||||
,ShellCheck.Fixer.runTests
|
[ ("Analytics" , ShellCheck.Analytics.runTests)
|
||||||
,ShellCheck.Formatter.Diff.runTests
|
, ("AnalyzerLib" , ShellCheck.AnalyzerLib.runTests)
|
||||||
,ShellCheck.Parser.runTests
|
, ("ASTLib" , ShellCheck.ASTLib.runTests)
|
||||||
|
, ("CFG" , ShellCheck.CFG.runTests)
|
||||||
|
, ("CFGAnalysis" , ShellCheck.CFGAnalysis.runTests)
|
||||||
|
, ("Checker" , ShellCheck.Checker.runTests)
|
||||||
|
, ("Checks.Commands" , ShellCheck.Checks.Commands.runTests)
|
||||||
|
, ("Checks.ControlFlow" , ShellCheck.Checks.ControlFlow.runTests)
|
||||||
|
, ("Checks.Custom" , ShellCheck.Checks.Custom.runTests)
|
||||||
|
, ("Checks.ShellSupport", ShellCheck.Checks.ShellSupport.runTests)
|
||||||
|
, ("Fixer" , ShellCheck.Fixer.runTests)
|
||||||
|
, ("Formatter.Diff" , ShellCheck.Formatter.Diff.runTests)
|
||||||
|
, ("Parser" , ShellCheck.Parser.runTests)
|
||||||
]
|
]
|
||||||
if and results
|
|
||||||
then exitSuccess
|
|
||||||
else exitFailure
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# various resolvers. It's run via distrotest.
|
# various resolvers. It's run via distrotest.
|
||||||
|
|
||||||
resolvers=(
|
resolvers=(
|
||||||
nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
|
# nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
|
||||||
)
|
)
|
||||||
|
|
||||||
die() { echo "$*" >&2; exit 1; }
|
die() { echo "$*" >&2; exit 1; }
|
||||||
|
@ -15,13 +15,14 @@ die() { echo "$*" >&2; exit 1; }
|
||||||
command -v stack ||
|
command -v stack ||
|
||||||
die "stack is missing"
|
die "stack is missing"
|
||||||
|
|
||||||
stack setup || die "Failed to setup with default resolver"
|
stack setup --allow-different-user || die "Failed to setup with default resolver"
|
||||||
stack build --test || die "Failed to build/test with default resolver"
|
stack build --test || die "Failed to build/test with default resolver"
|
||||||
|
|
||||||
|
# Nice to haves, but not necessary
|
||||||
for resolver in "${resolvers[@]}"
|
for resolver in "${resolvers[@]}"
|
||||||
do
|
do
|
||||||
stack --resolver="$resolver" setup || die "Failed to setup $resolver"
|
stack --resolver="$resolver" setup || die "Failed to setup $resolver. This probably doesn't matter."
|
||||||
stack --resolver="$resolver" build --test || die "Failed build/test with $resolver!"
|
stack --resolver="$resolver" build --test || die "Failed build/test with $resolver! This probably doesn't matter."
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Success"
|
echo "Success"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue