mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 00:39:19 +08:00
Compare commits
1544 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
f7547c9a5a | ||
|
bd717c9d1b | ||
|
da0931740f | ||
|
555f8a80dd | ||
|
a9c04e8a37 | ||
|
9378227570 | ||
|
a128796c0c | ||
|
a0005bfa5a | ||
|
37a72d05ec | ||
|
c60323fb25 | ||
|
db11e2f663 | ||
|
aac1d05a7e | ||
|
67f0dc4fd5 | ||
|
8cf037fe5e | ||
|
615063a9c3 | ||
|
37e78141bd | ||
|
9f833770b0 | ||
|
7963eeab9d | ||
|
7a5e261d03 | ||
|
9d5363377e | ||
|
86d470c74f | ||
|
acee69676b | ||
|
a57f6d2886 | ||
|
d28c8f883f | ||
|
c43b19f897 | ||
|
45a67e7c64 | ||
|
68a03e05e5 | ||
|
014a66f3f6 | ||
|
fee13732a4 | ||
|
741d499b3d | ||
|
9b66bc2f13 | ||
|
00574dd1fc | ||
|
7b998239af | ||
|
4c9210af79 | ||
|
a75219e525 | ||
|
99d6df8a08 | ||
|
106f321cf0 | ||
|
1da0becb0f | ||
|
472579052b | ||
|
c735bbf30a | ||
|
eecd003e2d | ||
|
440d0038aa | ||
|
12bc7a750c | ||
|
c2d67c15f8 | ||
|
6043deb8f2 | ||
|
83d329c8da | ||
|
d0beac6d0b | ||
|
b88b253cad | ||
|
a8f9f25ec9 | ||
|
85c49a8af9 | ||
|
42abcb7ae2 | ||
|
d5c5128115 | ||
|
6d06103cab | ||
|
c95914f9b3 | ||
|
ea24e25efd | ||
|
8f0448133c | ||
|
7fc9496320 | ||
|
962fad038c | ||
|
a223a7a5a5 | ||
|
8e9290badb | ||
|
292b0840d9 | ||
|
43c24cf79c | ||
|
21ad4196db | ||
|
172aa7c4fc | ||
|
c290eace54 | ||
|
a6efd02807 | ||
|
057cc714b3 | ||
|
0e00249eae | ||
|
0ca50159ec | ||
|
7e6a556ef1 | ||
|
4bfe6496d9 | ||
|
ffbbfcfe25 | ||
|
cc424bac11 | ||
|
cb01cbf7eb | ||
|
1e32139f66 | ||
|
4d92a2e15c | ||
|
f8648e5465 | ||
|
4fd8de058b | ||
|
aaffe38198 | ||
|
bd116f252b | ||
|
ef51ed3950 | ||
|
61b073d507 | ||
|
9d604ae732 | ||
|
1ca0b72329 | ||
|
474b23d6e7 | ||
|
fe2b4b5079 | ||
|
e820a5642b | ||
|
392b57b8e8 | ||
|
6595e14d25 | ||
|
115ef29079 | ||
|
76b798394f | ||
|
8a005526cc | ||
|
c29b6afa56 | ||
|
e6e89d68fd | ||
|
f25b8bd03a | ||
|
d7278b95f2 | ||
|
5487b3f229 | ||
|
28978a8b65 | ||
|
f5c6771016 | ||
|
0f48bb78a5 | ||
|
93be86f988 | ||
|
3449e6be21 | ||
|
2e52c2b56a | ||
|
1696296c0a | ||
|
a82e606e8d | ||
|
93486ed6ac | ||
|
499e0ceaba | ||
|
ff5f29f661 | ||
|
c7bf1fd96e | ||
|
b96b7f35f4 | ||
|
926ee54036 | ||
|
fdd02c94c0 | ||
|
9008a6833b | ||
|
ce60a1764f | ||
|
cbcca528ae | ||
|
83187dafd7 | ||
|
d919aaa847 | ||
|
3f296a08c1 | ||
|
0f15fa49ba | ||
|
0a4580e234 | ||
|
5c7d8129ad | ||
|
e075cde357 | ||
|
9f578f41a1 | ||
|
2c026f1ec7 | ||
|
874bdcb514 | ||
|
fa3eb47193 | ||
|
989ac32625 | ||
|
2bbfd0570d | ||
|
9b1befadc1 | ||
|
f44624a9c0 | ||
|
c75bbcbd60 | ||
|
daa9c08dd5 | ||
|
4da34fbc64 | ||
|
4a63a3a8bd | ||
|
2341a4c683 | ||
|
7eb6b35cb0 | ||
|
93eca1cb8e | ||
|
e701cf6fad | ||
|
5962b01816 | ||
|
5becc673b2 | ||
|
84ca7711c4 | ||
|
0e0de94045 | ||
|
699aac589a | ||
|
30c75340e6 | ||
|
4dfd7eb1cf | ||
|
79ba67dbd3 | ||
|
60f75e5b8a | ||
|
f042b0ebd1 | ||
|
764fdcb260 | ||
|
7473d4a743 | ||
|
91abd979f2 | ||
|
afea62de4e | ||
|
fa0f88c106 | ||
|
7fb399528c | ||
|
de9ab4e6ef | ||
|
ff1eab286c | ||
|
e01c470598 | ||
|
9423691039 | ||
|
71a4053e8c | ||
|
3fdc6babb2 | ||
|
c175971bf0 | ||
|
b7b4d5d29e | ||
|
9cc9a575b2 | ||
|
b2dd00e4ee | ||
|
2053ac8882 | ||
|
e4cbf59fda | ||
|
f9c8a255be | ||
|
bfb2d79e54 | ||
|
fbb571811f | ||
|
0eaef95db8 | ||
|
f4deac6e43 | ||
|
49aa600c85 | ||
|
25b5b77240 | ||
|
ded04820b8 | ||
|
7a1fb2523d | ||
|
38bb156a1c | ||
|
023ae5dfda | ||
|
e280116ef0 | ||
|
788cf17076 | ||
|
78b8e76066 | ||
|
914974bd4f | ||
|
c0d3a98fcd | ||
|
380221a02c | ||
|
ba2c20a08a | ||
|
4d56852b9f | ||
|
be1f1c1ab7 | ||
|
bee4303c32 | ||
|
ef764b60ca | ||
|
9e6b07dbba | ||
|
3e3e4fd0cd | ||
|
561075ea79 | ||
|
42f0dce467 | ||
|
9702f1ff9c | ||
|
544047c5af | ||
|
321afa427e | ||
|
c381c5746f | ||
|
eeb7ea01c9 | ||
|
3116ed3ae5 | ||
|
e95d8dd14e | ||
|
f6ba500d6b | ||
|
c5aa171a5f | ||
|
b1aeee564c | ||
|
b8b4a11348 | ||
|
e099625e7d | ||
|
5242e384a1 | ||
|
7e77bfae49 | ||
|
9059024de6 | ||
|
0eebb50563 | ||
|
3b5aa84757 | ||
|
200aabb63c | ||
|
c6dcb4127a | ||
|
61d2112e71 | ||
|
9f0ef5983a | ||
|
1297ef46d7 | ||
|
f4be53eb19 | ||
|
3e7c2bfec0 | ||
|
07ffcb626e | ||
|
36bb1e7858 | ||
|
95b1185882 | ||
|
8efbecd64a | ||
|
52a9d90e1a | ||
|
f5892f2d0d | ||
|
de7541e656 | ||
|
861b63aa77 | ||
|
64c9c83cc8 | ||
|
aa3b709b5d | ||
|
0358090b3c | ||
|
ea05271fa3 | ||
|
50116e8aee | ||
|
5ccaddbcc2 | ||
|
80b7e1e099 | ||
|
50af8aba29 | ||
|
5fb1da6814 | ||
|
58205a3573 | ||
|
5b177d62cb | ||
|
0ab3a726d3 | ||
|
2791a48444 | ||
|
bb63d66f7c | ||
|
d9e419d60f | ||
|
aa4b24e458 | ||
|
1c7a9f8a2f | ||
|
2521c1cf56 | ||
|
65e7f2059d | ||
|
248858c13e | ||
|
c0d4c5a106 | ||
|
ec25fb4052 | ||
|
a3cd5979a2 | ||
|
37b24cc129 | ||
|
d72a5faa1f | ||
|
e2e65e1350 | ||
|
6ccf9d6af1 | ||
|
9470b9dc31 | ||
|
bf1003eae3 | ||
|
301705edea | ||
|
c6c12f52bd | ||
|
af46758ff1 | ||
|
025c380b84 | ||
|
10955a143c | ||
|
67dbbcbd89 | ||
|
cef4c1a0bc | ||
|
b824294961 | ||
|
5b7354918f | ||
|
b76c0a8221 | ||
|
c860b74505 | ||
|
9652ccfdbd | ||
|
f514f5f735 | ||
|
c53c8a5ead | ||
|
b456987b84 | ||
|
ed92fe501f | ||
|
bbe5155e63 | ||
|
4dfb3fce9c | ||
|
581bcc3907 | ||
|
293c3b27b8 | ||
|
25ea405468 | ||
|
e45d81c8fa | ||
|
05e657e130 | ||
|
bd19ab4fa9 | ||
|
8aa44bf529 | ||
|
45021a9b40 | ||
|
d31d31df23 | ||
|
3a276bd336 | ||
|
d3f6e045e2 | ||
|
abe6afc09f | ||
|
d984f8cbe7 | ||
|
acef53be9c | ||
|
2ea2293154 | ||
|
d40d376bf4 | ||
|
a669e1684b | ||
|
420d913bbf | ||
|
fb7ac3f57e | ||
|
112a7d8b9b | ||
|
31c5601c5e | ||
|
2737496b3a | ||
|
a404efab65 | ||
|
3c94d8b3eb | ||
|
a89403f09b | ||
|
6dcf4b8e64 | ||
|
d4d219affd | ||
|
489c3a4ddf | ||
|
1507e92c44 | ||
|
63a259e5be | ||
|
59c47f2266 | ||
|
a621eba6d3 | ||
|
978bfdd5da | ||
|
a03d94c0b2 | ||
|
e1fe9be7af | ||
|
c97cb8cf54 | ||
|
a504ca6b57 | ||
|
437f73c001 | ||
|
f187382a0c | ||
|
661be056f1 | ||
|
9f45dc4c8b | ||
|
8e31e86cc4 | ||
|
c6c615217b | ||
|
c1adc588fb | ||
|
3107a1bae0 | ||
|
73859039dd | ||
|
1e6a30905a | ||
|
a4b9cec9f0 | ||
|
c3a56659f4 | ||
|
e0a4241baa | ||
|
1835ebd3a0 | ||
|
b34f4c1f4b | ||
|
ec6f9e4d49 | ||
|
3760e7945f | ||
|
fcdd6055df | ||
|
fd2beaadfa | ||
|
df7f00eaed | ||
|
e45b679d58 | ||
|
263401cfcb | ||
|
0e21f91c07 | ||
|
4ecdc10599 | ||
|
baa4d2e555 | ||
|
26c55750cf | ||
|
9c42d43e90 | ||
|
434b904746 | ||
|
ab2b0e11a3 | ||
|
394f4d6505 | ||
|
4a2b2c7396 | ||
|
97cb753d21 | ||
|
98266a1878 | ||
|
6138206ce5 | ||
|
6debd59f02 | ||
|
9425654a42 | ||
|
461be74976 | ||
|
278ce56650 | ||
|
73822c3588 | ||
|
29dedbdc9c | ||
|
f6bc009331 | ||
|
ef811995fa | ||
|
73a41cdd2f | ||
|
1b4c486748 | ||
|
95a8cf93c9 | ||
|
bd04af0769 | ||
|
9acc8fcb53 | ||
|
897f019353 | ||
|
0636e7023c | ||
|
08ca1ee6e9 | ||
|
eb3e6fe8e1 | ||
|
ecd61bfc68 | ||
|
a8d88dfe98 | ||
|
7d2c519d64 | ||
|
3403f8d75b | ||
|
408a3b99d8 | ||
|
bc111141f8 | ||
|
3471ad45b1 | ||
|
d5ba41035b | ||
|
88aef838f1 | ||
|
3d61b73e91 | ||
|
138080bdc7 | ||
|
5b3f17c29d | ||
|
b47e083ee3 | ||
|
3cba76dc7d | ||
|
eb588f62f6 | ||
|
bcd13614eb | ||
|
a8376a09a9 | ||
|
5ed89d2241 | ||
|
4a87d2a3de | ||
|
41613babd9 | ||
|
cb57b4a74f | ||
|
e3f0243c0e | ||
|
66b5f13c6f | ||
|
a7a404a5a8 | ||
|
0761f5c923 | ||
|
b55149b22d | ||
|
4097bb5154 | ||
|
1b207b3d43 | ||
|
135b4aa485 | ||
|
cb76951ad2 | ||
|
705e476e4c | ||
|
e705552c97 | ||
|
198aa4fc3d | ||
|
f4044fbcc7 | ||
|
2827b35696 | ||
|
de95c376ea | ||
|
5e1b1e010a | ||
|
620c9c2023 | ||
|
359b1467a2 | ||
|
df0a0d41fa | ||
|
b815242506 | ||
|
07b5aa2971 | ||
|
f7b82658f4 | ||
|
e0e46e979a | ||
|
79319558a5 | ||
|
8d13add1ed | ||
|
8940e60300 | ||
|
5f1c969546 | ||
|
dadfdfde97 | ||
|
3e2cb26119 | ||
|
1a6ae4f19e | ||
|
95a376aad1 | ||
|
a06d7c1841 | ||
|
5202072a34 | ||
|
72af1cfd59 | ||
|
228af7df54 | ||
|
6db392511b | ||
|
07f04e13ce | ||
|
493ecd6f73 | ||
|
f0a2e688c4 | ||
|
0cee8a993d | ||
|
3d03b0ab3b | ||
|
488d6dcb41 | ||
|
d02a9bbcce | ||
|
165e408114 | ||
|
932e2b3538 | ||
|
76b1482f64 | ||
|
49250eadae | ||
|
3fe11927bb | ||
|
b16da4b242 | ||
|
c8e0797350 | ||
|
15aaacf715 | ||
|
5ef4229f61 | ||
|
afada43978 | ||
|
8be76b13b9 | ||
|
581be5878b | ||
|
0f835a5a2c | ||
|
4b0a35d4c9 | ||
|
51e0c1be62 | ||
|
d8a32da07f | ||
|
0d1a34a291 | ||
|
5005dc0fa1 | ||
|
b8ee7436e5 | ||
|
da8e450386 | ||
|
c3ac4c3d87 | ||
|
03ce3b15b6 | ||
|
10edba3ab8 | ||
|
797b424917 | ||
|
84e678e9ff | ||
|
3a672968f3 | ||
|
8c7efae393 | ||
|
f91b5bc270 | ||
|
b01f1128c7 | ||
|
db33294838 | ||
|
75fb4da387 | ||
|
366262af18 | ||
|
6869c2fa18 | ||
|
868a7be33e | ||
|
7138abff4b | ||
|
9d3e79b576 | ||
|
402e635f86 | ||
|
91cbcddd9d | ||
|
963b39b002 | ||
|
0cc45447d3 | ||
|
32a53f21b5 | ||
|
12b8720bd8 | ||
|
7adeaccd11 | ||
|
b63483d44c | ||
|
4111ce8fde | ||
|
b9a9eb2529 | ||
|
e717802de1 | ||
|
1699c9e9ba | ||
|
bfc32200e2 | ||
|
52e8a42d9d | ||
|
00360af672 | ||
|
8ff35fb4af | ||
|
29e8c0a16e | ||
|
3848788c2d | ||
|
0c459ae2cb | ||
|
e496b413bd | ||
|
48ac654a93 | ||
|
4470fe715c | ||
|
379321d1f3 | ||
|
0adea473fd | ||
|
a3be776f80 | ||
|
b5e5d249c4 | ||
|
efffc6150b | ||
|
cb4a0c0250 | ||
|
135cf5932f | ||
|
467dfe07b6 | ||
|
d140388bea | ||
|
884eff0c36 | ||
|
1d8047cce1 | ||
|
77546fba2f | ||
|
4a5ee06ce4 | ||
|
4dceecb1ed | ||
|
5b226e733b | ||
|
46c10c1571 | ||
|
1de8ba0210 | ||
|
407f6a63b9 | ||
|
7ee7448a70 | ||
|
48ebd41e22 | ||
|
0c88fbc76d | ||
|
b3362f1dc3 | ||
|
2c6bc43614 | ||
|
235bf6605f | ||
|
7029a713c7 | ||
|
cf608dc2f6 | ||
|
aa3b3fdc56 | ||
|
bca2ad4e18 | ||
|
719e1854e5 | ||
|
20ad7dc8de | ||
|
f84859ab90 | ||
|
08235a1cb2 | ||
|
728922d2b8 | ||
|
a953dd3454 | ||
|
ef6a5b97b9 | ||
|
8873a1732b | ||
|
5adfce72e1 | ||
|
12b3fdf661 | ||
|
bb4ce86fab | ||
|
7ec2fa2d3e | ||
|
5481ccd7f7 | ||
|
a1d8947297 | ||
|
683a30abde | ||
|
573936f353 | ||
|
ce7658ed86 | ||
|
0136d9ccce | ||
|
a4c6cea5e6 | ||
|
32af2783f0 | ||
|
08aab3c161 | ||
|
cf39adff75 | ||
|
da4072a118 | ||
|
08d2eef411 | ||
|
de257a6cf3 | ||
|
68c24925bc | ||
|
366dc5d3f8 | ||
|
1ed743e410 | ||
|
9a2aad16ad | ||
|
177cb10daa | ||
|
4aca1ff128 | ||
|
ffed7caff4 | ||
|
ef28200199 | ||
|
55216792c9 | ||
|
51e115cf47 | ||
|
764b242f1b | ||
|
c3b606c68a | ||
|
9f53109dfa | ||
|
795a881219 | ||
|
6dd5350e3b | ||
|
a5b359591c | ||
|
966194e387 | ||
|
71bcc80c2f | ||
|
48616225b3 | ||
|
99276cb9f5 | ||
|
f769d4e92c | ||
|
71df01c00f | ||
|
5364701914 | ||
|
fb97aca5a6 | ||
|
6b81a9924c | ||
|
cd7c077ecc | ||
|
b33607b048 | ||
|
969230f171 | ||
|
a98d69f4ff | ||
|
f71c142a44 | ||
|
9dfcf54f10 | ||
|
c8cd9dd09c | ||
|
8b8aeb4409 | ||
|
ee354ffce8 | ||
|
9fc3ddf849 | ||
|
ecb9d07f52 | ||
|
d16bf41c3d | ||
|
8d5e3a80ae | ||
|
34e0fa53c8 | ||
|
7fb27310e1 | ||
|
00d3c09ddb | ||
|
e8fc09414a | ||
|
b7a8b090d2 | ||
|
72044a79c6 | ||
|
6511dc0246 | ||
|
740441f2c4 | ||
|
b311563421 | ||
|
6d257bfa17 | ||
|
d8717c7046 | ||
|
7aa3a7ffc3 | ||
|
017af8333f | ||
|
f73d6f2332 | ||
|
a840f4e464 | ||
|
0f5e40c076 | ||
|
ccaacb108a | ||
|
56751413b4 | ||
|
ba5f20deda | ||
|
c86885427c | ||
|
7b3c4025fb | ||
|
3b004275cf | ||
|
72971fa52b | ||
|
dbdab5705f | ||
|
46a3019ed7 | ||
|
81978d15bd | ||
|
1d0db9267d | ||
|
a6fb9d1ef8 | ||
|
dc1e7c1bd4 | ||
|
5b14dba489 | ||
|
ee997fdec4 | ||
|
1badeff383 | ||
|
2d5ed23ca1 | ||
|
ec581cee90 | ||
|
bb32289ee3 | ||
|
31d6b063d9 | ||
|
3c5c74ff04 | ||
|
9657e8dda3 | ||
|
6ed60b403f | ||
|
8fa8823981 | ||
|
161801a86e | ||
|
c36f6d89ba | ||
|
e801da0621 | ||
|
51e6bf809f | ||
|
3413a076ff | ||
|
53f63b85bb | ||
|
df068bc8ed | ||
|
102683ab04 | ||
|
acead72c93 | ||
|
0c1e2bbd4d | ||
|
5d9cb81008 | ||
|
1491402dcb | ||
|
436a46ebab | ||
|
db1e24d140 | ||
|
35daf7534b | ||
|
76ad5dbb9f | ||
|
f73736e5c9 | ||
|
3785a08906 | ||
|
74c199b51a | ||
|
371dcdda3a | ||
|
38044e3f75 | ||
|
b0f6f935f3 | ||
|
bd2facb245 | ||
|
895ba31337 | ||
|
ccc037d458 | ||
|
a1b370efbc | ||
|
7f36c369f3 | ||
|
7b55e73e03 | ||
|
6c068e7d29 | ||
|
8dd40efb44 | ||
|
751aebf984 | ||
|
3bf6913a15 | ||
|
73d06c4f47 | ||
|
72ed234291 | ||
|
b94c03e5a1 | ||
|
226bc4409c | ||
|
4a6acb6ff0 | ||
|
1d76abc439 | ||
|
807d899f3b | ||
|
d6803ffa24 | ||
|
4ec8d73a14 | ||
|
81388cefd2 | ||
|
43bb6a20ad | ||
|
8f99d2b008 | ||
|
79ae89076a | ||
|
aa33280cb0 | ||
|
bd13224907 | ||
|
b064cf3038 | ||
|
79d6066450 | ||
|
1463cf773a | ||
|
31bb02d6b7 | ||
|
5bd33dbf92 | ||
|
a3c6aff0fb | ||
|
8184ef1e8b | ||
|
a839a6657b | ||
|
a10b924570 | ||
|
8f31ae913b | ||
|
a06ad41bfa | ||
|
21f5bf01eb | ||
|
2ded4df6fa | ||
|
90da31f226 | ||
|
b1486ec1e9 | ||
|
954aa99b11 | ||
|
79872f92f8 | ||
|
bf9b841b07 | ||
|
5fad708df5 | ||
|
5cece759cc | ||
|
50c8172de4 | ||
|
ce950edbfd | ||
|
f8e75d3e89 | ||
|
6f4e06d83c | ||
|
505ff7832f | ||
|
ac3f0b3360 | ||
|
070a465b64 | ||
|
4243c6a0bf | ||
|
8bc89bc451 | ||
|
5099ebf9b9 | ||
|
d943ef6f77 | ||
|
5e4c288cf4 | ||
|
9e35aa7ce8 | ||
|
21d7068bc8 | ||
|
324aa3cc88 | ||
|
9c4f651e6b | ||
|
3cf8b9ceab | ||
|
5c01b6c7f5 | ||
|
7604e5eb58 | ||
|
4fb1080809 | ||
|
4f9a80db15 | ||
|
3a38c50b8e | ||
|
fd79e80e78 | ||
|
1fd9b474ba | ||
|
faafc99704 | ||
|
bc882fd85a | ||
|
41b6e3d5eb | ||
|
da1691912b | ||
|
0feb95b337 | ||
|
f0e0d9ffdb | ||
|
3c75674b50 | ||
|
8e5e77ad76 | ||
|
66c7cf19e2 | ||
|
36573b5b26 | ||
|
9e4a9c8c6c | ||
|
c2fcb742db | ||
|
e8b4a79b65 | ||
|
08d1d37094 | ||
|
e28e90133d | ||
|
2688a81526 | ||
|
82c3084438 | ||
|
c6ff1933b7 | ||
|
22c86256ac | ||
|
a3a4873190 | ||
|
750212af39 | ||
|
2154583fd3 | ||
|
35c74e4747 | ||
|
46fb91b44d | ||
|
128d5d6013 | ||
|
41176c23a6 | ||
|
342726f480 | ||
|
1863f2f12d | ||
|
8809a36952 | ||
|
7f307c5775 | ||
|
eb12086d05 | ||
|
6259a32601 | ||
|
4e13c7cbc1 | ||
|
edb01fa855 | ||
|
85e6c35845 | ||
|
43f667a8f9 | ||
|
daacc98a8f | ||
|
40907b1636 | ||
|
96168fc707 | ||
|
6aee12a572 | ||
|
0048f34b11 | ||
|
e679ff222a | ||
|
6f5648faca | ||
|
30e94ea7ab | ||
|
d8f8a2fa14 | ||
|
df3cc70658 | ||
|
5669702362 | ||
|
bd9d05c759 | ||
|
af87fe9315 | ||
|
c2850e436f | ||
|
d73fc8f91d | ||
|
7124c113e8 | ||
|
f594f01d35 | ||
|
6ccb7e9129 | ||
|
838f0ce4dc | ||
|
105b09792c | ||
|
f6618d4332 | ||
|
0a381be37b | ||
|
46e47dad45 | ||
|
fee6c94d40 | ||
|
cf1c46d852 | ||
|
a40efffec9 | ||
|
44b96fca66 | ||
|
b4fb439191 | ||
|
0897ab7092 | ||
|
9703f89f79 | ||
|
aadf02e635 | ||
|
090051bdc1 | ||
|
b8c96b4361 | ||
|
f26038125d | ||
|
08f7ff37c5 | ||
|
60fc33ebdf | ||
|
3a006f7bcb | ||
|
89b6fd58fa | ||
|
069ddeffcc | ||
|
59c4ed106c | ||
|
5efb724a3e | ||
|
619b6c42f3 | ||
|
88c56ecd53 | ||
|
6b62b5bf7e | ||
|
8672af29ef | ||
|
1a8e34bfea | ||
|
b0dae063bf | ||
|
4fc4899803 | ||
|
cd4896192c | ||
|
868d53af95 | ||
|
6a4b86cbea | ||
|
fe2398edc9 | ||
|
3a7dc86de1 | ||
|
1c0ec9c6f6 | ||
|
84110dbef4 | ||
|
1a7e98beaf | ||
|
a5d5831a2e | ||
|
47201822f9 | ||
|
32689ef5eb | ||
|
87481dce25 | ||
|
a90b6d14b3 | ||
|
5a46eeb09a | ||
|
47a7065a7a | ||
|
dbafbb3b3b | ||
|
13a2070a32 | ||
|
fa4874c044 | ||
|
6a71ff6f46 | ||
|
36263fb3f5 | ||
|
6dc419bbf5 | ||
|
7af3470a91 | ||
|
42f7479fb8 | ||
|
50084c06c5 | ||
|
e3bef9dc97 | ||
|
6c1abb2dee | ||
|
43c26061b9 | ||
|
07fd5724b8 | ||
|
eb2472ada8 | ||
|
3e5ecaa262 | ||
|
e1cec6c5d3 | ||
|
eaa319ec57 | ||
|
717b5e91f5 | ||
|
7f5f5b7fb5 | ||
|
856d57f7d8 | ||
|
c45e9d4878 | ||
|
89c6f6c800 | ||
|
85e69f86eb | ||
|
47fd16b8e8 | ||
|
1d04754b37 | ||
|
13ff0a7432 | ||
|
40136fe249 | ||
|
86999ded1f | ||
|
7551a241ad | ||
|
2f0ae44de4 | ||
|
51d8caf2c9 | ||
|
f835c2d4c1 | ||
|
db0c8c2dc9 | ||
|
9911470d67 | ||
|
a5821c3a4d | ||
|
c91083354f | ||
|
2957fb64c9 | ||
|
459e30804f | ||
|
49569e10e6 | ||
|
ba0221a1da | ||
|
944313c6ba | ||
|
6af1aeb259 | ||
|
b7c9d23452 | ||
|
e792d69293 | ||
|
4d8f2eb707 | ||
|
8a3bd25f7c | ||
|
825c1b5d22 | ||
|
92473b512a | ||
|
7e75d12ce1 | ||
|
7d278c3ca1 | ||
|
5f1175fb58 | ||
|
257b794322 | ||
|
89572d3a96 | ||
|
15edcbd4d5 | ||
|
736febaa3c | ||
|
a21df2d88f | ||
|
d473fb8867 | ||
|
f754363733 | ||
|
ef1f8f535e | ||
|
f9909504dd | ||
|
fa4cefda9d | ||
|
f2f6c66902 | ||
|
1f4dd85548 | ||
|
528381796e | ||
|
ad7ad28246 | ||
|
33ab998b02 | ||
|
9c28237d52 | ||
|
e0e5ba3a90 | ||
|
b4390414ef | ||
|
8acd5b13cd | ||
|
d00ca0c283 | ||
|
8bc98d89a7 | ||
|
c7964a7a78 | ||
|
8ec87d6655 | ||
|
c3df2bf761 | ||
|
d1df3713ca | ||
|
23496e93b0 | ||
|
437e69fbba | ||
|
63ad3f99ad | ||
|
0044c3dd6e | ||
|
a3d4101d6c | ||
|
bd359c5c0f | ||
|
498de63337 | ||
|
52ab7dee2d | ||
|
1a5296659b | ||
|
a66ee2967c | ||
|
d985380f48 | ||
|
6739c4a729 | ||
|
7415c9dcb7 | ||
|
d3fc1f355d | ||
|
48fd793581 | ||
|
e5842e2e2b | ||
|
cf445c7d20 | ||
|
ffb9578a98 | ||
|
630f20e888 | ||
|
8f5f91f041 | ||
|
8d9d4533c3 | ||
|
a4b4954a23 | ||
|
38cea9201d | ||
|
4ce916ec1d | ||
|
b9cb040128 | ||
|
2488be7298 | ||
|
d01b59a827 | ||
|
f77821625c | ||
|
1eece5b2ee | ||
|
58d45e3fa4 | ||
|
5aaa1a7d9a | ||
|
3b36c2c820 | ||
|
55692926b9 | ||
|
4172722167 | ||
|
485593da2c | ||
|
1181c6b3af | ||
|
ee181cfc43 | ||
|
c72667407b | ||
|
5467a0f1d9 | ||
|
3fc77d94ec | ||
|
23e0420cb1 | ||
|
a898165ac7 | ||
|
ba5e3db31a | ||
|
56145217fe | ||
|
94d265ce41 | ||
|
0f00de80fd | ||
|
c808c9b6fe | ||
|
bf9297e2a5 | ||
|
7f547cc0ec | ||
|
01c27dc96a | ||
|
856a204ec3 | ||
|
f054e2e2cc | ||
|
090e09e4ca | ||
|
10276c878d | ||
|
ae4aea4530 | ||
|
d0029ae1d4 | ||
|
eea7bc326e | ||
|
73cd2cdd6f | ||
|
a01862bc12 | ||
|
ccb6bf1ed5 | ||
|
136b654867 | ||
|
f31c8bd3a3 | ||
|
0dd61b65d8 | ||
|
07747b30fb | ||
|
26d16eb8ad | ||
|
54b2d14847 | ||
|
f653362b18 | ||
|
f85441add9 | ||
|
67cfcfd206 | ||
|
72eeafe002 | ||
|
6d9e8472e6 | ||
|
47d68019e5 | ||
|
cbda90eeb5 | ||
|
722b0606e8 | ||
|
95cfd87589 | ||
|
0a1beb883f | ||
|
83adcba88e | ||
|
35fb5073f4 | ||
|
de59c3586b | ||
|
8894333556 | ||
|
b1843c520f | ||
|
d406ba9950 | ||
|
d5dfb4a7c1 | ||
|
7929a9dbba | ||
|
7e84ad031f | ||
|
7eef12102b | ||
|
0522a5f0bd | ||
|
6c21e4671b | ||
|
3d83b87c9a | ||
|
f86d68bcc0 | ||
|
1e65d36874 | ||
|
1ff67a61b4 | ||
|
349dfdab35 | ||
|
1ab29ddb39 | ||
|
09b7788412 | ||
|
ef2135f3aa | ||
|
d10c3b2709 | ||
|
ca37794b7c | ||
|
8b8b48ef55 | ||
|
aea0310a07 | ||
|
7fff088ce9 | ||
|
65ab8c8ecb | ||
|
3a041954d1 | ||
|
828378cdff | ||
|
509cda4dcf | ||
|
6076f0b1da | ||
|
1d26c280d6 | ||
|
c785d43e34 | ||
|
4c3e731445 | ||
|
3940462da3 | ||
|
bb7ef5834b | ||
|
2f7bd556e8 | ||
|
081751c1b5 | ||
|
cc86aab3f1 | ||
|
9f1f00cdd1 | ||
|
93debd3556 | ||
|
47b971c582 | ||
|
f25ae90746 | ||
|
3daa47c0f2 | ||
|
ed56a837c3 | ||
|
80cf5d9852 | ||
|
8e554ae3d4 | ||
|
0a80188363 | ||
|
0e1a64b6ba | ||
|
0a2cf208c8 | ||
|
dcc10bbdf6 | ||
|
2c2e41952f | ||
|
0d74140650 | ||
|
955ad60823 | ||
|
2573332d77 | ||
|
00c470f323 | ||
|
63188282e9 | ||
|
61b4b65184 | ||
|
39b2bf4378 | ||
|
2fe117728d | ||
|
cde3ba8769 | ||
|
33c78b7c95 | ||
|
a485482979 | ||
|
895d83afc5 | ||
|
39bc011757 | ||
|
fe0a398239 | ||
|
1be0f1ea75 | ||
|
c9aa133282 | ||
|
7b70500d41 | ||
|
8bed447411 | ||
|
22710bf4d8 | ||
|
a354685ab1 | ||
|
a8ff7a02fd | ||
|
c5479b8ca3 | ||
|
d9dd58bec8 | ||
|
af1bb93aba | ||
|
e909c8ac42 | ||
|
93140e31a0 | ||
|
97f3834852 | ||
|
0369f43bac | ||
|
eb2eae2888 | ||
|
30c0c1f27d | ||
|
bff5d11566 | ||
|
eccb9f3f71 | ||
|
2814572116 | ||
|
90bafb9aba | ||
|
39b88bbaac | ||
|
39805ab200 | ||
|
9dadce96c0 | ||
|
1a0e208cc3 | ||
|
a69e27b774 | ||
|
b05c12223f | ||
|
38ead0385b | ||
|
9e8a11e57c | ||
|
6b84b35ec0 | ||
|
669fdf8e5e | ||
|
dccfb3c4a1 | ||
|
40ce949a56 | ||
|
9f3802138f | ||
|
2f3533fff6 | ||
|
f9c346cfd7 | ||
|
5f7419ca37 | ||
|
8494509150 | ||
|
8ba1f2fdf2 | ||
|
dbadca9f61 | ||
|
0347ce1b7a | ||
|
7fbe66e1c6 | ||
|
b000b05507 | ||
|
39423ddf81 | ||
|
875c2d2aad | ||
|
64cc7c691a | ||
|
b9784cbcc0 | ||
|
1a3f6aadaf | ||
|
35756c2cd6 | ||
|
0fd351404f | ||
|
4caa7e7900 | ||
|
c11c0196d5 | ||
|
b035331d4a | ||
|
d13253973b | ||
|
d9c622ae33 | ||
|
aac7d76047 | ||
|
fc421adb45 | ||
|
e0d3c6923a | ||
|
9772ba9de4 | ||
|
3a944de606 | ||
|
3dd592a02a | ||
|
61531cbb10 | ||
|
d53087f056 | ||
|
39756b420e | ||
|
52d4efc951 | ||
|
5dac723593 | ||
|
2364fd58b6 | ||
|
cde364c97b | ||
|
98b790f87a | ||
|
726a4e5848 | ||
|
0a9ed917e7 | ||
|
b10d31c8b7 | ||
|
133c779701 | ||
|
b18ee3fdef | ||
|
3fcc6c44d8 | ||
|
d830a36bc8 | ||
|
1af23fd131 | ||
|
d21b3362b2 | ||
|
6cd454e88b | ||
|
0b5f6b9762 | ||
|
3824e9cfc2 | ||
|
fdce0116da | ||
|
b069f7ed27 | ||
|
c4181d45d2 | ||
|
680f838c63 | ||
|
e6d81ca7b7 | ||
|
fd909eeca0 | ||
|
deab146fab | ||
|
f9aeabc245 | ||
|
558d8ffc6c | ||
|
e96c4c3ffa | ||
|
c566efd442 | ||
|
47c220d59c | ||
|
4bd902c5c4 | ||
|
033ce6d941 | ||
|
6ad3f557fe | ||
|
d0bad6c057 | ||
|
58c362f97c | ||
|
5f568dd207 | ||
|
2c1e414ac5 | ||
|
6699109ab8 | ||
|
423ca82296 | ||
|
0a263579e0 | ||
|
d63406abe4 | ||
|
81956d324d | ||
|
f549aad809 | ||
|
f9f965693d | ||
|
727d940e10 | ||
|
c26c2b8536 | ||
|
d8878ed852 | ||
|
c3cc5f649f | ||
|
8bd4365cdb | ||
|
a00a6fb53b | ||
|
3332eba9a0 | ||
|
ad08bb64aa | ||
|
f01e6e1a99 | ||
|
de0145fb29 | ||
|
0d4ae95e1d | ||
|
50db49e2fb | ||
|
60aafae21d | ||
|
902cb9c303 | ||
|
4f1fd43360 | ||
|
ca5af5c55a | ||
|
503cac3bb3 | ||
|
2a9c9ae0ad | ||
|
def4551991 | ||
|
67f4a0d6eb | ||
|
f92f934688 | ||
|
d4059c30b7 | ||
|
b68de7f42b | ||
|
7dacb62d36 | ||
|
3423cde931 | ||
|
b2d1aa01f7 | ||
|
19e1bdf11f | ||
|
75d51087c8 | ||
|
ed524fb77f | ||
|
97045c4af1 | ||
|
1b806f6c9f | ||
|
632c1614a1 | ||
|
00d9ef12e7 | ||
|
d07294810b | ||
|
948b750754 | ||
|
41ae95116d | ||
|
bf3c942294 | ||
|
055b40462d | ||
|
b087b7efb1 | ||
|
5d8d57cf07 | ||
|
661091a9da | ||
|
2ec60c2627 | ||
|
8b4909b238 | ||
|
95a3be6546 | ||
|
968e34e002 | ||
|
197b3e3f20 | ||
|
0e464ea476 | ||
|
811df6f0da | ||
|
4e5d32b05a | ||
|
c5141b77bf | ||
|
9dfeb6b42a | ||
|
77916d2645 | ||
|
4968e7d9ff | ||
|
075d58ee90 | ||
|
6a4a5a815e | ||
|
76a39f254b | ||
|
8ec9fa43fd | ||
|
e8634a3c27 | ||
|
9ae776530b | ||
|
0ec62390d5 | ||
|
82328cd86e | ||
|
5b58da7249 | ||
|
8676517270 | ||
|
4262c4b1bf | ||
|
7ad0110443 | ||
|
e9bba2f75a | ||
|
74ea5eaeec | ||
|
b7ee5f4410 | ||
|
e294db171e | ||
|
8c3d8d7cfa | ||
|
380d6c3317 | ||
|
16bd52333a | ||
|
cfb44b3fe2 | ||
|
43ed5e748d | ||
|
4dca88aade | ||
|
1d2c7a8551 | ||
|
ba080e7e34 | ||
|
fc716738eb | ||
|
659709d529 | ||
|
5b4729d940 | ||
|
b936f28763 | ||
|
78d9a7ad97 | ||
|
d540a98d33 | ||
|
8c00850134 | ||
|
d1990e3396 | ||
|
91fc4a046c | ||
|
95ebe1cd07 | ||
|
27822a1f56 | ||
|
eb06b06475 | ||
|
5d72432046 | ||
|
da51b14789 | ||
|
7be8485b8b | ||
|
a4d36ba0d2 | ||
|
d4bc0f6e10 | ||
|
1011ae7b3c | ||
|
d603ee1e89 | ||
|
4fc518c877 | ||
|
7fda86d6e2 | ||
|
6905373b6c | ||
|
1d8401d583 | ||
|
a89aee1a34 | ||
|
4853dce3fe | ||
|
a793e09bab | ||
|
fbd85e93ee | ||
|
77f754fa32 | ||
|
01d557abe6 | ||
|
68cc00b6e8 | ||
|
8b7c0be06f | ||
|
473bb666d8 | ||
|
376d407ea1 | ||
|
2e13cedc4b | ||
|
17515ad706 | ||
|
d8b5d6393a | ||
|
d404bc703d | ||
|
e5e08df1d9 | ||
|
1988cba147 | ||
|
4cee7fd27f | ||
|
b75fe02aac | ||
|
83c3dd3418 | ||
|
020850dbbb | ||
|
8d265aa25e | ||
|
c343217fd2 | ||
|
71bc26aefa | ||
|
8a3d259ae6 | ||
|
3a9ae0ebf1 | ||
|
d6b903e6cc | ||
|
b9f7f82e29 | ||
|
6d0bfcf37a | ||
|
e0bbb89d00 | ||
|
a0a58d432a | ||
|
206900fb64 | ||
|
794a5523d1 | ||
|
389c7b670c | ||
|
b1af7bb8f2 | ||
|
157fea73da | ||
|
b439f02b8e | ||
|
710a28c572 | ||
|
702d57b655 | ||
|
34e69556b1 | ||
|
7c411b39ac | ||
|
5a959bc340 | ||
|
fb5f72951d | ||
|
7630136d6c | ||
|
dacb8c597f | ||
|
d99aaaf8dc | ||
|
876831b419 | ||
|
24580609b8 | ||
|
5828abe324 | ||
|
c229d3929a | ||
|
31907ca51d | ||
|
58b8e0ab70 | ||
|
9586a46c9c | ||
|
bb49cf8e65 | ||
|
de1fa61560 | ||
|
07b1fd6f44 | ||
|
d0caa1e1df |
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
*
|
||||
!LICENSE
|
||||
!Setup.hs
|
||||
!ShellCheck.cabal
|
||||
!shellcheck.hs
|
||||
!src
|
27
.github/ISSUE_TEMPLATE.md
vendored
Normal file
27
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
#### For bugs
|
||||
- Rule Id (if any, e.g. SC1000):
|
||||
- My shellcheck version (`shellcheck --version` or "online"):
|
||||
- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
|
||||
- [ ] I tried on https://www.shellcheck.net/ and verified that this is still a problem on the latest commit
|
||||
|
||||
#### For new checks and feature suggestions
|
||||
- [ ] 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
|
||||
|
||||
|
||||
#### Here's a snippet or screenshot that shows the problem:
|
||||
|
||||
```sh
|
||||
|
||||
#!/your/interpreter
|
||||
your script here
|
||||
|
||||
```
|
||||
|
||||
#### Here's what shellcheck currently says:
|
||||
|
||||
|
||||
|
||||
#### Here's what I wanted or expected to see:
|
||||
|
||||
|
131
.github/workflows/build.yml
vendored
Normal file
131
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
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@v2
|
||||
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@v2
|
||||
with:
|
||||
name: source
|
||||
path: source/
|
||||
|
||||
build_source:
|
||||
name: Build Source Code
|
||||
needs: package_source
|
||||
strategy:
|
||||
matrix:
|
||||
build: [linux.x86_64, linux.aarch64, linux.armv6hf, darwin.x86_64, windows.x86_64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
|
||||
- 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@v2
|
||||
with:
|
||||
name: bin
|
||||
path: bin/
|
||||
|
||||
package_binary:
|
||||
name: Package Binaries
|
||||
needs: build_source
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
|
||||
- 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@v2
|
||||
with:
|
||||
name: deploy
|
||||
path: deploy/
|
||||
|
||||
deploy:
|
||||
name: Deploy binaries
|
||||
needs: package_binary
|
||||
runs-on: ubuntu-latest
|
||||
environment: Deploy
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
|
||||
- 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 )
|
28
.github_deploy
Executable file
28
.github_deploy
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
set -x
|
||||
shopt -s extglob
|
||||
|
||||
export EDITOR="touch"
|
||||
|
||||
# Sanity check
|
||||
gh --version || exit 1
|
||||
hub release show latest || exit 1
|
||||
|
||||
for tag in $TAGS
|
||||
do
|
||||
if ! hub release show "$tag"
|
||||
then
|
||||
echo "Creating new release $tag"
|
||||
git show --no-patch --format='format:%B' > description
|
||||
hub release create -F description "$tag"
|
||||
fi
|
||||
|
||||
files=()
|
||||
for file in deploy/*
|
||||
do
|
||||
[[ $file == *.@(xz|gz|zip) ]] || continue
|
||||
[[ $file == *"$tag"* ]] || continue
|
||||
files+=("$file")
|
||||
done
|
||||
gh release upload "$tag" "${files[@]}" --clobber || exit 1
|
||||
done
|
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# Created by https://www.gitignore.io
|
||||
|
||||
### Haskell ###
|
||||
dist
|
||||
cabal-dev
|
||||
*.o
|
||||
*.hi
|
||||
*.chi
|
||||
*.chs.h
|
||||
.virtualenv
|
||||
.hsenv
|
||||
.cabal-sandbox/
|
||||
cabal.sandbox.config
|
||||
cabal.config
|
||||
.stack-work
|
||||
|
||||
### Snap ###
|
||||
/snap/.snapcraft/
|
||||
/stage/
|
||||
/parts/
|
||||
/prime/
|
||||
*.snap
|
106
.multi_arch_docker
Executable file
106
.multi_arch_docker
Executable file
@@ -0,0 +1,106 @@
|
||||
#!/bin/bash
|
||||
# This script builds and deploys multi-architecture docker images from the
|
||||
# binaries previously built and deployed to GitHub.
|
||||
|
||||
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.
|
||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
|
||||
# Instantiate docker buildx builder with multi-architecture support.
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
docker buildx create --name mybuilder
|
||||
docker buildx use mybuilder
|
||||
# Start up buildx and verify that all is OK.
|
||||
docker buildx inspect --bootstrap
|
||||
}
|
||||
|
||||
# Log in to Docker Hub for deployment.
|
||||
function multi_arch_docker::login_to_docker_hub() {
|
||||
echo "$DOCKER_PASSWORD" | docker login -u="$DOCKER_USERNAME" --password-stdin
|
||||
}
|
||||
|
||||
# Run buildx build and push. Passed in arguments augment the command line.
|
||||
function multi_arch_docker::buildx() {
|
||||
mkdir -p /tmp/empty
|
||||
docker buildx build \
|
||||
--platform "${DOCKER_PLATFORMS// /,}" \
|
||||
--push \
|
||||
--progress plain \
|
||||
-f Dockerfile.multi-arch \
|
||||
"$@" \
|
||||
/tmp/empty
|
||||
rmdir /tmp/empty
|
||||
}
|
||||
|
||||
# Build and push plain and alpine docker images for all tags.
|
||||
function multi_arch_docker::build_and_push_all() {
|
||||
for tag in $TAGS; do
|
||||
multi_arch_docker::buildx -t "$DOCKER_BASE:$tag" --build-arg "tag=$tag"
|
||||
multi_arch_docker::buildx -t "$DOCKER_BASE-alpine:$tag" \
|
||||
--build-arg "tag=$tag" --target alpine
|
||||
done
|
||||
}
|
||||
|
||||
# Test all pushed docker images.
|
||||
function multi_arch_docker::test_all() {
|
||||
printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
|
||||
|
||||
for platform in $DOCKER_PLATFORMS; do
|
||||
for tag in $TAGS; do
|
||||
for ext in '-alpine' ''; do
|
||||
image="${DOCKER_BASE}${ext}:${tag}"
|
||||
msg="Testing docker image $image on platform $platform"
|
||||
line="${msg//?/=}"
|
||||
printf '\n%s\n%s\n%s\n' "${line}" "${msg}" "${line}"
|
||||
docker pull -q --platform "$platform" "$image"
|
||||
if [ -n "$ext" ]; then
|
||||
echo -n "Image architecture: "
|
||||
docker run --rm --entrypoint /bin/sh "$image" -c 'uname -m'
|
||||
version=$(docker run --rm "$image" shellcheck --version \
|
||||
| grep 'version:')
|
||||
else
|
||||
version=$(docker run --rm "$image" --version | grep 'version:')
|
||||
fi
|
||||
version=${version/#version: /v}
|
||||
echo "shellcheck version: $version"
|
||||
if [[ ! ("$tag" =~ ^(latest|stable)$) && "$tag" != "$version" ]]; then
|
||||
echo "Version mismatch: shellcheck $version tagged as $tag"
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$ext" ]; then
|
||||
docker run --rm -v "$PWD:/mnt" -w /mnt "$image" shellcheck myscript
|
||||
else
|
||||
docker run --rm -v "$PWD:/mnt" "$image" myscript
|
||||
fi
|
||||
done
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
function multi_arch_docker::main() {
|
||||
export DOCKER_PLATFORMS='linux/amd64'
|
||||
DOCKER_PLATFORMS+=' linux/arm64'
|
||||
DOCKER_PLATFORMS+=' linux/arm/v6'
|
||||
|
||||
multi_arch_docker::install_docker_buildx
|
||||
multi_arch_docker::login_to_docker_hub
|
||||
multi_arch_docker::build_and_push_all
|
||||
multi_arch_docker::test_all
|
||||
}
|
54
.prepare_deploy
Executable file
54
.prepare_deploy
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
# This script packages up compiled binaries
|
||||
set -ex
|
||||
shopt -s nullglob extglob
|
||||
|
||||
ls -l
|
||||
|
||||
cp ../LICENSE LICENSE.txt
|
||||
sed -e $'s/$/\r/' > README.txt << END
|
||||
This is a precompiled ShellCheck binary.
|
||||
https://www.shellcheck.net/
|
||||
|
||||
ShellCheck is a static analysis tool for shell scripts.
|
||||
It's licensed under the GNU General Public License v3.0.
|
||||
Information and source code is available on the website.
|
||||
|
||||
This binary was compiled on $(date -u).
|
||||
|
||||
|
||||
|
||||
====== Latest commits ======
|
||||
|
||||
$(git log -n 3)
|
||||
END
|
||||
|
||||
for dir in */
|
||||
do
|
||||
cp LICENSE.txt README.txt "$dir"
|
||||
done
|
||||
|
||||
echo "Tags are $TAGS"
|
||||
|
||||
for tag in $TAGS
|
||||
do
|
||||
|
||||
for dir in windows.*/
|
||||
do
|
||||
( cd "$dir" && zip "../shellcheck-$tag.zip" * )
|
||||
done
|
||||
|
||||
for dir in {linux,darwin}.*/
|
||||
do
|
||||
base="${dir%/}"
|
||||
( cd "$dir" && tar -cJf "../shellcheck-$tag.$base.tar.xz" --transform="s:^:shellcheck-$tag/:" * )
|
||||
done
|
||||
done
|
||||
|
||||
for file in ./*
|
||||
do
|
||||
[[ -f "$file" ]] || continue
|
||||
sha512sum "$file" > "$file.sha512sum"
|
||||
done
|
||||
|
||||
ls -l
|
14
.snapsquid.conf
Normal file
14
.snapsquid.conf
Normal file
@@ -0,0 +1,14 @@
|
||||
# In 2015, cabal-install had a http bug triggered when proxies didn't keep
|
||||
# the connection open. This version made it into Ubuntu Xenial as used by
|
||||
# Snapcraft. In June 2018, Snapcraft's proxy started triggering this bug.
|
||||
#
|
||||
# https://bugs.launchpad.net/launchpad-buildd/+bug/1797809
|
||||
#
|
||||
# Workaround: add more proxy
|
||||
|
||||
visible_hostname localhost
|
||||
http_port 8888
|
||||
cache_peer 10.10.10.1 parent 8222 0 no-query default
|
||||
cache_peer_domain localhost !.internal
|
||||
http_access allow all
|
||||
|
481
CHANGELOG.md
Normal file
481
CHANGELOG.md
Normal file
@@ -0,0 +1,481 @@
|
||||
## 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
|
||||
### Fixed
|
||||
- `-f diff` no longer claims that it found more issues when it didn't
|
||||
- Known empty variables now correctly trigger SC2086
|
||||
- ShellCheck should now be compatible with Cabal 3
|
||||
- SC2154 and all command-specific checks now trigger for builtins
|
||||
called with `builtin`
|
||||
|
||||
### Added
|
||||
- SC1136: Warn about unexpected characters after ]/]]
|
||||
- SC2254: Suggest quoting expansions in case statements
|
||||
- SC2255: Suggest using `$((..))` in `[ 2*3 -eq 6 ]`
|
||||
- SC2256: Warn about translated strings that are known variables
|
||||
- SC2257: Warn about arithmetic mutation in redirections
|
||||
- SC2258: Warn about trailing commas in for loop elements
|
||||
|
||||
### Changed
|
||||
- SC2230: 'command -v' suggestion is now off by default (-i deprecate-which)
|
||||
- SC1081: Keywords are now correctly parsed case sensitively, with a warning
|
||||
|
||||
|
||||
## v0.7.0 - 2019-07-28
|
||||
### Added
|
||||
- Precompiled binaries for macOS and Linux aarch64
|
||||
- Preliminary support for fix suggestions
|
||||
- New `-f diff` unified diff format for auto-fixes
|
||||
- Files containing Bats tests can now be checked
|
||||
- Directory wide directives can now be placed in a `.shellcheckrc`
|
||||
- Optional checks: Use `--list-optional` to show a list of tests,
|
||||
Enable with `-o` flags or `enable=name` directives
|
||||
- Source paths: Use `-P dir1:dir2` or a `source-path=dir1` directive
|
||||
to specify search paths for sourced files.
|
||||
- json1 format like --format=json but treats tabs as single characters
|
||||
- Recognize FLAGS variables created by the shflags library.
|
||||
- Site-specific changes can now be made in Custom.hs for ease of patching
|
||||
- SC2154: Also warn about unassigned uppercase variables (optional)
|
||||
- SC2252: Warn about `[ $a != x ] || [ $a != y ]`, similar to SC2055
|
||||
- SC2251: Inform about ineffectual ! in front of commands
|
||||
- SC2250: Warn about variable references without braces (optional)
|
||||
- SC2249: Warn about `case` with missing default case (optional)
|
||||
- SC2248: Warn about unquoted variables without special chars (optional)
|
||||
- SC2247: Warn about $"(cmd)" and $"{var}"
|
||||
- SC2246: Warn if a shebang's interpreter ends with /
|
||||
- SC2245: Warn that Ksh ignores all but the first glob result in `[`
|
||||
- SC2243/SC2244: Suggest using explicit -n for `[ $foo ]` (optional)
|
||||
- SC1135: Suggest not ending double quotes just to make $ literal
|
||||
|
||||
### Changed
|
||||
- If a directive or shebang is not specified, a `.bash/.bats/.dash/.ksh`
|
||||
extension will be used to infer the shell type when present.
|
||||
- Disabling SC2120 on a function now disables SC2119 on call sites
|
||||
|
||||
### Fixed
|
||||
- SC2183 no longer warns about missing printf args for `%()T`
|
||||
|
||||
## v0.6.0 - 2018-12-02
|
||||
### Added
|
||||
- Command line option --severity/-S for filtering by minimum severity
|
||||
- Command line option --wiki-link-count/-W for showing wiki links
|
||||
- SC2152/SC2151: Warn about bad `exit` values like `1234` and `"foo"`
|
||||
- SC2236/SC2237: Suggest -n/-z instead of ! -z/-n
|
||||
- SC2238: Warn when redirecting to a known command name, e.g. ls > rm
|
||||
- SC2239: Warn if the shebang is not an absolute path, e.g. #!bin/sh
|
||||
- SC2240: Warn when passing additional arguments to dot (.) in sh/dash
|
||||
- SC1133: Better diagnostics when starting a line with |/||/&&
|
||||
|
||||
### Changed
|
||||
- Most warnings now have useful end positions
|
||||
- SC1117 about unknown double-quoted escape sequences has been retired
|
||||
|
||||
### Fixed
|
||||
- SC2021 no longer triggers for equivalence classes like `[=e=]`
|
||||
- SC2221/SC2222 no longer mistriggers on fall-through case branches
|
||||
- SC2081 about glob matches in `[ .. ]` now also triggers for `!=`
|
||||
- SC2086 no longer warns about spaces in `$#`
|
||||
- SC2164 no longer suggests subshells for `cd ..; cmd; cd ..`
|
||||
- `read -a` is now correctly considered an array assignment
|
||||
- SC2039 no longer warns about LINENO now that it's POSIX
|
||||
|
||||
## v0.5.0 - 2018-05-31
|
||||
### Added
|
||||
- SC2233/SC2234/SC2235: Suggest removing or replacing (..) around tests
|
||||
- SC2232: Warn about invalid arguments to sudo
|
||||
- SC2231: Suggest quoting expansions in for loop globs
|
||||
- SC2229: Warn about 'read $var'
|
||||
- SC2227: Warn about redirections in the middle of 'find' commands
|
||||
- SC2224/SC2225/SC2226: Warn when using mv/cp/ln without a destination
|
||||
- SC2223: Quote warning specific to `: ${var=value}`
|
||||
- SC1131: Warn when using `elseif` or `elsif`
|
||||
- SC1128: Warn about blanks/comments before shebang
|
||||
- SC1127: Warn about C-style comments
|
||||
|
||||
### Fixed
|
||||
- Annotations intended for a command's here documents now work
|
||||
- Escaped characters inside groups in =~ regexes now parse
|
||||
- Associative arrays are now respected in arithmetic contexts
|
||||
- SC1087 about `$var[@]` now correctly triggers on any index
|
||||
- Bad expansions in here documents are no longer ignored
|
||||
- FD move operations like {fd}>1- now parse correctly
|
||||
|
||||
### Changed
|
||||
- Here docs are now terminated as per spec, rather than by presumed intent
|
||||
- SC1073: 'else if' is now parsed correctly and not like 'elif'
|
||||
- SC2163: 'export $name' can now be silenced with 'export ${name?}'
|
||||
- SC2183: Now warns when printf arg count is not a multiple of format count
|
||||
|
||||
## v0.4.7 - 2017-12-08
|
||||
### Added
|
||||
- Statically linked binaries for Linux and Windows (see README.md)!
|
||||
- `-a` flag to also include warnings in `source`d files
|
||||
- SC2221/SC2222: Warn about overridden case branches
|
||||
- SC2220: Warn about unhandled error cases in getopt loops
|
||||
- SC2218: Warn when using functions before they're defined
|
||||
- SC2216/SC2217: Warn when piping/redirecting to mv/cp and other non-readers
|
||||
- SC2215: Warn about commands starting with leading dash
|
||||
- SC2214: Warn about superfluous getopt flags
|
||||
- SC2213: Warn about unhandled getopt flags
|
||||
- SC2212: Suggest `false` over `[ ]`
|
||||
- SC2211: Warn when using a glob as a command name
|
||||
- SC2210: Warn when redirecting to an integer, e.g. `foo 1>2`
|
||||
- SC2206/SC2207: Suggest alternatives when using word splitting in arrays
|
||||
- SC1117: Warn about double quoted, undefined backslash sequences
|
||||
- SC1113/SC1114/SC1115: Recognized more malformed shebangs
|
||||
|
||||
### Fixed
|
||||
- `[ -v foo ]` no longer warns if `foo` is undefined
|
||||
- SC2037 is now suppressed by quotes, e.g. `PAGER="cat" man foo`
|
||||
- Ksh nested array declarations now parse correctly
|
||||
- Parameter Expansion without colons are now recognized, e.g. `${foo+bar}`
|
||||
- The `lastpipe` option is now respected with regard to subshell warnings
|
||||
- `\(` is now respected for grouping in `[`
|
||||
- Leading `\` is now ignored for commands, to allow alias suppression
|
||||
- Comments are now allowed after directives to e.g. explain 'disable'
|
||||
|
||||
|
||||
## v0.4.6 - 2017-03-26
|
||||
### Added
|
||||
- SC2204/SC2205: Warn about `( -z foo )` and `( foo -eq bar )`
|
||||
- SC2200/SC2201: Warn about brace expansion in [/[[
|
||||
- SC2198/SC2199: Warn about arrays in [/[[
|
||||
- SC2196/SC2197: Warn about deprecated egrep/fgrep
|
||||
- SC2195: Warn about unmatchable case branches
|
||||
- SC2194: Warn about constant 'case' statements
|
||||
- SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables
|
||||
- SC2188/SC2189: Warn about redirections without commands
|
||||
- SC2186: Warn about deprecated `tempfile`
|
||||
- SC1109: Warn when finding `&`/`>`/`<` unquoted
|
||||
- SC1108: Warn about missing spaces in `[ var= foo ]`
|
||||
|
||||
### Changed
|
||||
- All files are now read as UTF-8 with lenient latin1 fallback, ignoring locale
|
||||
- Unicode quotes are no longer considered syntactic quotes
|
||||
- `ash` scripts will now be checked as `dash` with a warning
|
||||
|
||||
### Fixed
|
||||
- `-c` no longer suggested when using `grep -o | wc`
|
||||
- Comments and whitespace are now allowed before filewide directives
|
||||
- Here doc delimiters with esoteric quoting like `foo""` are now handled
|
||||
- SC2095 about `ssh` in while read loops is now suppressed when using `-n`
|
||||
- `%(%Y%M%D)T` now recognized as a single formatter in `printf` checks
|
||||
- `grep -F` now suppresses regex related suggestions
|
||||
- Command name checks now recognize busybox applet names
|
||||
|
||||
|
||||
## v0.4.5 - 2016-10-21
|
||||
### Added
|
||||
- A Docker build (thanks, kpankonen!)
|
||||
- SC2185: Suggest explicitly adding path for `find`
|
||||
- SC2184: Warn about unsetting globs (e.g. `unset foo[1]`)
|
||||
- SC2183: Warn about `printf` with more formatters than variables
|
||||
- SC2182: Warn about ignored arguments with `printf`
|
||||
- SC2181: Suggest using command directly instead of `if [ $? -eq 0 ]`
|
||||
- SC1106: Warn when using `test` operators in `(( 1 -eq 2 ))`
|
||||
|
||||
### Changed
|
||||
- Unrecognized directives now causes a warning rather than parse failure.
|
||||
|
||||
### Fixed
|
||||
- Indices in associative arrays are now parsed correctly
|
||||
- Missing shebang warning squashed when specifying with a directive
|
||||
- Ksh multidimensional arrays are now supported
|
||||
- Variables in substring ${a:x:y} expansions now count as referenced
|
||||
- SC1102 now also handles ambiguous `$((`
|
||||
- Using `$(seq ..)` will no longer suggest quoting
|
||||
- SC2148 (missing shebang) is now suppressed when using shell directives
|
||||
- `[ a '>' b ]` is now recognized as being correctly escaped
|
||||
|
||||
|
||||
## v0.4.4 - 2016-05-15
|
||||
### Added
|
||||
- Haskell Stack support (thanks, Arguggi!)
|
||||
- SC2179/SC2178: Warn when assigning/appending strings to arrays
|
||||
- SC1102: Warn about ambiguous `$(((`
|
||||
- SC1101: Warn when \\ linebreaks have trailing spaces
|
||||
|
||||
### Changed
|
||||
- Directives directly after the shebang now apply to the entire file
|
||||
|
||||
### Fixed
|
||||
- `{$i..10}` is now flagged similar to `{1..$i}`
|
||||
|
||||
|
||||
## v0.4.3 - 2016-01-13
|
||||
### Fixed
|
||||
- Build now works on GHC 7.6.3 as found on Debian Stable/Ubuntu LTS
|
||||
|
||||
|
||||
## v0.4.2 - 2016-01-09
|
||||
### Added
|
||||
- First class support for the `dash` shell
|
||||
- The `--color` flag similar to ls/grep's (thanks, haguenau!)
|
||||
- SC2174: Warn about unexpected behavior of `mkdir -pm` (thanks, eatnumber1!)
|
||||
- SC2172: Warn about non-portable use of signal numbers in `trap`
|
||||
- SC2171: Warn about `]]` without leading `[[`
|
||||
- SC2168: Warn about `local` outside functions
|
||||
|
||||
### Fixed
|
||||
- Warnings about unchecked `cd` will no longer trigger with `set -e`
|
||||
- `[ a -nt/-ot/-ef b ]` no longer warns about being constant
|
||||
- Quoted test operators like `[ foo "<" bar ]` now parse
|
||||
- Escaped quotes in backticks now parse correctly
|
||||
|
||||
|
||||
## v0.4.1 - 2015-09-05
|
||||
### Fixed
|
||||
- Added missing files to Cabal, fixing the build
|
||||
|
||||
|
||||
## v0.4.0 - 2015-09-05
|
||||
### Added
|
||||
- Support for following `source`d files
|
||||
- Support for setting default flags in `SHELLCHECK_OPTS`
|
||||
- An `--external-sources` flag for following arbitrary `source`d files
|
||||
- A `source` directive to override the filename to `source`
|
||||
- SC2166: Suggest using `[ p ] && [ q ]` over `[ p -a q ]`
|
||||
- SC2165: Warn when nested `for` loops use the same variable name
|
||||
- SC2164: Warn when using `cd` without checking that it succeeds
|
||||
- SC2163: Warn about `export $var`
|
||||
- SC2162: Warn when using `read` without `-r`
|
||||
- SC2157: Warn about `[ "$var " ]` and similar never-empty string matches
|
||||
|
||||
### Fixed
|
||||
- `cat -vnE file` and similar will no longer flag as UUOC
|
||||
- Nested trinary operators in `(( ))` now parse correctly
|
||||
- Ksh `${ ..; }` command expansions now parse
|
||||
|
||||
|
||||
## v0.3.8 - 2015-06-20
|
||||
### Changed
|
||||
- ShellCheck's license has changed from AGPLv3 to GPLv3.
|
||||
|
||||
### Added
|
||||
- SC2156: Warn about injecting filenames in `find -exec sh -c "{}" \;`
|
||||
|
||||
### Fixed
|
||||
- Variables and command substitutions in brace expansions are now parsed
|
||||
- ANSI colors are now disabled on Windows
|
||||
- Empty scripts now parse
|
||||
|
||||
|
||||
## v0.3.7 - 2015-04-16
|
||||
### Fixed
|
||||
- Build now works on GHC 7.10
|
||||
- Use `regex-tdfa` over `regex-compat` since the latter crashes on OS X.
|
||||
|
||||
## v0.3.6 - 2015-03-28
|
||||
### Added
|
||||
- SC2155: Warn about masked return values in `export foo=$(exit 1)`
|
||||
- SC2154: Warn when a lowercase variable is referenced but not assigned
|
||||
- SC2152/SC2151: Warn about bad `return` values like `1234` and `"foo"`
|
||||
- SC2150: Warn about `find -exec "shell command" \;`
|
||||
|
||||
### Fixed
|
||||
- `coproc` is now supported
|
||||
- Trinary operator now recognized in `((..))`
|
||||
|
||||
### Removed
|
||||
- Zsh support has been removed
|
||||
|
||||
|
||||
## v0.3.5 - 2014-11-09
|
||||
### Added
|
||||
- SC2148: Warn when not including a shebang
|
||||
- SC2147: Warn about literal ~ in PATH
|
||||
- SC1086: Warn about `$` in for loop variables, e.g. `for $i in ..`
|
||||
- SC1084: Warn when the shebang uses `!#` instead of `#!`
|
||||
|
||||
### Fixed
|
||||
- Empty and comment-only backtick expansions now parse
|
||||
- Variables used in PS1/PROMPT\_COMMAND/trap now count as referenced
|
||||
- ShellCheck now skips unreadable files and directories
|
||||
- `-f gcc` on empty files no longer crashes
|
||||
- Variables in $".." are now considered quoted
|
||||
- Warnings about expansions in single quotes now include backticks
|
||||
|
||||
|
||||
## v0.3.4 - 2014-07-08
|
||||
### Added
|
||||
- SC2146: Warn about precedence when combining `find -o` with actions
|
||||
- SC2145: Warn when concatenating arrays and strings
|
||||
|
||||
### Fixed
|
||||
- Case statements now support `;&` and `;;&`
|
||||
- Indices in array declarations now parse correctly
|
||||
- `let` expressions now parsed as arithmetic expressions
|
||||
- Escaping is now respected in here documents
|
||||
|
||||
### Changed
|
||||
- Completely drop Makefile in favor of Cabal (thanks rodrigosetti!)
|
||||
|
||||
|
||||
## v0.3.3 - 2014-05-29
|
||||
### Added
|
||||
- SC2144: Warn when using globs in `[/[[`
|
||||
- SC2143: Suggesting using `grep -q` over `[ "$(.. | grep)" ]`
|
||||
- SC2142: Warn when referencing positional parameters in aliases
|
||||
- SC2141: Warn about suspicious IFS assignments like `IFS="\n"`
|
||||
- SC2140: Warn about bad embedded quotes like `echo "var="value""`
|
||||
- SC2130: Warn when using `-eq` on strings
|
||||
- SC2139: Warn about define time expansions in alias definitions
|
||||
- SC2129: Suggest command grouping over `a >> log; b >> log; c >> log`
|
||||
- SC2128: Warn when expanding arrays without an index
|
||||
- SC2126: Suggest `grep -c` over `grep|wc`
|
||||
- SC2123: Warn about accidentally overriding `$PATH`, e.g. `PATH=/my/dir`
|
||||
- SC1083: Warn about literal `{/}` outside of quotes
|
||||
- SC1082: Warn about UTF-8 BOMs
|
||||
|
||||
### Fixed
|
||||
- SC2051 no longer triggers for `{1,$n}`, only `{1..$n}`
|
||||
- Improved detection of single quoted `sed` variables, e.g. `sed '$s///'`
|
||||
- Stop warning about single quoted variables in `PS1` and similar
|
||||
- Support for Zsh short form loops, `=(..)`
|
||||
|
||||
### Removed
|
||||
- SC1000 about unescaped lonely `$`, e.g. `grep "^foo$"`
|
||||
|
||||
|
||||
## v0.3.2 - 2014-03-22
|
||||
### Added
|
||||
- SC2121: Warn about trying to `set` variables, e.g. `set var = value`
|
||||
- SC2120/SC2119: Warn when a function uses `$1..` if none are ever passed
|
||||
- SC2117: Warn when using `su` in interactive mode, e.g. `su foo; whoami`
|
||||
- SC2116: Detect useless use of echo, e.g. `for i in $(echo $var)`
|
||||
- SC2115/SC2114: Detect some catastrophic `rm -r "$empty/"` mistakes
|
||||
- SC1081: Warn when capitalizing keywords like `While`
|
||||
- SC1077: Warn when using acute accents instead of backticks
|
||||
|
||||
### Fixed
|
||||
- Shells are now properly recognized in shebangs containing flags
|
||||
- Stop warning about math on decimals in ksh/zsh
|
||||
- Stop warning about decimal comparisons with `=`, e.g. `[ $version = 1.2 ]`
|
||||
- Parsing of `|&`
|
||||
- `${a[x]}` not counting as a reference of `x`
|
||||
- `(( x[0] ))` not counting as a reference of `x`
|
||||
|
||||
|
||||
## v0.3.1 - 2014-02-03
|
||||
### Added
|
||||
- The `-s` flag to specify shell dialect
|
||||
- SC2105/SC2104: Warn about `break/continue` outside loops
|
||||
- SC1076: Detect invalid `[/[[` arithmetic like `[ 1 + 2 = 3 ]`
|
||||
- SC1075: Suggest using `elif` over `else if`
|
||||
|
||||
### Fixed
|
||||
- Don't warn when comma separating elements in brace expansions
|
||||
- Improved detection of single quoted `sed` variables, e.g. `sed '$d'`
|
||||
- Parsing of arithmetic for loops using `{..}` instead of `do..done`
|
||||
- Don't treat the last pipeline stage as a subshell in ksh/zsh
|
||||
|
||||
|
||||
## v0.3.0 - 2014-01-19
|
||||
### Added
|
||||
- A man page (thanks Dridi!)
|
||||
- GCC compatible error reporting (`shellcheck -f gcc`)
|
||||
- CheckStyle compatible XML error reporting (`shellcheck -f checkstyle`)
|
||||
- Error codes for each warning, e.g. SC1234
|
||||
- Allow disabling warnings with `# shellcheck disable=SC1234`
|
||||
- Allow disabling warnings with `--exclude`
|
||||
- SC2103: Suggest using subshells over `cd foo; bar; cd ..`
|
||||
- SC2102: Warn about duplicates in char ranges, e.g. `[10-15]`
|
||||
- SC2101: Warn about named classes not inside a char range, e.g. `[:digit:]`
|
||||
- SC2100/SC2099: Warn about bad math expressions like `i=i+5`
|
||||
- SC2098/SC2097: Warn about `foo=bar echo $foo`
|
||||
- SC2095: Warn when using `ssh`/`ffmpeg` in `while read` loops
|
||||
- Better warnings for missing here doc tokens
|
||||
|
||||
### Fixed
|
||||
- Don't warn when single quoting variables with `ssh/perl/eval`
|
||||
- `${!var}` is now counted as a variable reference
|
||||
|
||||
### Removed
|
||||
- Suggestions about using parameter expansion over basename
|
||||
- The `jsoncheck` binary. Use `shellcheck -f json` instead.
|
||||
|
||||
|
||||
## v0.2.0 - 2013-10-27
|
||||
### Added
|
||||
- Suggest `./*` instead of `*` when passing globs to commands
|
||||
- Suggest `pgrep` over `ps | grep`
|
||||
- Warn about unicode quotes
|
||||
- Warn about assigned but unused variables
|
||||
- Inform about client side expansion when using `ssh`
|
||||
|
||||
### Fixed
|
||||
- CLI tool now uses exit codes and stderr canonically
|
||||
- Parsing of extglobs containing empty patterns
|
||||
- Parsing of bash style `eval foo=(bar)`
|
||||
- Parsing of expansions in here documents
|
||||
- Parsing of function names containing :+-
|
||||
- Don't warn about `find|xargs` when using `-print0`
|
||||
|
||||
|
||||
## v0.1.0 - 2013-07-23
|
||||
### Added
|
||||
- First release
|
26
Dockerfile.multi-arch
Normal file
26
Dockerfile.multi-arch
Normal file
@@ -0,0 +1,26 @@
|
||||
# Alpine image
|
||||
FROM alpine:latest AS alpine
|
||||
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
|
||||
ARG tag
|
||||
|
||||
# Put the right binary for each architecture into place for the
|
||||
# multi-architecture docker image.
|
||||
RUN set -x; \
|
||||
arch="$(uname -m)"; \
|
||||
echo "arch is $arch"; \
|
||||
if [ "${arch}" = 'armv7l' ]; then \
|
||||
arch='armv6hf'; \
|
||||
fi; \
|
||||
url_base='https://github.com/koalaman/shellcheck/releases/download/'; \
|
||||
tar_file="${tag}/shellcheck-${tag}.linux.${arch}.tar.xz"; \
|
||||
wget "${url_base}${tar_file}" -O - | tar xJf -; \
|
||||
mv "shellcheck-${tag}/shellcheck" /bin/; \
|
||||
rm -rf "shellcheck-${tag}"; \
|
||||
ls -laF /bin/shellcheck
|
||||
|
||||
# ShellCheck image
|
||||
FROM scratch
|
||||
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
|
||||
WORKDIR /mnt
|
||||
COPY --from=alpine /bin/shellcheck /bin/
|
||||
ENTRYPOINT ["/bin/shellcheck"]
|
157
LICENSE
157
LICENSE
@@ -1,21 +1,33 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
Employer mandated disclaimer:
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
I am providing code in the repository to you under an open source license.
|
||||
Because this is my personal repository, the license you receive to my code is
|
||||
from me and other individual contributors, and not my employer (Facebook).
|
||||
|
||||
- Vidar "koala_man" Holen
|
||||
|
||||
----
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@@ -24,34 +36,44 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@@ -60,7 +82,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@@ -537,45 +559,35 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@@ -633,29 +645,40 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
24
Makefile
24
Makefile
@@ -1,24 +0,0 @@
|
||||
# TODO: Phase out Makefile in favor of Cabal
|
||||
|
||||
GHCFLAGS=-O9
|
||||
|
||||
all: shellcheck jsoncheck .tests
|
||||
: Done
|
||||
|
||||
shellcheck: regardless
|
||||
: Conditionally compiling shellcheck
|
||||
ghc $(GHCFLAGS) --make shellcheck
|
||||
|
||||
jsoncheck: regardless
|
||||
: Conditionally compiling shellcheck
|
||||
ghc $(GHCFLAGS) --make jsoncheck
|
||||
|
||||
.tests: *.hs */*.hs
|
||||
: Running unit tests
|
||||
./test/runQuack && touch .tests
|
||||
|
||||
clean:
|
||||
rm -f .tests shellcheck *.hi *.o ShellCheck/*.hi ShellCheck/*.o
|
||||
|
||||
regardless:
|
||||
|
26
README
26
README
@@ -1,26 +0,0 @@
|
||||
ShellCheck - A shell script static analysis tool
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
|
||||
Copyright 2012, Vidar 'koala_man' Holen
|
||||
Licensed under the GNU Affero General Public License, v3
|
||||
|
||||
The goals of ShellCheck are:
|
||||
|
||||
- To point out and clarify typical beginner's syntax issues,
|
||||
that causes a shell to give cryptic error messages.
|
||||
|
||||
- To point out and clarify typical intermediate level semantic problems,
|
||||
that causes a shell to behave strangely and counter-intuitively.
|
||||
|
||||
- To point out subtle caveats, corner cases and pitfalls, that may cause an
|
||||
advanced user's otherwise working script to fail under future circumstances.
|
||||
|
||||
ShellCheck is written in Haskell, and requires GHC, Parsec3 and Text.Regex.
|
||||
To build the JSON interface and run the unit tests, it also requires QuickCheck2 and JSON.
|
||||
|
||||
On Ubuntu and similar, these are called:
|
||||
ghc6 libghc6-parsec3-dev libghc6-quickcheck2-dev libghc6-json-dev libghc-regex-compat-dev
|
||||
|
||||
Executables can be built with cabal. Tests currently still rely on a Makefile.
|
||||
|
||||
Happy ShellChecking!
|
551
README.md
Normal file
551
README.md
Normal file
@@ -0,0 +1,551 @@
|
||||
[](https://github.com/koalaman/shellcheck/actions/workflows/build.yml)
|
||||
|
||||
|
||||
# ShellCheck - A shell script static analysis tool
|
||||
|
||||
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts:
|
||||
|
||||

|
||||
|
||||
The goals of ShellCheck are
|
||||
|
||||
* To point out and clarify typical beginner's syntax issues that cause a shell
|
||||
to give cryptic error messages.
|
||||
|
||||
* To point out and clarify typical intermediate level semantic problems that
|
||||
cause a shell to behave strangely and counter-intuitively.
|
||||
|
||||
* To point out subtle caveats, corner cases and pitfalls that may cause an
|
||||
advanced user's otherwise working script to fail under future circumstances.
|
||||
|
||||
See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for examples of what ShellCheck can help you identify!
|
||||
|
||||
## Table of Contents
|
||||
|
||||
* [How to use](#how-to-use)
|
||||
* [On the web](#on-the-web)
|
||||
* [From your terminal](#from-your-terminal)
|
||||
* [In your editor](#in-your-editor)
|
||||
* [In your build or test suites](#in-your-build-or-test-suites)
|
||||
* [Installing](#installing)
|
||||
* [Compiling from source](#compiling-from-source)
|
||||
* [Installing Cabal](#installing-cabal)
|
||||
* [Compiling ShellCheck](#compiling-shellcheck)
|
||||
* [Running tests](#running-tests)
|
||||
* [Gallery of bad code](#gallery-of-bad-code)
|
||||
* [Quoting](#quoting)
|
||||
* [Conditionals](#conditionals)
|
||||
* [Frequently misused commands](#frequently-misused-commands)
|
||||
* [Common beginner's mistakes](#common-beginners-mistakes)
|
||||
* [Style](#style)
|
||||
* [Data and typing errors](#data-and-typing-errors)
|
||||
* [Robustness](#robustness)
|
||||
* [Portability](#portability)
|
||||
* [Miscellaneous](#miscellaneous)
|
||||
* [Testimonials](#testimonials)
|
||||
* [Ignoring issues](#ignoring-issues)
|
||||
* [Reporting bugs](#reporting-bugs)
|
||||
* [Contributing](#contributing)
|
||||
* [Copyright](#copyright)
|
||||
* [Other Resources](#other-resources)
|
||||
|
||||
## How to use
|
||||
|
||||
There are a number of ways to use ShellCheck!
|
||||
|
||||
### On the web
|
||||
|
||||
Paste a shell script on <https://www.shellcheck.net> for instant feedback.
|
||||
|
||||
[ShellCheck.net](https://www.shellcheck.net) is always synchronized to the latest git commit, and is the easiest way to give ShellCheck a go. Tell your friends!
|
||||
|
||||
### From your terminal
|
||||
|
||||
Run `shellcheck yourscript` in your terminal for instant output, as seen above.
|
||||
|
||||
### In your editor
|
||||
|
||||
You can see ShellCheck suggestions directly in a variety of editors.
|
||||
|
||||
* Vim, through [ALE](https://github.com/w0rp/ale), [Neomake](https://github.com/neomake/neomake), or [Syntastic](https://github.com/scrooloose/syntastic):
|
||||
|
||||
.
|
||||
|
||||
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck) or [Flymake](https://github.com/federicotdn/flymake-shellcheck):
|
||||
|
||||
.
|
||||
|
||||
* Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck).
|
||||
|
||||
* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
|
||||
|
||||
* VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck).
|
||||
|
||||
* Most other editors, through [GCC error compatibility](shellcheck.1.md#user-content-formats).
|
||||
|
||||
### In your build or test suites
|
||||
|
||||
While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites.
|
||||
It makes canonical use of exit codes, so you can just add a `shellcheck` command as part of the process.
|
||||
|
||||
For example, in a Makefile:
|
||||
|
||||
```Makefile
|
||||
check-scripts:
|
||||
# Fail if any of these files have warnings
|
||||
shellcheck myscripts/*.sh
|
||||
```
|
||||
|
||||
or in a Travis CI `.travis.yml` file:
|
||||
|
||||
```yaml
|
||||
script:
|
||||
# Fail if any of these files have warnings
|
||||
- shellcheck myscripts/*.sh
|
||||
```
|
||||
|
||||
Services and platforms that have ShellCheck pre-installed and ready to use:
|
||||
|
||||
* [Travis CI](https://travis-ci.org/)
|
||||
* [Codacy](https://www.codacy.com/)
|
||||
* [Code Climate](https://codeclimate.com/)
|
||||
* [Code Factor](https://www.codefactor.io/)
|
||||
* [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck)
|
||||
* [Github](https://github.com/features/actions) (only Linux)
|
||||
|
||||
Most other services, including [GitLab](https://about.gitlab.com/), let you install
|
||||
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
|
||||
or by downloading and unpacking a [binary release](#installing-a-pre-compiled-binary).
|
||||
|
||||
It's a good idea to manually install a specific ShellCheck version regardless. This avoids
|
||||
any surprise build breaks when a new version with new warnings is published.
|
||||
|
||||
For customized filtering or reporting, ShellCheck can output simple JSON, CheckStyle compatible XML,
|
||||
GCC compatible warnings as well as human readable text (with or without ANSI colors). See the
|
||||
[Integration](https://github.com/koalaman/shellcheck/wiki/Integration) wiki page for more documentation.
|
||||
|
||||
## Installing
|
||||
|
||||
The easiest way to install ShellCheck locally is through your package manager.
|
||||
|
||||
On systems with Cabal (installs to `~/.cabal/bin`):
|
||||
|
||||
cabal update
|
||||
cabal install ShellCheck
|
||||
|
||||
On systems with Stack (installs to `~/.local/bin`):
|
||||
|
||||
stack update
|
||||
stack install ShellCheck
|
||||
|
||||
On Debian based distros:
|
||||
|
||||
sudo apt install shellcheck
|
||||
|
||||
On Arch Linux based distros:
|
||||
|
||||
pacman -S shellcheck
|
||||
|
||||
or get the dependency free [shellcheck-bin](https://aur.archlinux.org/packages/shellcheck-bin/) from the AUR.
|
||||
|
||||
On Gentoo based distros:
|
||||
|
||||
emerge --ask shellcheck
|
||||
|
||||
On EPEL based distros:
|
||||
|
||||
sudo yum -y install epel-release
|
||||
sudo yum install ShellCheck
|
||||
|
||||
On Fedora based distros:
|
||||
|
||||
dnf install ShellCheck
|
||||
|
||||
On FreeBSD:
|
||||
|
||||
pkg install hs-ShellCheck
|
||||
|
||||
On macOS (OS X) with Homebrew:
|
||||
|
||||
brew install shellcheck
|
||||
|
||||
Or with MacPorts:
|
||||
|
||||
sudo port install shellcheck
|
||||
|
||||
On OpenBSD:
|
||||
|
||||
pkg_add shellcheck
|
||||
|
||||
On openSUSE
|
||||
|
||||
zypper in ShellCheck
|
||||
|
||||
Or use OneClickInstall - <https://software.opensuse.org/package/ShellCheck>
|
||||
|
||||
On Solus:
|
||||
|
||||
eopkg install shellcheck
|
||||
|
||||
On Windows (via [chocolatey](https://chocolatey.org/packages/shellcheck)):
|
||||
|
||||
```cmd
|
||||
C:\> choco install shellcheck
|
||||
```
|
||||
|
||||
Or Windows (via [scoop](http://scoop.sh)):
|
||||
|
||||
```cmd
|
||||
C:\> scoop install shellcheck
|
||||
```
|
||||
|
||||
From [conda-forge](https://anaconda.org/conda-forge/shellcheck):
|
||||
|
||||
conda install -c conda-forge shellcheck
|
||||
|
||||
From Snap Store:
|
||||
|
||||
snap install --channel=edge shellcheck
|
||||
|
||||
From Docker Hub:
|
||||
|
||||
```sh
|
||||
docker run --rm -v "$PWD:/mnt" koalaman/shellcheck:stable myscript
|
||||
# Or :v0.4.7 for that version, or :latest for daily builds
|
||||
```
|
||||
|
||||
or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend. It works exactly like a regular Alpine image, but has shellcheck preinstalled.
|
||||
|
||||
Using the [nix package manager](https://nixos.org/nix):
|
||||
```sh
|
||||
nix-env -iA nixpkgs.shellcheck
|
||||
```
|
||||
|
||||
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, 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)
|
||||
* [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)
|
||||
|
||||
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).
|
||||
|
||||
Distro packages already come with a `man` page. If you are building from source, it can be installed with:
|
||||
|
||||
```console
|
||||
pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1
|
||||
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 has now integrated ShellCheck by default, so you don't need to manually install it.
|
||||
|
||||
If you still want to do so in order to upgrade at your leisure or ensure you're
|
||||
using the latest release, follow the steps below to install a binary version.
|
||||
|
||||
### Installing a pre-compiled binary
|
||||
|
||||
The pre-compiled binaries come in `tar.xz` files. To decompress them, make sure
|
||||
`xz` is installed.
|
||||
On Debian/Ubuntu/Mint, you can `apt install xz-utils`.
|
||||
On Redhat/Fedora/CentOS, `yum -y install xz`.
|
||||
|
||||
A simple installer may do something like:
|
||||
|
||||
```bash
|
||||
scversion="stable" # or "v0.4.7", or "latest"
|
||||
wget -qO- "https://github.com/koalaman/shellcheck/releases/download/${scversion?}/shellcheck-${scversion?}.linux.x86_64.tar.xz" | tar -xJv
|
||||
cp "shellcheck-${scversion}/shellcheck" /usr/bin/
|
||||
shellcheck --version
|
||||
```
|
||||
|
||||
## Compiling from source
|
||||
|
||||
This section describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile.
|
||||
|
||||
### Installing Cabal
|
||||
|
||||
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.
|
||||
|
||||
$ brew install cabal-install
|
||||
|
||||
On MacPorts, the package is instead called `hs-cabal-install`, while native Windows users should install the latest version of the Haskell platform from <https://www.haskell.org/platform/>
|
||||
|
||||
Verify that `cabal` is installed and update its dependency list with
|
||||
|
||||
$ cabal update
|
||||
|
||||
### Compiling ShellCheck
|
||||
|
||||
`git clone` this repository, and `cd` to the ShellCheck source directory to build/install:
|
||||
|
||||
$ cabal install
|
||||
|
||||
Or if you intend to run the tests:
|
||||
|
||||
$ cabal install --enable-tests
|
||||
|
||||
This will compile ShellCheck and install it to your `~/.cabal/bin` directory.
|
||||
|
||||
Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
|
||||
|
||||
```sh
|
||||
export PATH="$HOME/.cabal/bin:$PATH"
|
||||
```
|
||||
|
||||
Log out and in again, and verify that your PATH is set up correctly:
|
||||
|
||||
```sh
|
||||
$ which shellcheck
|
||||
~/.cabal/bin/shellcheck
|
||||
```
|
||||
|
||||
On native Windows, the `PATH` should already be set up, but the system
|
||||
may use a legacy codepage. In `cmd.exe`, `powershell.exe` and Powershell ISE,
|
||||
make sure to use a TrueType font, not a Raster font, and set the active
|
||||
codepage to UTF-8 (65001) with `chcp`:
|
||||
|
||||
```cmd
|
||||
chcp 65001
|
||||
```
|
||||
|
||||
In Powershell ISE, you may need to additionally update the output encoding:
|
||||
|
||||
```powershell
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
```
|
||||
|
||||
### Running tests
|
||||
|
||||
To run the unit test suite:
|
||||
|
||||
$ cabal test
|
||||
|
||||
## Gallery of bad code
|
||||
|
||||
So what kind of things does ShellCheck look for? Here is an incomplete list of detected issues.
|
||||
|
||||
### Quoting
|
||||
|
||||
ShellCheck can recognize several types of incorrect quoting:
|
||||
|
||||
```sh
|
||||
echo $1 # Unquoted variables
|
||||
find . -name *.ogg # Unquoted find/grep patterns
|
||||
rm "~/my file.txt" # Quoted tilde expansion
|
||||
v='--verbose="true"'; cmd $v # Literal quotes in variables
|
||||
for f in "*.ogg" # Incorrectly quoted 'for' loops
|
||||
touch $@ # Unquoted $@
|
||||
echo 'Don't forget to restart!' # Singlequote closed by apostrophe
|
||||
echo 'Don\'t try this at home' # Attempting to escape ' in ''
|
||||
echo 'Path is $PATH' # Variables in single quotes
|
||||
trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap
|
||||
unset var[i] # Array index treated as glob
|
||||
```
|
||||
|
||||
### Conditionals
|
||||
|
||||
ShellCheck can recognize many types of incorrect test statements.
|
||||
|
||||
```sh
|
||||
[[ n != 0 ]] # Constant test expressions
|
||||
[[ -e *.mpg ]] # Existence checks of globs
|
||||
[[ $foo==0 ]] # Always true due to missing spaces
|
||||
[[ -n "$foo " ]] # Always true due to literals
|
||||
[[ $foo =~ "fo+" ]] # Quoted regex in =~
|
||||
[ foo =~ re ] # Unsupported [ ] operators
|
||||
[ $1 -eq "shellcheck" ] # Numerical comparison of strings
|
||||
[ $n && $m ] # && in [ .. ]
|
||||
[ grep -q foo file ] # Command without $(..)
|
||||
[[ "$$file" == *.jpg ]] # Comparisons that can't succeed
|
||||
(( 1 -lt 2 )) # Using test operators in ((..))
|
||||
[ x ] & [ y ] | [ z ] # Accidental backgrounding and piping
|
||||
```
|
||||
|
||||
### Frequently misused commands
|
||||
|
||||
ShellCheck can recognize instances where commands are used incorrectly:
|
||||
|
||||
```sh
|
||||
grep '*foo*' file # Globs in regex contexts
|
||||
find . -exec foo {} && bar {} \; # Prematurely terminated find -exec
|
||||
sudo echo 'Var=42' > /etc/profile # Redirecting sudo
|
||||
time --format=%s sleep 10 # Passing time(1) flags to time builtin
|
||||
while read h; do ssh "$h" uptime # Commands eating while loop input
|
||||
alias archive='mv $1 /backup' # Defining aliases with arguments
|
||||
tr -cd '[a-zA-Z0-9]' # [] around ranges in tr
|
||||
exec foo; echo "Done!" # Misused 'exec'
|
||||
find -name \*.bak -o -name \*~ -delete # Implicit precedence in find
|
||||
# find . -exec foo > bar \; # Redirections in find
|
||||
f() { whoami; }; sudo f # External use of internal functions
|
||||
```
|
||||
|
||||
### Common beginner's mistakes
|
||||
|
||||
ShellCheck recognizes many common beginner's syntax errors:
|
||||
|
||||
```sh
|
||||
var = 42 # Spaces around = in assignments
|
||||
$foo=42 # $ in assignments
|
||||
for $var in *; do ... # $ in for loop variables
|
||||
var$n="Hello" # Wrong indirect assignment
|
||||
echo ${var$n} # Wrong indirect reference
|
||||
var=(1, 2, 3) # Comma separated arrays
|
||||
array=( [index] = value ) # Incorrect index initialization
|
||||
echo $var[14] # Missing {} in array references
|
||||
echo "Argument 10 is $10" # Positional parameter misreference
|
||||
if $(myfunction); then ..; fi # Wrapping commands in $()
|
||||
else if othercondition; then .. # Using 'else if'
|
||||
f; f() { echo "hello world; } # Using function before definition
|
||||
[ false ] # 'false' being true
|
||||
if ( -f file ) # Using (..) instead of test
|
||||
```
|
||||
|
||||
### Style
|
||||
|
||||
ShellCheck can make suggestions to improve style:
|
||||
|
||||
```sh
|
||||
[[ -z $(find /tmp | grep mpg) ]] # Use grep -q instead
|
||||
a >> log; b >> log; c >> log # Use a redirection block instead
|
||||
echo "The time is `date`" # Use $() instead
|
||||
cd dir; process *; cd ..; # Use subshells instead
|
||||
echo $[1+2] # Use standard $((..)) instead of old $[]
|
||||
echo $(($RANDOM % 6)) # Don't use $ on variables in $((..))
|
||||
echo "$(date)" # Useless use of echo
|
||||
cat file | grep foo # Useless use of cat
|
||||
```
|
||||
|
||||
### Data and typing errors
|
||||
|
||||
ShellCheck can recognize issues related to data and typing:
|
||||
|
||||
```sh
|
||||
args="$@" # Assigning arrays to strings
|
||||
files=(foo bar); echo "$files" # Referencing arrays as strings
|
||||
declare -A arr=(foo bar) # Associative arrays without index
|
||||
printf "%s\n" "Arguments: $@." # Concatenating strings and arrays
|
||||
[[ $# > 2 ]] # Comparing numbers as strings
|
||||
var=World; echo "Hello " var # Unused lowercase variables
|
||||
echo "Hello $name" # Unassigned lowercase variables
|
||||
cmd | read bar; echo $bar # Assignments in subshells
|
||||
cat foo | cp bar # Piping to commands that don't read
|
||||
printf '%s: %s\n' foo # Mismatches in printf argument count
|
||||
eval "${array[@]}" # Lost word boundaries in array eval
|
||||
for i in "${x[@]}"; do ${x[$i]} # Using array value as key
|
||||
```
|
||||
|
||||
### Robustness
|
||||
|
||||
ShellCheck can make suggestions for improving the robustness of a script:
|
||||
|
||||
```sh
|
||||
rm -rf "$STEAMROOT/"* # Catastrophic rm
|
||||
touch ./-l; ls * # Globs that could become options
|
||||
find . -exec sh -c 'a && b {}' \; # Find -exec shell injection
|
||||
printf "Hello $name" # Variables in printf format
|
||||
for f in $(ls *.txt); do # Iterating over ls output
|
||||
export MYVAR=$(cmd) # Masked exit codes
|
||||
case $version in 2.*) :;; 2.6.*) # Shadowed case branches
|
||||
```
|
||||
|
||||
### Portability
|
||||
|
||||
ShellCheck will warn when using features not supported by the shebang. For example, if you set the shebang to `#!/bin/sh`, ShellCheck will warn about portability issues similar to `checkbashisms`:
|
||||
|
||||
```sh
|
||||
echo {1..$n} # Works in ksh, but not bash/dash/sh
|
||||
echo {1..10} # Works in ksh and bash, but not dash/sh
|
||||
echo -n 42 # Works in ksh, bash and dash, undefined in sh
|
||||
expr match str regex # Unportable alias for `expr str : regex`
|
||||
trap 'exit 42' sigint # Unportable signal spec
|
||||
cmd &> file # Unportable redirection operator
|
||||
read foo < /dev/tcp/host/22 # Unportable intercepted files
|
||||
foo-bar() { ..; } # Undefined/unsupported function name
|
||||
[ $UID = 0 ] # Variable undefined in dash/sh
|
||||
local var=value # local is undefined in sh
|
||||
time sleep 1 | sleep 5 # Undefined uses of 'time'
|
||||
```
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
ShellCheck recognizes a menagerie of other issues:
|
||||
|
||||
```sh
|
||||
PS1='\e[0;32m\$\e[0m ' # PS1 colors not in \[..\]
|
||||
PATH="$PATH:~/bin" # Literal tilde in $PATH
|
||||
rm “file” # Unicode quotes
|
||||
echo "Hello world" # Carriage return / DOS line endings
|
||||
echo hello \ # Trailing spaces after \
|
||||
var=42 echo $var # Expansion of inlined environment
|
||||
!# bin/bash -x -e # Common shebang errors
|
||||
echo $((n/180*100)) # Unnecessary loss of precision
|
||||
ls *[:digit:].txt # Bad character class globs
|
||||
sed 's/foo/bar/' file > file # Redirecting to input
|
||||
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
|
||||
```
|
||||
|
||||
## Testimonials
|
||||
|
||||
> At first you're like "shellcheck is awesome" but then you're like "wtf are we still using bash"
|
||||
|
||||
Alexander Tarasikov,
|
||||
[via Twitter](https://twitter.com/astarasikov/status/568825996532707330)
|
||||
|
||||
## Ignoring issues
|
||||
|
||||
Issues can be ignored via environmental variable, command line, individually or globally within a file:
|
||||
|
||||
<https://github.com/koalaman/shellcheck/wiki/Ignore>
|
||||
|
||||
## Reporting bugs
|
||||
|
||||
Please use the GitHub issue tracker for any bugs or feature suggestions:
|
||||
|
||||
<https://github.com/koalaman/shellcheck/issues>
|
||||
|
||||
## Contributing
|
||||
|
||||
Please submit patches to code or documentation as GitHub pull requests! Check
|
||||
out the [DevGuide](https://github.com/koalaman/shellcheck/wiki/DevGuide) on the
|
||||
ShellCheck Wiki.
|
||||
|
||||
Contributions must be licensed under the GNU GPLv3.
|
||||
The contributor retains the copyright.
|
||||
|
||||
## Copyright
|
||||
|
||||
ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE).
|
||||
|
||||
Copyright 2012-2019, [Vidar 'koala_man' Holen](https://github.com/koalaman/) and contributors.
|
||||
|
||||
Happy ShellChecking!
|
||||
|
||||
## Other Resources
|
||||
|
||||
* The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221).
|
||||
* ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)!
|
||||
|
128
ShellCheck.cabal
128
ShellCheck.cabal
@@ -1,20 +1,130 @@
|
||||
Name: ShellCheck
|
||||
Version: 0.1.0
|
||||
Description: Shell script analysis tool
|
||||
Version: 0.8.0
|
||||
Synopsis: Shell script analysis tool
|
||||
License: GPL-3
|
||||
License-file: LICENSE
|
||||
Category: Static Analysis
|
||||
Author: Vidar Holen
|
||||
Maintainer: vidar@vidarholen.net
|
||||
Homepage: http://www.shellcheck.net/
|
||||
Homepage: https://www.shellcheck.net/
|
||||
Build-Type: Simple
|
||||
Cabal-Version: >= 1.2
|
||||
Cabal-Version: 1.18
|
||||
Bug-reports: https://github.com/koalaman/shellcheck/issues
|
||||
Description:
|
||||
The goals of ShellCheck are:
|
||||
.
|
||||
* To point out and clarify typical beginner's syntax issues,
|
||||
that causes a shell to give cryptic error messages.
|
||||
.
|
||||
* To point out and clarify typical intermediate level semantic problems,
|
||||
that causes a shell to behave strangely and counter-intuitively.
|
||||
.
|
||||
* To point out subtle caveats, corner cases and pitfalls, that may cause an
|
||||
advanced user's otherwise working script to fail under future circumstances.
|
||||
|
||||
Extra-Doc-Files:
|
||||
README.md
|
||||
CHANGELOG.md
|
||||
Extra-Source-Files:
|
||||
-- documentation
|
||||
shellcheck.1.md
|
||||
-- A script to build the man page using pandoc
|
||||
manpage
|
||||
-- convenience script for stripping tests
|
||||
striptests
|
||||
-- tests
|
||||
test/shellcheck.hs
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: git://github.com/koalaman/shellcheck.git
|
||||
|
||||
library
|
||||
build-depends: base >= 4, parsec, containers, regex-compat, mtl, directory
|
||||
exposed-modules: ShellCheck.AST, ShellCheck.Parser, ShellCheck.Analytics, ShellCheck.Simple
|
||||
hs-source-dirs: src
|
||||
if impl(ghc < 8.0)
|
||||
build-depends:
|
||||
semigroups
|
||||
build-depends:
|
||||
aeson,
|
||||
array,
|
||||
base >= 4.8.0.0 && < 5,
|
||||
bytestring,
|
||||
containers >= 0.5,
|
||||
deepseq >= 1.4.0.0,
|
||||
Diff >= 0.2.0,
|
||||
directory >= 1.2.3.0,
|
||||
mtl >= 2.2.1,
|
||||
filepath,
|
||||
parsec,
|
||||
regex-tdfa,
|
||||
QuickCheck >= 2.7.4,
|
||||
-- When cabal supports it, move this to setup-depends:
|
||||
process
|
||||
exposed-modules:
|
||||
ShellCheck.AST
|
||||
ShellCheck.ASTLib
|
||||
ShellCheck.Analytics
|
||||
ShellCheck.Analyzer
|
||||
ShellCheck.AnalyzerLib
|
||||
ShellCheck.Checker
|
||||
ShellCheck.Checks.Commands
|
||||
ShellCheck.Checks.Custom
|
||||
ShellCheck.Checks.ShellSupport
|
||||
ShellCheck.Data
|
||||
ShellCheck.Fixer
|
||||
ShellCheck.Formatter.Format
|
||||
ShellCheck.Formatter.CheckStyle
|
||||
ShellCheck.Formatter.Diff
|
||||
ShellCheck.Formatter.GCC
|
||||
ShellCheck.Formatter.JSON
|
||||
ShellCheck.Formatter.JSON1
|
||||
ShellCheck.Formatter.TTY
|
||||
ShellCheck.Formatter.Quiet
|
||||
ShellCheck.Interface
|
||||
ShellCheck.Parser
|
||||
ShellCheck.Regex
|
||||
other-modules:
|
||||
Paths_ShellCheck
|
||||
default-language: Haskell98
|
||||
|
||||
executable shellcheck
|
||||
if impl(ghc < 8.0)
|
||||
build-depends:
|
||||
semigroups
|
||||
build-depends:
|
||||
aeson,
|
||||
array,
|
||||
base >= 4 && < 5,
|
||||
bytestring,
|
||||
containers,
|
||||
deepseq >= 1.4.0.0,
|
||||
Diff >= 0.2.0,
|
||||
directory >= 1.2.3.0,
|
||||
mtl >= 2.2.1,
|
||||
filepath,
|
||||
parsec >= 3.0,
|
||||
QuickCheck >= 2.7.4,
|
||||
regex-tdfa,
|
||||
ShellCheck
|
||||
default-language: Haskell98
|
||||
main-is: shellcheck.hs
|
||||
|
||||
executable jsoncheck
|
||||
build-depends: json
|
||||
main-is: jsoncheck.hs
|
||||
test-suite test-shellcheck
|
||||
type: exitcode-stdio-1.0
|
||||
build-depends:
|
||||
aeson,
|
||||
array,
|
||||
base >= 4 && < 5,
|
||||
bytestring,
|
||||
containers,
|
||||
deepseq >= 1.4.0.0,
|
||||
Diff >= 0.2.0,
|
||||
directory >= 1.2.3.0,
|
||||
mtl >= 2.2.1,
|
||||
filepath,
|
||||
parsec,
|
||||
QuickCheck >= 2.7.4,
|
||||
regex-tdfa,
|
||||
ShellCheck
|
||||
default-language: Haskell98
|
||||
main-is: test/shellcheck.hs
|
||||
|
@@ -1,327 +0,0 @@
|
||||
{-
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.AST where
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.Identity
|
||||
import qualified Text.Regex as Re
|
||||
|
||||
data Id = Id Int deriving (Show, Eq, Ord)
|
||||
|
||||
data Token =
|
||||
TA_Base Id String Token
|
||||
| TA_Binary Id String Token Token
|
||||
| TA_Expansion Id Token
|
||||
| TA_Literal Id String
|
||||
| TA_Sequence Id [Token]
|
||||
| TA_Trinary Id Token Token Token
|
||||
| TA_Unary Id String Token
|
||||
| TA_Variable Id String
|
||||
| TC_And Id ConditionType String Token Token
|
||||
| TC_Binary Id ConditionType String Token Token
|
||||
| TC_Group Id ConditionType Token
|
||||
| TC_Noary Id ConditionType Token
|
||||
| TC_Or Id ConditionType String Token Token
|
||||
| TC_Unary Id ConditionType String Token
|
||||
| T_AND_IF Id
|
||||
| T_AndIf Id (Token) (Token)
|
||||
| T_Arithmetic Id Token
|
||||
| T_Array Id [Token]
|
||||
| T_Assignment Id String Token
|
||||
| T_Backgrounded Id Token
|
||||
| T_Backticked Id [Token]
|
||||
| T_Bang Id
|
||||
| T_Banged Id Token
|
||||
| T_BraceExpansion Id String
|
||||
| T_BraceGroup Id [Token]
|
||||
| T_CLOBBER Id
|
||||
| T_Case Id
|
||||
| T_CaseExpression Id Token [([Token],[Token])]
|
||||
| T_Condition Id ConditionType Token
|
||||
| T_DGREAT Id
|
||||
| T_DLESS Id
|
||||
| T_DLESSDASH Id
|
||||
| T_DSEMI Id
|
||||
| T_Do Id
|
||||
| T_DollarArithmetic Id Token
|
||||
| T_DollarBraced Id Token
|
||||
| T_DollarBracket Id Token
|
||||
| T_DollarDoubleQuoted Id [Token]
|
||||
| T_DollarExpansion Id [Token]
|
||||
| T_DollarSingleQuoted Id String
|
||||
| T_Done Id
|
||||
| T_DoubleQuoted Id [Token]
|
||||
| T_EOF Id
|
||||
| T_Elif Id
|
||||
| T_Else Id
|
||||
| T_Esac Id
|
||||
| T_Extglob Id String [Token]
|
||||
| T_FdRedirect Id String Token
|
||||
| T_Fi Id
|
||||
| T_For Id
|
||||
| T_ForArithmetic Id Token Token Token [Token]
|
||||
| T_ForIn Id String [Token] [Token]
|
||||
| T_Function Id String Token
|
||||
| T_GREATAND Id
|
||||
| T_Glob Id String
|
||||
| T_Greater Id
|
||||
| T_HereDoc Id Bool Bool String
|
||||
| T_HereString Id Token
|
||||
| T_If Id
|
||||
| T_IfExpression Id [([Token],[Token])] [Token]
|
||||
| T_In Id
|
||||
| T_IoFile Id Token Token
|
||||
| T_LESSAND Id
|
||||
| T_LESSGREAT Id
|
||||
| T_Lbrace Id
|
||||
| T_Less Id
|
||||
| T_Literal Id String
|
||||
| T_Lparen Id
|
||||
| T_NEWLINE Id
|
||||
| T_NormalWord Id [Token]
|
||||
| T_OR_IF Id
|
||||
| T_OrIf Id (Token) (Token)
|
||||
| T_Pipeline Id [Token]
|
||||
| T_ProcSub Id String [Token]
|
||||
| T_Rbrace Id
|
||||
| T_Redirecting Id [Token] Token
|
||||
| T_Rparen Id
|
||||
| T_Script Id String [Token]
|
||||
| T_Select Id
|
||||
| T_SelectIn Id String [Token] [Token]
|
||||
| T_Semi Id
|
||||
| T_SimpleCommand Id [Token] [Token]
|
||||
| T_SingleQuoted Id String
|
||||
| T_Subshell Id [Token]
|
||||
| T_Then Id
|
||||
| T_Until Id
|
||||
| T_UntilExpression Id [Token] [Token]
|
||||
| T_While Id
|
||||
| T_WhileExpression Id [Token] [Token]
|
||||
deriving (Show)
|
||||
|
||||
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
|
||||
|
||||
-- I apologize for nothing!
|
||||
lolHax s = Re.subRegex (Re.mkRegex "(Id [0-9]+)") (show s) "(Id 0)"
|
||||
instance Eq Token where
|
||||
(==) a b = (lolHax a) == (lolHax b)
|
||||
|
||||
|
||||
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> Token) -> Token -> m Token
|
||||
analyze f g i t =
|
||||
round t
|
||||
where
|
||||
round t = do
|
||||
f t
|
||||
newT <- delve t
|
||||
g t
|
||||
return . i $ newT
|
||||
roundAll = mapM round
|
||||
|
||||
dl l v = do
|
||||
x <- roundAll l
|
||||
return $ v x
|
||||
dll l m v = do
|
||||
x <- roundAll l
|
||||
y <- roundAll m
|
||||
return $ v x m
|
||||
d1 t v = do
|
||||
x <- round t
|
||||
return $ v x
|
||||
d2 t1 t2 v = do
|
||||
x <- round t1
|
||||
y <- round t2
|
||||
return $ v x y
|
||||
|
||||
delve (T_NormalWord id list) = dl list $ T_NormalWord id
|
||||
delve (T_DoubleQuoted id list) = dl list $ T_DoubleQuoted id
|
||||
delve (T_DollarDoubleQuoted id list) = dl list $ T_DollarDoubleQuoted id
|
||||
delve (T_DollarExpansion id list) = dl list $ T_DollarExpansion id
|
||||
delve (T_Backticked id list) = dl list $ T_Backticked id
|
||||
delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id
|
||||
delve (T_DollarBracket id c) = d1 c $ T_DollarBracket id
|
||||
delve (T_IoFile id op file) = d2 op file $ T_IoFile id
|
||||
delve (T_HereString id word) = d1 word $ T_HereString id
|
||||
delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v
|
||||
delve (T_Assignment id v t) = d1 t $ T_Assignment id v
|
||||
delve (T_Array id t) = dl t $ T_Array id
|
||||
delve (T_Redirecting id redirs cmd) = do
|
||||
a <- roundAll redirs
|
||||
b <- round cmd
|
||||
return $ T_Redirecting id a b
|
||||
delve (T_SimpleCommand id vars cmds) = dll vars cmds $ T_SimpleCommand id
|
||||
delve (T_Pipeline id l) = dl l $ T_Pipeline id
|
||||
delve (T_Banged id l) = d1 l $ T_Banged id
|
||||
delve (T_AndIf id t u) = d2 t u $ T_AndIf id
|
||||
delve (T_OrIf id t u) = d2 t u $ T_OrIf id
|
||||
delve (T_Backgrounded id l) = d1 l $ T_Backgrounded id
|
||||
delve (T_Subshell id l) = dl l $ T_Subshell id
|
||||
delve (T_ProcSub id typ l) = dl l $ T_ProcSub id typ
|
||||
delve (T_Arithmetic id c) = d1 c $ T_Arithmetic id
|
||||
delve (T_IfExpression id conditions elses) = do
|
||||
newConds <- mapM (\(c, t) -> do
|
||||
x <- mapM round c
|
||||
y <- mapM round t
|
||||
return (x,y)
|
||||
) conditions
|
||||
newElses <- roundAll elses
|
||||
return $ T_IfExpression id newConds newElses
|
||||
delve (T_BraceGroup id l) = dl l $ T_BraceGroup id
|
||||
delve (T_WhileExpression id c l) = dll c l $ T_WhileExpression id
|
||||
delve (T_UntilExpression id c l) = dll c l $ T_UntilExpression id
|
||||
delve (T_ForIn id v w l) = dll w l $ T_ForIn id v
|
||||
delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v
|
||||
delve (T_CaseExpression id word cases) = do
|
||||
newWord <- round word
|
||||
newCases <- mapM (\(c, t) -> do
|
||||
x <- mapM round c
|
||||
y <- mapM round t
|
||||
return (x,y)
|
||||
) cases
|
||||
return $ T_CaseExpression id newWord newCases
|
||||
|
||||
delve (T_ForArithmetic id a b c group) = do
|
||||
x <- round a
|
||||
y <- round b
|
||||
z <- round c
|
||||
list <- mapM round group
|
||||
return $ T_ForArithmetic id x y z list
|
||||
|
||||
delve (T_Script id s l) = dl l $ T_Script id s
|
||||
delve (T_Function id name body) = d1 body $ T_Function id name
|
||||
delve (T_Condition id typ token) = d1 token $ T_Condition id typ
|
||||
delve (T_Extglob id str l) = dl l $ T_Extglob id str
|
||||
delve (T_DollarBraced id op) = d1 op $ T_DollarBraced id
|
||||
|
||||
delve (TC_And id typ str t1 t2) = d2 t1 t2 $ TC_And id typ str
|
||||
delve (TC_Or id typ str t1 t2) = d2 t1 t2 $ TC_Or id typ str
|
||||
delve (TC_Group id typ token) = d1 token $ TC_Group id typ
|
||||
delve (TC_Binary id typ op lhs rhs) = d2 lhs rhs $ TC_Binary id typ op
|
||||
delve (TC_Unary id typ op token) = d1 token $ TC_Unary id typ op
|
||||
delve (TC_Noary id typ token) = d1 token $ TC_Noary id typ
|
||||
|
||||
delve (TA_Binary id op t1 t2) = d2 t1 t2 $ TA_Binary id op
|
||||
delve (TA_Unary id op t1) = d1 t1 $ TA_Unary id op
|
||||
delve (TA_Sequence id l) = dl l $ TA_Sequence id
|
||||
delve (TA_Trinary id t1 t2 t3) = do
|
||||
a <- round t1
|
||||
b <- round t2
|
||||
c <- round t3
|
||||
return $ TA_Trinary id a b c
|
||||
delve (TA_Expansion id t) = d1 t $ TA_Expansion id
|
||||
delve (TA_Base id b t) = d1 t $ TA_Base id b
|
||||
delve t = return t
|
||||
|
||||
getId t = case t of
|
||||
T_AND_IF id -> id
|
||||
T_OR_IF id -> id
|
||||
T_DSEMI id -> id
|
||||
T_Semi id -> id
|
||||
T_DLESS id -> id
|
||||
T_DGREAT id -> id
|
||||
T_LESSAND id -> id
|
||||
T_GREATAND id -> id
|
||||
T_LESSGREAT id -> id
|
||||
T_DLESSDASH id -> id
|
||||
T_CLOBBER id -> id
|
||||
T_If id -> id
|
||||
T_Then id -> id
|
||||
T_Else id -> id
|
||||
T_Elif id -> id
|
||||
T_Fi id -> id
|
||||
T_Do id -> id
|
||||
T_Done id -> id
|
||||
T_Case id -> id
|
||||
T_Esac id -> id
|
||||
T_While id -> id
|
||||
T_Until id -> id
|
||||
T_For id -> id
|
||||
T_Select id -> id
|
||||
T_Lbrace id -> id
|
||||
T_Rbrace id -> id
|
||||
T_Lparen id -> id
|
||||
T_Rparen id -> id
|
||||
T_Bang id -> id
|
||||
T_In id -> id
|
||||
T_NEWLINE id -> id
|
||||
T_EOF id -> id
|
||||
T_Less id -> id
|
||||
T_Greater id -> id
|
||||
T_SingleQuoted id _ -> id
|
||||
T_Literal id _ -> id
|
||||
T_NormalWord id _ -> id
|
||||
T_DoubleQuoted id _ -> id
|
||||
T_DollarExpansion id _ -> id
|
||||
T_DollarBraced id _ -> id
|
||||
T_DollarArithmetic id _ -> id
|
||||
T_BraceExpansion id _ -> id
|
||||
T_IoFile id _ _ -> id
|
||||
T_HereDoc id _ _ _ -> id
|
||||
T_HereString id _ -> id
|
||||
T_FdRedirect id _ _ -> id
|
||||
T_Assignment id _ _ -> id
|
||||
T_Array id _ -> id
|
||||
T_Redirecting id _ _ -> id
|
||||
T_SimpleCommand id _ _ -> id
|
||||
T_Pipeline id _ -> id
|
||||
T_Banged id _ -> id
|
||||
T_AndIf id _ _ -> id
|
||||
T_OrIf id _ _ -> id
|
||||
T_Backgrounded id _ -> id
|
||||
T_IfExpression id _ _ -> id
|
||||
T_Subshell id _ -> id
|
||||
T_BraceGroup id _ -> id
|
||||
T_WhileExpression id _ _ -> id
|
||||
T_UntilExpression id _ _ -> id
|
||||
T_ForIn id _ _ _ -> id
|
||||
T_SelectIn id _ _ _ -> id
|
||||
T_CaseExpression id _ _ -> id
|
||||
T_Function id _ _ -> id
|
||||
T_Arithmetic id _ -> id
|
||||
T_Script id _ _ -> id
|
||||
T_Condition id _ _ -> id
|
||||
T_Extglob id _ _ -> id
|
||||
T_Backticked id _ -> id
|
||||
TC_And id _ _ _ _ -> id
|
||||
TC_Or id _ _ _ _ -> id
|
||||
TC_Group id _ _ -> id
|
||||
TC_Binary id _ _ _ _ -> id
|
||||
TC_Unary id _ _ _ -> id
|
||||
TC_Noary id _ _ -> id
|
||||
TA_Binary id _ _ _ -> id
|
||||
TA_Unary id _ _ -> id
|
||||
TA_Sequence id _ -> id
|
||||
TA_Variable id _ -> id
|
||||
TA_Trinary id _ _ _ -> id
|
||||
TA_Expansion id _ -> id
|
||||
TA_Literal id _ -> id
|
||||
TA_Base id _ _ -> id
|
||||
T_ProcSub id _ _ -> id
|
||||
T_Glob id _ -> id
|
||||
T_ForArithmetic id _ _ _ _ -> id
|
||||
T_DollarSingleQuoted id _ -> id
|
||||
T_DollarDoubleQuoted id _ -> id
|
||||
T_DollarBracket id _ -> id
|
||||
|
||||
blank :: Monad m => Token -> m ()
|
||||
blank = const $ return ()
|
||||
doAnalysis f t = analyze f blank id t
|
||||
doStackAnalysis startToken endToken t = analyze startToken endToken id t
|
||||
doTransform i t = runIdentity $ analyze blank blank i t
|
||||
|
File diff suppressed because it is too large
Load Diff
1730
ShellCheck/Parser.hs
1730
ShellCheck/Parser.hs
File diff suppressed because it is too large
Load Diff
@@ -1,49 +0,0 @@
|
||||
{-
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Simple (shellCheck, ShellCheckComment, scLine, scColumn, scSeverity, scMessage) where
|
||||
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Analytics
|
||||
import Data.Maybe
|
||||
import Text.Parsec.Pos
|
||||
import Data.List
|
||||
|
||||
shellCheck :: String -> [ShellCheckComment]
|
||||
shellCheck script =
|
||||
let (ParseResult result notes) = parseShell "-" script in
|
||||
let allNotes = notes ++ (concat $ maybeToList $ do
|
||||
(tree, map) <- result
|
||||
let newMap = runAllAnalytics tree map
|
||||
return $ notesFromMap newMap
|
||||
)
|
||||
in
|
||||
map formatNote $ nub $ sortNotes allNotes
|
||||
|
||||
data ShellCheckComment = ShellCheckComment { scLine :: Int, scColumn :: Int, scSeverity :: String, scMessage :: String }
|
||||
|
||||
instance Show ShellCheckComment where
|
||||
show c = concat ["(", show $ scLine c, ",", show $ scColumn c, ") ", scSeverity c, ": ", scMessage c]
|
||||
|
||||
severityToString s =
|
||||
case s of
|
||||
ErrorC -> "error"
|
||||
WarningC -> "warning"
|
||||
InfoC -> "info"
|
||||
StyleC -> "style"
|
||||
|
||||
formatNote (ParseNote pos severity text) = ShellCheckComment (sourceLine pos) (sourceColumn pos) (severityToString severity) text
|
13
build/README.md
Normal file
13
build/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
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`.
|
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
|
31
build/darwin.x86_64/Dockerfile
Normal file
31
build/darwin.x86_64/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
# DIGEST:sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76
|
||||
FROM liushuyu/osxcross:latest
|
||||
|
||||
ENV TARGET x86_64-apple-darwin18
|
||||
ENV TARGETNAME darwin.x86_64
|
||||
|
||||
# Build dependencies
|
||||
USER root
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
RUN apt-get update && apt-get install -y ghc automake autoconf llvm curl
|
||||
|
||||
# Build GHC
|
||||
WORKDIR /ghc
|
||||
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-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.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.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"]
|
14
build/darwin.x86_64/build
Executable file
14
build/darwin.x86_64/build
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
set -xe
|
||||
{
|
||||
tar xzv --strip-components=1
|
||||
chmod +x striptests && ./striptests
|
||||
mkdir "$TARGETNAME"
|
||||
cabal update
|
||||
( 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
|
30
build/linux.aarch64/Dockerfile
Normal file
30
build/linux.aarch64/Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
||||
FROM ubuntu:20.04
|
||||
|
||||
ENV TARGET aarch64-linux-gnu
|
||||
ENV TARGETNAME linux.aarch64
|
||||
|
||||
# Build dependencies
|
||||
USER root
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
RUN apt-get update && apt-get install -y ghc automake autoconf build-essential llvm curl qemu-user-static gcc-$TARGET
|
||||
|
||||
# Build GHC
|
||||
WORKDIR /ghc
|
||||
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-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.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.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;--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"]
|
15
build/linux.aarch64/build
Executable file
15
build/linux.aarch64/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"
|
||||
"$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
|
59
build/linux.armv6hf/Dockerfile
Normal file
59
build/linux.armv6hf/Dockerfile
Normal file
@@ -0,0 +1,59 @@
|
||||
# I've again spent days trying to get a working armv6hf compiler going.
|
||||
# God only knows how many recompilations of GCC, GHC, libraries, and
|
||||
# ShellCheck itself, has gone into it.
|
||||
#
|
||||
# I tried Debian's toolchain. I tried my custom one built according to
|
||||
# RPi `gcc -v`. I tried GHC9, glibc, musl, registerised vs not, but
|
||||
# nothing has yielded an armv6hf binary that does not immediately
|
||||
# segfault on qemu-arm-static or the RPi itself.
|
||||
#
|
||||
# I then tried the same but with armv7hf. Same story.
|
||||
#
|
||||
# Emulating the entire userspace with balenalib again? Very strange build
|
||||
# failures where programs would fail to execute with > ~100 arguments.
|
||||
#
|
||||
# Finally, creating our own appears to work when using a custom QEmu
|
||||
# patched to follow execve calls.
|
||||
#
|
||||
# PS: $100 bounty for getting a RPi1 compatible static build going
|
||||
# with cross-compilation, similar to what the aarch64 build does.
|
||||
#
|
||||
|
||||
FROM ubuntu:20.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 build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev
|
||||
WORKDIR /build
|
||||
RUN git clone --depth 1 https://github.com/koalaman/qemu
|
||||
RUN cd qemu && ./configure --static && cd build && ninja qemu-arm
|
||||
RUN cp qemu/build/qemu-arm /build/qemu-arm-static
|
||||
ENV QEMU_EXECVE 1
|
||||
|
||||
# Set up an armv6 userspace
|
||||
WORKDIR /
|
||||
RUN apt-get install -y debootstrap qemu-user-static
|
||||
# We expect this to fail if the host doesn't have binfmt qemu support
|
||||
RUN qemu-debootstrap --arch armhf bullseye pi http://mirrordirector.raspbian.org/raspbian || [ -e /pi/etc/issue ]
|
||||
RUN cp /build/qemu-arm-static /pi/usr/bin/qemu-arm-static
|
||||
RUN printf > /bin/pirun '%s\n' '#!/bin/sh' 'chroot /pi /usr/bin/qemu-arm-static /usr/bin/env "$@"' && chmod +x /bin/pirun
|
||||
# If the debootstrap process didn't finish, continue it
|
||||
RUN [ ! -e /pi/debootstrap ] || pirun '/debootstrap/debootstrap' --second-stage
|
||||
|
||||
# Install deps in the chroot
|
||||
RUN pirun apt-get update
|
||||
RUN pirun apt-get install -y ghc cabal-install
|
||||
|
||||
# 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"
|
||||
RUN pirun cabal update
|
||||
RUN IFS=";" && pirun cabal install --dependencies-only $CABALOPTS ShellCheck
|
||||
|
||||
# Copy the build script
|
||||
WORKDIR /pi/scratch
|
||||
COPY build /pi/usr/bin
|
||||
ENTRYPOINT ["/bin/pirun", "/usr/bin/build"]
|
16
build/linux.armv6hf/build
Executable file
16
build/linux.armv6hf/build
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
set -xe
|
||||
cd /scratch
|
||||
{
|
||||
tar xzv --strip-components=1
|
||||
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"
|
1
build/linux.armv6hf/tag
Normal file
1
build/linux.armv6hf/tag
Normal file
@@ -0,0 +1 @@
|
||||
koalaman/scbuilder-linux-armv6hf
|
26
build/linux.x86_64/Dockerfile
Normal file
26
build/linux.x86_64/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM ubuntu:20.04
|
||||
|
||||
ENV TARGETNAME linux.x86_64
|
||||
|
||||
# Install GHC and cabal
|
||||
USER root
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
RUN apt-get update && apt-get install -y ghc curl xz-utils
|
||||
|
||||
# So we'd like a later version of Cabal that supports --enable-executable-static,
|
||||
# but we can't use Ubuntu 20.10 because coreutils has switched to new syscalls that
|
||||
# the TravisCI kernel doesn't support. Download it manually.
|
||||
RUN curl "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/bin
|
||||
|
||||
# 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-7.75.0/curl-7.75.0-win64-mingw.zip" | busybox unzip - && mv curl-7.75.0-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"]
|
19
build/windows.x86_64/build
Executable file
19
build/windows.x86_64/build
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
cabal() {
|
||||
wine /haskell/bin/cabal.exe "$@"
|
||||
}
|
||||
|
||||
set -xe
|
||||
{
|
||||
tar xzv --strip-components=1
|
||||
chmod +x striptests && ./striptests
|
||||
mkdir "$TARGETNAME"
|
||||
cabal update
|
||||
( 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
|
BIN
doc/emacs-flycheck.png
Normal file
BIN
doc/emacs-flycheck.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
doc/terminal.png
Normal file
BIN
doc/terminal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
BIN
doc/vim-syntastic.png
Normal file
BIN
doc/vim-syntastic.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
32
jsoncheck.hs
32
jsoncheck.hs
@@ -1,32 +0,0 @@
|
||||
{-
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-}
|
||||
import ShellCheck.Simple
|
||||
import Text.JSON
|
||||
|
||||
instance JSON ShellCheckComment where
|
||||
showJSON c = makeObj [
|
||||
("line", showJSON $ scLine c),
|
||||
("column", showJSON $ scColumn c),
|
||||
("level", showJSON $ scSeverity c),
|
||||
("message", showJSON $ scMessage c)
|
||||
]
|
||||
readJSON = undefined
|
||||
|
||||
main = do
|
||||
script <- getContents
|
||||
putStrLn $ encodeStrict $ shellCheck script
|
4
manpage
Executable file
4
manpage
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
echo >&2 "Generating man page using pandoc"
|
||||
pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1 || exit
|
||||
echo >&2 "Done. You can read it with: man ./shellcheck.1"
|
13
nextnumber
Executable file
13
nextnumber
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# TODO: Find a less trashy way to get the next available error code
|
||||
if ! shopt -s globstar
|
||||
then
|
||||
echo "Error: This script depends on Bash 4." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for i in 1 2 3
|
||||
do
|
||||
last=$(grep -hv "^prop" ./**/*.hs | grep -Ewo "${i}[0-9]{3}" | sort -n | tail -n 1)
|
||||
echo "Next ${i}xxx: $((last+1))"
|
||||
done
|
13
quickrun
Executable file
13
quickrun
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# quickrun runs ShellCheck in an interpreted mode.
|
||||
# This allows testing changes without recompiling.
|
||||
|
||||
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 "$@"
|
24
quicktest
Executable file
24
quicktest
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# quicktest runs the ShellCheck unit tests in an interpreted mode.
|
||||
# This allows running tests without compiling, which can be faster.
|
||||
# 'cabal test' remains the source of truth.
|
||||
|
||||
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 -isrc -i"$path" test/shellcheck.hs 2>&1 | tee /dev/stderr)
|
||||
if [[ $var == *ExitSuccess* ]]
|
||||
then
|
||||
exit 0
|
||||
else
|
||||
grep -C 3 -e "Fail" -e "Tracing" <<< "$var"
|
||||
exit 1
|
||||
fi
|
||||
) 2>&1
|
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"
|
384
shellcheck.1.md
Normal file
384
shellcheck.1.md
Normal file
@@ -0,0 +1,384 @@
|
||||
% SHELLCHECK(1) Shell script analysis tool
|
||||
|
||||
# NAME
|
||||
|
||||
shellcheck - Shell script analysis tool
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
**shellcheck** [*OPTIONS*...] *FILES*...
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
ShellCheck is a static analysis and linting tool for sh/bash scripts. It's
|
||||
mainly focused on handling typical beginner and intermediate level syntax
|
||||
errors and pitfalls where the shell just gives a cryptic error message or
|
||||
strange behavior, but it also reports on a few more advanced issues where
|
||||
corner cases can cause delayed failures.
|
||||
|
||||
ShellCheck gives shell specific advice. Consider this line:
|
||||
|
||||
(( area = 3.14*r*r ))
|
||||
|
||||
+ For scripts starting with `#!/bin/sh` (or when using `-s sh`), ShellCheck
|
||||
will warn that `(( .. ))` is not POSIX compliant (similar to checkbashisms).
|
||||
|
||||
+ For scripts starting with `#!/bin/bash` (or using `-s bash`), ShellCheck
|
||||
will warn that decimals are not supported.
|
||||
|
||||
+ For scripts starting with `#!/bin/ksh` (or using `-s ksh`), ShellCheck will
|
||||
not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
||||
|
||||
# OPTIONS
|
||||
|
||||
**-a**,\ **--check-sourced**
|
||||
|
||||
: Emit warnings in sourced files. Normally, `shellcheck` will only warn
|
||||
about issues in the specified files. With this option, any issues in
|
||||
sourced files will also be reported.
|
||||
|
||||
**-C**[*WHEN*],\ **--color**[=*WHEN*]
|
||||
|
||||
: For TTY output, enable colors *always*, *never* or *auto*. The default
|
||||
is *auto*. **--color** without an argument is equivalent to
|
||||
**--color=always**.
|
||||
|
||||
**-i**\ *CODE1*[,*CODE2*...],\ **--include=***CODE1*[,*CODE2*...]
|
||||
|
||||
: Explicitly include only the specified codes in the report. Subsequent **-i**
|
||||
options are cumulative, but all the codes can be specified at once,
|
||||
comma-separated as a single argument. Include options override any provided
|
||||
exclude options.
|
||||
|
||||
**-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...]
|
||||
|
||||
: Explicitly exclude the specified codes from the report. Subsequent **-e**
|
||||
options are cumulative, but all the codes can be specified at once,
|
||||
comma-separated as a single argument.
|
||||
|
||||
**-f** *FORMAT*, **--format=***FORMAT*
|
||||
|
||||
: Specify the output format of shellcheck, which prints its results in the
|
||||
standard output. Subsequent **-f** options are ignored, see **FORMATS**
|
||||
below for more information.
|
||||
|
||||
**--list-optional**
|
||||
|
||||
: Output a list of known optional checks. These can be enabled with **-o**
|
||||
flags or **enable** directives.
|
||||
|
||||
**--norc**
|
||||
|
||||
: Don't try to look for .shellcheckrc configuration files.
|
||||
|
||||
**-o**\ *NAME1*[,*NAME2*...],\ **--enable=***NAME1*[,*NAME2*...]
|
||||
|
||||
: Enable optional checks. The special name *all* enables all of them.
|
||||
Subsequent **-o** options accumulate. This is equivalent to specifying
|
||||
**enable** directives.
|
||||
|
||||
**-P**\ *SOURCEPATH*,\ **--source-path=***SOURCEPATH*
|
||||
|
||||
: Specify paths to search for sourced files, separated by `:` on Unix and
|
||||
`;` on Windows. This is equivalent to specifying `search-path`
|
||||
directives.
|
||||
|
||||
**-s**\ *shell*,\ **--shell=***shell*
|
||||
|
||||
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
|
||||
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
|
||||
POSIX `sh` (not the system's), and will warn of portability issues.
|
||||
|
||||
**-S**\ *SEVERITY*,\ **--severity=***severity*
|
||||
|
||||
: Specify minimum severity of errors to consider. Valid values in order of
|
||||
severity are *error*, *warning*, *info* and *style*.
|
||||
The default is *style*.
|
||||
|
||||
**-V**,\ **--version**
|
||||
|
||||
: Print version information and exit.
|
||||
|
||||
**-W** *NUM*,\ **--wiki-link-count=NUM**
|
||||
|
||||
: For TTY output, show *NUM* wiki links to more information about mentioned
|
||||
warnings. Set to 0 to disable them entirely.
|
||||
|
||||
**-x**,\ **--external-sources**
|
||||
|
||||
: Follow `source` statements even when the file is not specified as input.
|
||||
By default, `shellcheck` will only follow files specified on the command
|
||||
line (plus `/dev/null`). This option allows following any file the script
|
||||
may `source`.
|
||||
|
||||
This option may also be enabled using `external-sources=true` in
|
||||
`.shellcheckrc`. This flag takes precedence.
|
||||
|
||||
**FILES...**
|
||||
|
||||
: One or more script files to check, or "-" for standard input.
|
||||
|
||||
|
||||
# FORMATS
|
||||
|
||||
**tty**
|
||||
|
||||
: Plain text, human readable output. This is the default.
|
||||
|
||||
**gcc**
|
||||
|
||||
: GCC compatible output. Useful for editors that support compiling and
|
||||
showing syntax errors.
|
||||
|
||||
For example, in Vim, `:set makeprg=shellcheck\ -f\ gcc\ %` will allow
|
||||
using `:make` to check the script, and `:cnext` to jump to the next error.
|
||||
|
||||
<file>:<line>:<column>: <type>: <message>
|
||||
|
||||
**checkstyle**
|
||||
|
||||
: Checkstyle compatible XML output. Supported directly or through plugins
|
||||
by many IDEs and build monitoring systems.
|
||||
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<checkstyle version='4.3'>
|
||||
<file name='file'>
|
||||
<error
|
||||
line='line'
|
||||
column='column'
|
||||
severity='severity'
|
||||
message='message'
|
||||
source='ShellCheck.SC####' />
|
||||
...
|
||||
</file>
|
||||
...
|
||||
</checkstyle>
|
||||
|
||||
**diff**
|
||||
|
||||
: Auto-fixes in unified diff format. Can be piped to `git apply` or `patch -p1`
|
||||
to automatically apply fixes.
|
||||
|
||||
--- a/test.sh
|
||||
+++ b/test.sh
|
||||
@@ -2,6 +2,6 @@
|
||||
## Example of a broken script.
|
||||
for f in $(ls *.m3u)
|
||||
do
|
||||
- grep -qi hq.*mp3 $f \
|
||||
+ grep -qi hq.*mp3 "$f" \
|
||||
&& echo -e 'Playlist $f contains a HQ file in mp3 format'
|
||||
done
|
||||
|
||||
|
||||
**json1**
|
||||
|
||||
: Json is a popular serialization format that is more suitable for web
|
||||
applications. ShellCheck's json is compact and contains only the bare
|
||||
minimum. Tabs are counted as 1 character.
|
||||
|
||||
{
|
||||
comments: [
|
||||
{
|
||||
"file": "filename",
|
||||
"line": lineNumber,
|
||||
"column": columnNumber,
|
||||
"level": "severitylevel",
|
||||
"code": errorCode,
|
||||
"message": "warning message"
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
**json**
|
||||
|
||||
: This is a legacy version of the **json1** format. It's a raw array of
|
||||
comments, and all offsets have a tab stop of 8.
|
||||
|
||||
**quiet**
|
||||
|
||||
: Suppress all normal output. Exit with zero if no issues are found,
|
||||
otherwise exit with one. Stops processing after the first issue.
|
||||
|
||||
|
||||
# DIRECTIVES
|
||||
|
||||
ShellCheck directives can be specified as comments in the shell script.
|
||||
If they appear before the first command, they are considered file-wide.
|
||||
Otherwise, they apply to the immediately following command or block:
|
||||
|
||||
# shellcheck key=value key=value
|
||||
command-or-structure
|
||||
|
||||
For example, to suppress SC2035 about using `./*.jpg`:
|
||||
|
||||
# shellcheck disable=SC2035
|
||||
echo "Files: " *.jpg
|
||||
|
||||
To tell ShellCheck where to look for an otherwise dynamically determined file:
|
||||
|
||||
# shellcheck source=./lib.sh
|
||||
source "$(find_install_dir)/lib.sh"
|
||||
|
||||
Here a shell brace group is used to suppress a warning on multiple lines:
|
||||
|
||||
# shellcheck disable=SC2016
|
||||
{
|
||||
echo 'Modifying $PATH'
|
||||
echo 'PATH=foo:$PATH' >> ~/.bashrc
|
||||
}
|
||||
|
||||
Valid keys are:
|
||||
|
||||
**disable**
|
||||
: Disables a comma separated list of error codes for the following command.
|
||||
The command can be a simple command like `echo foo`, or a compound command
|
||||
like a function definition, subshell block or loop. 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 an optional check by name, as listed with **--list-optional**.
|
||||
Only file-wide `enable` directives are considered.
|
||||
|
||||
**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**
|
||||
: Overrides the filename included by a `source`/`.` statement. This can be
|
||||
used to tell shellcheck where to look for a file whose name is determined
|
||||
at runtime, or to skip a source by telling it to use `/dev/null`.
|
||||
|
||||
**source-path**
|
||||
: Add a directory to the search path for `source`/`.` statements (by default,
|
||||
only ShellCheck's working directory is included). Absolute paths will also
|
||||
be rooted in these paths. The special path `SCRIPTDIR` can be used to
|
||||
specify the currently checked script's directory, as in
|
||||
`source-path=SCRIPTDIR` or `source-path=SCRIPTDIR/../libs`. Multiple
|
||||
paths accumulate, and `-P` takes precedence over them.
|
||||
|
||||
**shell**
|
||||
: Overrides the shell detected from the shebang. This is useful for
|
||||
files meant to be included (and thus lacking a shebang), or possibly
|
||||
as a more targeted alternative to 'disable=SC2039'.
|
||||
|
||||
# RC FILES
|
||||
|
||||
Unless `--norc` is used, ShellCheck will look for a file `.shellcheckrc` or
|
||||
`shellcheckrc` in the script's directory and each parent directory. If found,
|
||||
it will read `key=value` pairs from it and treat them as file-wide directives.
|
||||
|
||||
Here is an example `.shellcheckrc`:
|
||||
|
||||
# Look for 'source'd files relative to the checked script,
|
||||
# and also look for absolute paths in /mnt/chroot
|
||||
source-path=SCRIPTDIR
|
||||
source-path=/mnt/chroot
|
||||
|
||||
# Allow opening any 'source'd file, even if not specified as input
|
||||
external-sources=true
|
||||
|
||||
# Turn on warnings for unquoted variables with safe values
|
||||
enable=quote-safe-variables
|
||||
|
||||
# Turn on warnings for unassigned uppercase variables
|
||||
enable=check-unassigned-uppercase
|
||||
|
||||
# Allow [ ! -z foo ] instead of suggesting -n
|
||||
disable=SC2236
|
||||
|
||||
If no `.shellcheckrc` is found in any of the parent directories, ShellCheck
|
||||
will look in `~/.shellcheckrc` followed by the XDG config directory
|
||||
(usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on
|
||||
Windows. Only the first file found will be used.
|
||||
|
||||
Note for Snap users: the Snap sandbox disallows access to hidden files.
|
||||
Use `shellcheckrc` without the dot instead.
|
||||
|
||||
Note for Docker users: ShellCheck will only be able to look for files that
|
||||
are mounted in the container, so `~/.shellcheckrc` will not be read.
|
||||
|
||||
|
||||
# ENVIRONMENT VARIABLES
|
||||
|
||||
The environment variable `SHELLCHECK_OPTS` can be set with default flags:
|
||||
|
||||
export SHELLCHECK_OPTS='--shell=bash --exclude=SC2016'
|
||||
|
||||
Its value will be split on spaces and prepended to the command line on each
|
||||
invocation.
|
||||
|
||||
# RETURN VALUES
|
||||
|
||||
ShellCheck uses the following exit codes:
|
||||
|
||||
+ 0: All files successfully scanned with no issues.
|
||||
+ 1: All files successfully scanned with some issues.
|
||||
+ 2: Some files could not be processed (e.g. file not found).
|
||||
+ 3: ShellCheck was invoked with bad syntax (e.g. unknown flag).
|
||||
+ 4: ShellCheck was invoked with bad options (e.g. unknown formatter).
|
||||
|
||||
# LOCALE
|
||||
|
||||
This version of ShellCheck is only available in English. All files are
|
||||
leniently decoded as UTF-8, with a fallback of ISO-8859-1 for invalid
|
||||
sequences. `LC_CTYPE` is respected for output, and defaults to UTF-8 for
|
||||
locales where encoding is unspecified (such as the `C` locale).
|
||||
|
||||
Windows users seeing `commitBuffer: invalid argument (invalid character)`
|
||||
should set their terminal to use UTF-8 with `chcp 65001`.
|
||||
|
||||
# KNOWN INCOMPATIBILITIES
|
||||
|
||||
(If nothing in this section makes sense, you are unlikely to be affected by it)
|
||||
|
||||
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
|
||||
|
||||
Bugs and issues can be reported on GitHub:
|
||||
|
||||
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 2012-2021, Vidar Holen and contributors.
|
||||
Licensed under the GNU General Public License version 3 or later,
|
||||
see https://gnu.org/licenses/gpl.html
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
sh(1) bash(1)
|
630
shellcheck.hs
630
shellcheck.hs
@@ -1,86 +1,590 @@
|
||||
{-
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
import Control.Monad
|
||||
import GHC.Exts
|
||||
import GHC.IO.Device
|
||||
import ShellCheck.Simple
|
||||
import System.Directory
|
||||
import System.Environment
|
||||
import System.Exit
|
||||
import System.IO
|
||||
import qualified ShellCheck.Analyzer
|
||||
import ShellCheck.Checker
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Regex
|
||||
|
||||
import qualified ShellCheck.Formatter.CheckStyle
|
||||
import ShellCheck.Formatter.Format
|
||||
import qualified ShellCheck.Formatter.Diff
|
||||
import qualified ShellCheck.Formatter.GCC
|
||||
import qualified ShellCheck.Formatter.JSON
|
||||
import qualified ShellCheck.Formatter.JSON1
|
||||
import qualified ShellCheck.Formatter.TTY
|
||||
import qualified ShellCheck.Formatter.Quiet
|
||||
|
||||
clear = ansi 0
|
||||
ansi n = "\x1B[" ++ (show n) ++ "m"
|
||||
import Control.Exception
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Data.Bits
|
||||
import Data.Char
|
||||
import Data.Either
|
||||
import Data.Functor
|
||||
import Data.IORef
|
||||
import Data.List
|
||||
import qualified Data.Map as Map
|
||||
import Data.Maybe
|
||||
import Data.Monoid
|
||||
import Data.Semigroup (Semigroup (..))
|
||||
import Prelude hiding (catch)
|
||||
import System.Console.GetOpt
|
||||
import System.Directory
|
||||
import System.Environment
|
||||
import System.Exit
|
||||
import System.FilePath
|
||||
import System.IO
|
||||
|
||||
colorForLevel "error" = 31 -- red
|
||||
colorForLevel "warning" = 33 -- yellow
|
||||
colorForLevel "info" = 32 -- green
|
||||
colorForLevel "style" = 32 -- green
|
||||
colorForLevel "message" = 1 -- bold
|
||||
colorForLevel "source" = 0 -- none
|
||||
colorForLevel _ = 0 -- none
|
||||
data Flag = Flag String String
|
||||
data Status =
|
||||
NoProblems
|
||||
| SomeProblems
|
||||
| SupportFailure
|
||||
| SyntaxFailure
|
||||
| RuntimeException
|
||||
deriving (Ord, Eq, Show)
|
||||
|
||||
colorComment level comment = (ansi $ colorForLevel level) ++ comment ++ clear
|
||||
instance Semigroup Status where
|
||||
(<>) = max
|
||||
|
||||
doFile path colorFunc = do
|
||||
let actualPath = if path == "-" then "/dev/stdin" else path
|
||||
exists <- doesFileExist actualPath
|
||||
if exists then do
|
||||
contents <- readFile actualPath
|
||||
doInput path contents colorFunc
|
||||
else do
|
||||
putStrLn (colorFunc "error" $ "No such file: " ++ actualPath)
|
||||
instance Monoid Status where
|
||||
mempty = NoProblems
|
||||
mappend = (Data.Semigroup.<>)
|
||||
|
||||
doInput filename contents colorFunc = do
|
||||
let fileLines = lines contents
|
||||
let lineCount = length fileLines
|
||||
let comments = shellCheck contents
|
||||
let groups = groupWith scLine comments
|
||||
if not $ null comments then do
|
||||
mapM_ (\x -> do
|
||||
let lineNum = scLine (head x)
|
||||
let line = if lineNum < 1 || lineNum > lineCount
|
||||
then ""
|
||||
else fileLines !! (lineNum - 1)
|
||||
putStrLn ""
|
||||
putStrLn $ colorFunc "message" ("In " ++ filename ++" line " ++ (show $ lineNum) ++ ":")
|
||||
putStrLn (colorFunc "source" line)
|
||||
mapM (\c -> putStrLn (colorFunc (scSeverity c) $ cuteIndent c)) x
|
||||
putStrLn ""
|
||||
) groups
|
||||
else do
|
||||
putStrLn ("No comments for " ++ filename)
|
||||
data Options = Options {
|
||||
checkSpec :: CheckSpec,
|
||||
externalSources :: Bool,
|
||||
sourcePaths :: [FilePath],
|
||||
formatterOptions :: FormatterOptions,
|
||||
minSeverity :: Severity
|
||||
}
|
||||
|
||||
cuteIndent comment =
|
||||
(replicate ((scColumn comment) - 1) ' ') ++ "^-- " ++ (scMessage comment)
|
||||
defaultOptions = Options {
|
||||
checkSpec = emptyCheckSpec,
|
||||
externalSources = False,
|
||||
sourcePaths = [],
|
||||
formatterOptions = newFormatterOptions {
|
||||
foColorOption = ColorAuto
|
||||
},
|
||||
minSeverity = StyleC
|
||||
}
|
||||
|
||||
getColorFunc = do
|
||||
term <- hIsTerminalDevice stdout
|
||||
return $ if term then colorComment else const id
|
||||
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
|
||||
options = [
|
||||
Option "a" ["check-sourced"]
|
||||
(NoArg $ Flag "sourced" "false") "Include warnings from sourced files",
|
||||
Option "C" ["color"]
|
||||
(OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN")
|
||||
"Use color (auto, always, never)",
|
||||
Option "i" ["include"]
|
||||
(ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings",
|
||||
Option "e" ["exclude"]
|
||||
(ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings",
|
||||
Option "f" ["format"]
|
||||
(ReqArg (Flag "format") "FORMAT") $
|
||||
"Output format (" ++ formatList ++ ")",
|
||||
Option "" ["list-optional"]
|
||||
(NoArg $ Flag "list-optional" "true") "List checks disabled by default",
|
||||
Option "" ["norc"]
|
||||
(NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc files",
|
||||
Option "o" ["enable"]
|
||||
(ReqArg (Flag "enable") "check1,check2..")
|
||||
"List of optional checks to enable (or 'all')",
|
||||
Option "P" ["source-path"]
|
||||
(ReqArg (Flag "source-path") "SOURCEPATHS")
|
||||
"Specify path when looking for sourced files (\"SCRIPTDIR\" for script's dir)",
|
||||
Option "s" ["shell"]
|
||||
(ReqArg (Flag "shell") "SHELLNAME")
|
||||
"Specify dialect (sh, bash, dash, ksh)",
|
||||
Option "S" ["severity"]
|
||||
(ReqArg (Flag "severity") "SEVERITY")
|
||||
"Minimum severity of errors to consider (error, warning, info, style)",
|
||||
Option "V" ["version"]
|
||||
(NoArg $ Flag "version" "true") "Print version information",
|
||||
Option "W" ["wiki-link-count"]
|
||||
(ReqArg (Flag "wiki-link-count") "NUM")
|
||||
"The number of wiki links to show, when applicable",
|
||||
Option "x" ["external-sources"]
|
||||
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES",
|
||||
Option "" ["help"]
|
||||
(NoArg $ Flag "help" "true") "Show this usage summary and exit"
|
||||
]
|
||||
getUsageInfo = usageInfo usageHeader options
|
||||
|
||||
printErr = lift . hPutStrLn stderr
|
||||
|
||||
parseArguments :: [String] -> ExceptT Status IO ([Flag], [FilePath])
|
||||
parseArguments argv =
|
||||
case getOpt Permute options argv of
|
||||
(opts, files, []) -> return (opts, files)
|
||||
(_, _, errors) -> do
|
||||
printErr $ concat errors ++ "\n" ++ getUsageInfo
|
||||
throwError SyntaxFailure
|
||||
|
||||
formats :: FormatterOptions -> Map.Map String (IO Formatter)
|
||||
formats options = Map.fromList [
|
||||
("checkstyle", ShellCheck.Formatter.CheckStyle.format),
|
||||
("diff", ShellCheck.Formatter.Diff.format options),
|
||||
("gcc", ShellCheck.Formatter.GCC.format),
|
||||
("json", ShellCheck.Formatter.JSON.format),
|
||||
("json1", ShellCheck.Formatter.JSON1.format),
|
||||
("tty", ShellCheck.Formatter.TTY.format options),
|
||||
("quiet", ShellCheck.Formatter.Quiet.format options)
|
||||
]
|
||||
|
||||
formatList = intercalate ", " names
|
||||
where
|
||||
names = Map.keys $ formats (formatterOptions defaultOptions)
|
||||
|
||||
getOption [] _ = Nothing
|
||||
getOption (Flag var val:_) name | name == var = return val
|
||||
getOption (_:rest) flag = getOption rest flag
|
||||
|
||||
getOptions options name =
|
||||
map (\(Flag _ val) -> val) . filter (\(Flag var _) -> var == name) $ options
|
||||
|
||||
split char str =
|
||||
split' str []
|
||||
where
|
||||
split' (a:rest) element =
|
||||
if a == char
|
||||
then reverse element : split' rest []
|
||||
else split' rest (a:element)
|
||||
split' [] element = [reverse element]
|
||||
|
||||
toStatus = fmap (either id id) . runExceptT
|
||||
|
||||
getEnvArgs = do
|
||||
opts <- getEnv "SHELLCHECK_OPTS" `catch` cantWaitForLookupEnv
|
||||
return . filter (not . null) $ opts `splitOn` mkRegex " +"
|
||||
where
|
||||
cantWaitForLookupEnv :: IOException -> IO String
|
||||
cantWaitForLookupEnv = const $ return ""
|
||||
|
||||
main = do
|
||||
args <- getArgs
|
||||
colors <- getColorFunc
|
||||
if null args then do
|
||||
hPutStrLn stderr "shellcheck -- bash/sh script static analysis tool"
|
||||
hPutStrLn stderr "Usage: shellcheck filenames..."
|
||||
exitFailure
|
||||
else
|
||||
mapM (\f -> doFile f colors) args
|
||||
params <- getArgs
|
||||
envOpts <- getEnvArgs
|
||||
let args = envOpts ++ params
|
||||
status <- toStatus $ do
|
||||
(flags, files) <- parseArguments args
|
||||
process flags files
|
||||
exitWith $ statusToCode status
|
||||
|
||||
statusToCode status =
|
||||
case status of
|
||||
NoProblems -> ExitSuccess
|
||||
SomeProblems -> ExitFailure 1
|
||||
SyntaxFailure -> ExitFailure 3
|
||||
SupportFailure -> ExitFailure 4
|
||||
RuntimeException -> ExitFailure 2
|
||||
|
||||
process :: [Flag] -> [FilePath] -> ExceptT Status IO Status
|
||||
process flags files = do
|
||||
options <- foldM (flip parseOption) defaultOptions flags
|
||||
verifyFiles files
|
||||
let format = fromMaybe "tty" $ getOption flags "format"
|
||||
let formatters = formats $ formatterOptions options
|
||||
formatter <-
|
||||
case Map.lookup format formatters of
|
||||
Nothing -> do
|
||||
printErr $ "Unknown format " ++ format
|
||||
printErr "Supported formats:"
|
||||
mapM_ (printErr . write) $ Map.keys formatters
|
||||
throwError SupportFailure
|
||||
where write s = " " ++ s
|
||||
Just f -> ExceptT $ fmap Right f
|
||||
sys <- lift $ ioInterface options files
|
||||
lift $ runFormatter sys formatter options files
|
||||
|
||||
runFormatter :: SystemInterface IO -> Formatter -> Options -> [FilePath]
|
||||
-> IO Status
|
||||
runFormatter sys format options files = do
|
||||
header format
|
||||
result <- foldM f NoProblems files
|
||||
footer format
|
||||
return result
|
||||
where
|
||||
f :: Status -> FilePath -> IO Status
|
||||
f status file = do
|
||||
newStatus <- process file `catch` handler file
|
||||
return $ status `mappend` newStatus
|
||||
handler :: FilePath -> IOException -> IO Status
|
||||
handler file e = reportFailure file (show e)
|
||||
reportFailure file str = do
|
||||
onFailure format file str
|
||||
return RuntimeException
|
||||
|
||||
process :: FilePath -> IO Status
|
||||
process filename = do
|
||||
input <- siReadFile sys Nothing filename
|
||||
either (reportFailure filename) check input
|
||||
where
|
||||
check contents = do
|
||||
let checkspec = (checkSpec options) {
|
||||
csFilename = filename,
|
||||
csScript = contents
|
||||
}
|
||||
result <- checkScript sys checkspec
|
||||
onResult format result sys
|
||||
return $
|
||||
if null (crComments result)
|
||||
then NoProblems
|
||||
else SomeProblems
|
||||
|
||||
parseEnum name value list =
|
||||
case filter ((== value) . fst) list of
|
||||
[(name, value)] -> return value
|
||||
[] -> do
|
||||
printErr $ "Unknown value for --" ++ name ++ ". " ++
|
||||
"Valid options are: " ++ (intercalate ", " $ map fst list)
|
||||
throwError SupportFailure
|
||||
|
||||
parseColorOption value =
|
||||
parseEnum "color" value [
|
||||
("auto", ColorAuto),
|
||||
("always", ColorAlways),
|
||||
("never", ColorNever)
|
||||
]
|
||||
|
||||
parseSeverityOption value =
|
||||
parseEnum "severity" value [
|
||||
("error", ErrorC),
|
||||
("warning", WarningC),
|
||||
("info", InfoC),
|
||||
("style", StyleC)
|
||||
]
|
||||
|
||||
parseOption flag options =
|
||||
case flag of
|
||||
Flag "shell" str ->
|
||||
fromMaybe (die $ "Unknown shell: " ++ str) $ do
|
||||
shell <- shellForExecutable str
|
||||
return $ return options {
|
||||
checkSpec = (checkSpec options) {
|
||||
csShellTypeOverride = Just shell
|
||||
}
|
||||
}
|
||||
|
||||
Flag "exclude" str -> do
|
||||
new <- mapM parseNum $ filter (not . null) $ split ',' str
|
||||
let old = csExcludedWarnings . checkSpec $ options
|
||||
return options {
|
||||
checkSpec = (checkSpec options) {
|
||||
csExcludedWarnings = new ++ old
|
||||
}
|
||||
}
|
||||
|
||||
Flag "include" str -> do
|
||||
new <- mapM parseNum $ filter (not . null) $ split ',' str
|
||||
let old = csIncludedWarnings . checkSpec $ options
|
||||
return options {
|
||||
checkSpec = (checkSpec options) {
|
||||
csIncludedWarnings =
|
||||
if null new
|
||||
then old
|
||||
else Just new `mappend` old
|
||||
}
|
||||
}
|
||||
|
||||
Flag "version" _ -> do
|
||||
liftIO printVersion
|
||||
throwError NoProblems
|
||||
|
||||
Flag "list-optional" _ -> do
|
||||
liftIO printOptional
|
||||
throwError NoProblems
|
||||
|
||||
Flag "help" _ -> do
|
||||
liftIO $ putStrLn getUsageInfo
|
||||
throwError NoProblems
|
||||
|
||||
Flag "externals" _ ->
|
||||
return options {
|
||||
externalSources = True
|
||||
}
|
||||
|
||||
Flag "color" color -> do
|
||||
option <- parseColorOption color
|
||||
return options {
|
||||
formatterOptions = (formatterOptions options) {
|
||||
foColorOption = option
|
||||
}
|
||||
}
|
||||
|
||||
Flag "source-path" str -> do
|
||||
let paths = splitSearchPath str
|
||||
return options {
|
||||
sourcePaths = (sourcePaths options) ++ paths
|
||||
}
|
||||
|
||||
Flag "sourced" _ ->
|
||||
return options {
|
||||
checkSpec = (checkSpec options) {
|
||||
csCheckSourced = True
|
||||
}
|
||||
}
|
||||
|
||||
Flag "severity" severity -> do
|
||||
option <- parseSeverityOption severity
|
||||
return options {
|
||||
checkSpec = (checkSpec options) {
|
||||
csMinSeverity = option
|
||||
}
|
||||
}
|
||||
|
||||
Flag "wiki-link-count" countString -> do
|
||||
count <- parseNum countString
|
||||
return options {
|
||||
formatterOptions = (formatterOptions options) {
|
||||
foWikiLinkCount = count
|
||||
}
|
||||
}
|
||||
|
||||
Flag "norc" _ ->
|
||||
return options {
|
||||
checkSpec = (checkSpec options) {
|
||||
csIgnoreRC = True
|
||||
}
|
||||
}
|
||||
|
||||
Flag "enable" value ->
|
||||
let cs = checkSpec options in return options {
|
||||
checkSpec = cs {
|
||||
csOptionalChecks = (csOptionalChecks cs) ++ split ',' value
|
||||
}
|
||||
}
|
||||
|
||||
-- This flag is handled specially in 'process'
|
||||
Flag "format" _ -> return options
|
||||
|
||||
Flag str _ -> do
|
||||
printErr $ "Internal error for --" ++ str ++ ". Please file a bug :("
|
||||
return options
|
||||
where
|
||||
die s = do
|
||||
printErr s
|
||||
throwError SupportFailure
|
||||
parseNum ('S':'C':str) = parseNum str
|
||||
parseNum num = do
|
||||
unless (all isDigit num) $ do
|
||||
printErr $ "Invalid number: " ++ num
|
||||
throwError SyntaxFailure
|
||||
return (Prelude.read num :: Integer)
|
||||
|
||||
ioInterface :: Options -> [FilePath] -> IO (SystemInterface IO)
|
||||
ioInterface options files = do
|
||||
inputs <- mapM normalize files
|
||||
cache <- newIORef emptyCache
|
||||
configCache <- newIORef ("", Nothing)
|
||||
return SystemInterface {
|
||||
siReadFile = get cache inputs,
|
||||
siFindSource = findSourceFile inputs (sourcePaths options),
|
||||
siGetConfig = getConfig configCache
|
||||
}
|
||||
where
|
||||
emptyCache :: Map.Map FilePath String
|
||||
emptyCache = Map.empty
|
||||
|
||||
get cache inputs rcSuggestsExternal file = do
|
||||
map <- readIORef cache
|
||||
case Map.lookup file map of
|
||||
Just x -> return $ Right x
|
||||
Nothing -> fetch cache inputs rcSuggestsExternal file
|
||||
|
||||
fetch cache inputs rcSuggestsExternal file = do
|
||||
ok <- allowable rcSuggestsExternal inputs file
|
||||
if ok
|
||||
then (do
|
||||
(contents, shouldCache) <- inputFile file
|
||||
when shouldCache $
|
||||
modifyIORef cache $ Map.insert file contents
|
||||
return $ Right contents
|
||||
) `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).")
|
||||
where
|
||||
handler :: IOException -> IO (Either ErrorMessage String)
|
||||
handler ex = return . Left $ show ex
|
||||
|
||||
allowable rcSuggestsExternal inputs x =
|
||||
if fromMaybe (externalSources options) rcSuggestsExternal
|
||||
then return True
|
||||
else do
|
||||
path <- normalize x
|
||||
return $ path `elem` inputs
|
||||
|
||||
normalize x =
|
||||
canonicalizePath x `catch` fallback x
|
||||
where
|
||||
fallback :: FilePath -> IOException -> IO FilePath
|
||||
fallback path _ = return path
|
||||
|
||||
-- Returns the name and contents of .shellcheckrc for the given file
|
||||
getConfig cache filename = do
|
||||
path <- normalize filename
|
||||
let dir = takeDirectory path
|
||||
(previousPath, result) <- readIORef cache
|
||||
if dir == previousPath
|
||||
then return result
|
||||
else do
|
||||
paths <- getConfigPaths dir
|
||||
result <- findConfig paths
|
||||
writeIORef cache (dir, result)
|
||||
return result
|
||||
|
||||
findConfig paths =
|
||||
case paths of
|
||||
(file:rest) -> do
|
||||
contents <- readConfig file
|
||||
if isJust contents
|
||||
then return contents
|
||||
else findConfig rest
|
||||
[] -> return Nothing
|
||||
|
||||
-- Get a list of candidate filenames. This includes .shellcheckrc
|
||||
-- in all parent directories, plus the user's home dir and xdg dir.
|
||||
-- The dot is optional for Windows and Snap users.
|
||||
getConfigPaths dir = do
|
||||
let next = takeDirectory dir
|
||||
rest <- if next /= dir
|
||||
then getConfigPaths next
|
||||
else defaultPaths `catch`
|
||||
((const $ return []) :: IOException -> IO [FilePath])
|
||||
return $ (dir </> ".shellcheckrc") : (dir </> "shellcheckrc") : rest
|
||||
|
||||
defaultPaths = do
|
||||
home <- getAppUserDataDirectory "shellcheckrc"
|
||||
xdg <- getXdgDirectory XdgConfig "shellcheckrc"
|
||||
return [home, xdg]
|
||||
|
||||
readConfig file = do
|
||||
exists <- doesFileExist file
|
||||
if exists
|
||||
then do
|
||||
(contents, _) <- inputFile file `catch` handler file
|
||||
return $ Just (file, contents)
|
||||
else
|
||||
return Nothing
|
||||
where
|
||||
handler :: FilePath -> IOException -> IO (String, Bool)
|
||||
handler file err = do
|
||||
putStrLn $ file ++ ": " ++ show err
|
||||
return ("", True)
|
||||
|
||||
andM a b arg = do
|
||||
first <- a arg
|
||||
if not first then return False else b arg
|
||||
|
||||
findM p = foldr go (pure Nothing)
|
||||
where
|
||||
go x acc = do
|
||||
b <- p x
|
||||
if b then pure (Just x) else acc
|
||||
|
||||
findSourceFile inputs sourcePathFlag currentScript rcSuggestsExternal sourcePathAnnotation original =
|
||||
if isAbsolute original
|
||||
then
|
||||
let (_, relative) = splitDrive original
|
||||
in find relative original
|
||||
else
|
||||
find original original
|
||||
where
|
||||
find filename deflt = do
|
||||
sources <- findM ((allowable rcSuggestsExternal inputs) `andM` doesFileExist) $
|
||||
(adjustPath filename):(map ((</> filename) . adjustPath) $ sourcePathFlag ++ sourcePathAnnotation)
|
||||
case sources of
|
||||
Nothing -> return deflt
|
||||
Just first -> return first
|
||||
scriptdir = dropFileName currentScript
|
||||
adjustPath str =
|
||||
case (splitDirectories str) of
|
||||
("SCRIPTDIR":rest) -> joinPath (scriptdir:rest)
|
||||
_ -> str
|
||||
|
||||
inputFile file = do
|
||||
(handle, shouldCache) <-
|
||||
if file == "-"
|
||||
then return (stdin, True)
|
||||
else do
|
||||
h <- openBinaryFile file ReadMode
|
||||
reopenable <- hIsSeekable h
|
||||
return (h, not reopenable)
|
||||
|
||||
hSetBinaryMode handle True
|
||||
contents <- decodeString <$> hGetContents handle -- closes handle
|
||||
|
||||
seq (length contents) $
|
||||
return (contents, shouldCache)
|
||||
|
||||
-- Decode a char8 string into a utf8 string, with fallback on
|
||||
-- ISO-8859-1. This avoids depending on additional libraries.
|
||||
decodeString = decode
|
||||
where
|
||||
decode [] = []
|
||||
decode (c:rest) | isAscii c = c : decode rest
|
||||
decode (c:rest) =
|
||||
let num = (fromIntegral $ ord c) :: Int
|
||||
next = case num of
|
||||
_ | num >= 0xF8 -> Nothing
|
||||
| num >= 0xF0 -> construct (num .&. 0x07) 3 rest
|
||||
| num >= 0xE0 -> construct (num .&. 0x0F) 2 rest
|
||||
| num >= 0xC0 -> construct (num .&. 0x1F) 1 rest
|
||||
| True -> Nothing
|
||||
in
|
||||
case next of
|
||||
Just (n, remainder) -> chr n : decode remainder
|
||||
Nothing -> c : decode rest
|
||||
|
||||
construct x 0 rest = do
|
||||
guard $ x <= 0x10FFFF
|
||||
return (x, rest)
|
||||
construct x n (c:rest) =
|
||||
let num = (fromIntegral $ ord c) :: Int in
|
||||
if num >= 0x80 && num <= 0xBF
|
||||
then construct ((x `shiftL` 6) .|. (num .&. 0x3f)) (n-1) rest
|
||||
else Nothing
|
||||
construct _ _ _ = Nothing
|
||||
|
||||
|
||||
verifyFiles files =
|
||||
when (null files) $ do
|
||||
printErr "No files specified.\n"
|
||||
printErr $ usageInfo usageHeader options
|
||||
throwError SyntaxFailure
|
||||
|
||||
printVersion = do
|
||||
putStrLn "ShellCheck - shell script analysis tool"
|
||||
putStrLn $ "version: " ++ shellcheckVersion
|
||||
putStrLn "license: GNU General Public License, version 3"
|
||||
putStrLn "website: https://www.shellcheck.net"
|
||||
|
||||
printOptional = do
|
||||
mapM f list
|
||||
where
|
||||
list = sortOn cdName ShellCheck.Analyzer.optionalChecks
|
||||
f item = do
|
||||
putStrLn $ "name: " ++ cdName item
|
||||
putStrLn $ "desc: " ++ cdDescription item
|
||||
putStrLn $ "example: " ++ cdPositive item
|
||||
putStrLn $ "fix: " ++ cdNegative item
|
||||
putStrLn ""
|
||||
|
56
snap/snapcraft.yaml
Normal file
56
snap/snapcraft.yaml
Normal file
@@ -0,0 +1,56 @@
|
||||
name: shellcheck
|
||||
summary: A shell script static analysis tool
|
||||
description: |
|
||||
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh
|
||||
shell scripts.
|
||||
|
||||
The goals of ShellCheck are
|
||||
|
||||
- To point out and clarify typical beginner's syntax issues that cause a
|
||||
shell to give cryptic error messages.
|
||||
|
||||
- To point out and clarify typical intermediate level semantic problems that
|
||||
cause a shell to behave strangely and counter-intuitively.
|
||||
|
||||
- To point out subtle caveats, corner cases and pitfalls that may cause an
|
||||
advanced user's otherwise working script to fail under future
|
||||
circumstances.
|
||||
|
||||
By default ShellCheck can only check non-hidden files under /home, to make
|
||||
ShellCheck be able to check files under /media and /run/media you must
|
||||
connect it to the `removable-media` interface manually:
|
||||
|
||||
# snap connect shellcheck:removable-media
|
||||
|
||||
version: git
|
||||
base: core18
|
||||
grade: stable
|
||||
confinement: strict
|
||||
|
||||
apps:
|
||||
shellcheck:
|
||||
command: usr/bin/shellcheck
|
||||
plugs: [home, removable-media]
|
||||
environment:
|
||||
LANG: C.UTF-8
|
||||
|
||||
parts:
|
||||
shellcheck:
|
||||
plugin: dump
|
||||
source: .
|
||||
build-packages:
|
||||
- cabal-install
|
||||
- squid
|
||||
override-build: |
|
||||
# See comments in .snapsquid.conf
|
||||
[ "$http_proxy" ] && {
|
||||
squid3 -f .snapsquid.conf
|
||||
export http_proxy="http://localhost:8888"
|
||||
sleep 3
|
||||
}
|
||||
cabal sandbox init
|
||||
cabal update || cat /var/log/squid/*
|
||||
cabal install -j
|
||||
|
||||
install -d $SNAPCRAFT_PART_INSTALL/usr/bin
|
||||
install .cabal-sandbox/bin/shellcheck $SNAPCRAFT_PART_INSTALL/usr/bin
|
285
src/ShellCheck/AST.hs
Normal file
285
src/ShellCheck/AST.hs
Normal file
@@ -0,0 +1,285 @@
|
||||
{-
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE DeriveGeneric, DeriveAnyClass, DeriveTraversable, PatternSynonyms #-}
|
||||
module ShellCheck.AST where
|
||||
|
||||
import GHC.Generics (Generic)
|
||||
import Control.Monad.Identity
|
||||
import Control.DeepSeq
|
||||
import Text.Parsec
|
||||
import qualified ShellCheck.Regex as Re
|
||||
import Prelude hiding (id)
|
||||
|
||||
newtype Id = Id Int deriving (Show, Eq, Ord, Generic, NFData)
|
||||
|
||||
data Quoted = Quoted | Unquoted deriving (Show, Eq)
|
||||
data Dashed = Dashed | Undashed deriving (Show, Eq)
|
||||
data AssignmentMode = Assign | Append deriving (Show, Eq)
|
||||
newtype FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
|
||||
newtype FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
|
||||
data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
|
||||
|
||||
newtype Root = Root Token
|
||||
data Token = OuterToken Id (InnerToken Token) deriving (Show)
|
||||
|
||||
data InnerToken t =
|
||||
Inner_TA_Binary String t t
|
||||
| Inner_TA_Assignment String t t
|
||||
| Inner_TA_Variable String [t]
|
||||
| Inner_TA_Expansion [t]
|
||||
| Inner_TA_Sequence [t]
|
||||
| Inner_TA_Trinary t t t
|
||||
| Inner_TA_Unary String t
|
||||
| Inner_TC_And ConditionType String t t
|
||||
| Inner_TC_Binary ConditionType String t t
|
||||
| Inner_TC_Group ConditionType t
|
||||
| Inner_TC_Nullary ConditionType t
|
||||
| Inner_TC_Or ConditionType String t t
|
||||
| Inner_TC_Unary ConditionType String t
|
||||
| Inner_TC_Empty ConditionType
|
||||
| Inner_T_AND_IF
|
||||
| Inner_T_AndIf t t
|
||||
| Inner_T_Arithmetic t
|
||||
| Inner_T_Array [t]
|
||||
| Inner_T_IndexedElement [t] t
|
||||
-- Store the index as string, and parse as arithmetic or string later
|
||||
| Inner_T_UnparsedIndex SourcePos String
|
||||
| Inner_T_Assignment AssignmentMode String [t] t
|
||||
| Inner_T_Backgrounded t
|
||||
| Inner_T_Backticked [t]
|
||||
| Inner_T_Bang
|
||||
| Inner_T_Banged t
|
||||
| Inner_T_BraceExpansion [t]
|
||||
| Inner_T_BraceGroup [t]
|
||||
| Inner_T_CLOBBER
|
||||
| Inner_T_Case
|
||||
| Inner_T_CaseExpression t [(CaseType, [t], [t])]
|
||||
| Inner_T_Condition ConditionType t
|
||||
| Inner_T_DGREAT
|
||||
| Inner_T_DLESS
|
||||
| Inner_T_DLESSDASH
|
||||
| Inner_T_DSEMI
|
||||
| Inner_T_Do
|
||||
| Inner_T_DollarArithmetic t
|
||||
| Inner_T_DollarBraced Bool t
|
||||
| Inner_T_DollarBracket t
|
||||
| Inner_T_DollarDoubleQuoted [t]
|
||||
| Inner_T_DollarExpansion [t]
|
||||
| Inner_T_DollarSingleQuoted String
|
||||
| Inner_T_DollarBraceCommandExpansion [t]
|
||||
| Inner_T_Done
|
||||
| Inner_T_DoubleQuoted [t]
|
||||
| Inner_T_EOF
|
||||
| Inner_T_Elif
|
||||
| Inner_T_Else
|
||||
| Inner_T_Esac
|
||||
| Inner_T_Extglob String [t]
|
||||
| Inner_T_FdRedirect String t
|
||||
| Inner_T_Fi
|
||||
| Inner_T_For
|
||||
| Inner_T_ForArithmetic t t t [t]
|
||||
| Inner_T_ForIn String [t] [t]
|
||||
| Inner_T_Function FunctionKeyword FunctionParentheses String t
|
||||
| Inner_T_GREATAND
|
||||
| Inner_T_Glob String
|
||||
| Inner_T_Greater
|
||||
| Inner_T_HereDoc Dashed Quoted String [t]
|
||||
| Inner_T_HereString t
|
||||
| Inner_T_If
|
||||
| Inner_T_IfExpression [([t],[t])] [t]
|
||||
| Inner_T_In
|
||||
| Inner_T_IoFile t t
|
||||
| Inner_T_IoDuplicate t String
|
||||
| Inner_T_LESSAND
|
||||
| Inner_T_LESSGREAT
|
||||
| Inner_T_Lbrace
|
||||
| Inner_T_Less
|
||||
| Inner_T_Literal String
|
||||
| Inner_T_Lparen
|
||||
| Inner_T_NEWLINE
|
||||
| Inner_T_NormalWord [t]
|
||||
| Inner_T_OR_IF
|
||||
| Inner_T_OrIf t t
|
||||
| Inner_T_ParamSubSpecialChar String -- e.g. '%' in ${foo%bar} or '/' in ${foo/bar/baz}
|
||||
| Inner_T_Pipeline [t] [t] -- [Pipe separators] [Commands]
|
||||
| Inner_T_ProcSub String [t]
|
||||
| Inner_T_Rbrace
|
||||
| Inner_T_Redirecting [t] t
|
||||
| Inner_T_Rparen
|
||||
| Inner_T_Script t [t] -- Shebang T_Literal, followed by script.
|
||||
| Inner_T_Select
|
||||
| Inner_T_SelectIn String [t] [t]
|
||||
| Inner_T_Semi
|
||||
| Inner_T_SimpleCommand [t] [t]
|
||||
| Inner_T_SingleQuoted String
|
||||
| Inner_T_Subshell [t]
|
||||
| Inner_T_Then
|
||||
| Inner_T_Until
|
||||
| Inner_T_UntilExpression [t] [t]
|
||||
| Inner_T_While
|
||||
| Inner_T_WhileExpression [t] [t]
|
||||
| Inner_T_Annotation [Annotation] t
|
||||
| Inner_T_Pipe String
|
||||
| Inner_T_CoProc (Maybe String) t
|
||||
| Inner_T_CoProcBody t
|
||||
| Inner_T_Include t
|
||||
| Inner_T_SourceCommand t t
|
||||
| Inner_T_BatsTest t t
|
||||
deriving (Show, Eq, Functor, Foldable, Traversable)
|
||||
|
||||
data Annotation =
|
||||
DisableComment Integer Integer -- [from, to)
|
||||
| EnableComment String
|
||||
| SourceOverride String
|
||||
| ShellOverride String
|
||||
| SourcePath String
|
||||
| ExternalSources Bool
|
||||
deriving (Show, Eq)
|
||||
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
|
||||
|
||||
pattern T_AND_IF id = OuterToken id Inner_T_AND_IF
|
||||
pattern T_Bang id = OuterToken id Inner_T_Bang
|
||||
pattern T_Case id = OuterToken id Inner_T_Case
|
||||
pattern TC_Empty id typ = OuterToken id (Inner_TC_Empty typ)
|
||||
pattern T_CLOBBER id = OuterToken id Inner_T_CLOBBER
|
||||
pattern T_DGREAT id = OuterToken id Inner_T_DGREAT
|
||||
pattern T_DLESS id = OuterToken id Inner_T_DLESS
|
||||
pattern T_DLESSDASH id = OuterToken id Inner_T_DLESSDASH
|
||||
pattern T_Do id = OuterToken id Inner_T_Do
|
||||
pattern T_DollarSingleQuoted id str = OuterToken id (Inner_T_DollarSingleQuoted str)
|
||||
pattern T_Done id = OuterToken id Inner_T_Done
|
||||
pattern T_DSEMI id = OuterToken id Inner_T_DSEMI
|
||||
pattern T_Elif id = OuterToken id Inner_T_Elif
|
||||
pattern T_Else id = OuterToken id Inner_T_Else
|
||||
pattern T_EOF id = OuterToken id Inner_T_EOF
|
||||
pattern T_Esac id = OuterToken id Inner_T_Esac
|
||||
pattern T_Fi id = OuterToken id Inner_T_Fi
|
||||
pattern T_For id = OuterToken id Inner_T_For
|
||||
pattern T_Glob id str = OuterToken id (Inner_T_Glob str)
|
||||
pattern T_GREATAND id = OuterToken id Inner_T_GREATAND
|
||||
pattern T_Greater id = OuterToken id Inner_T_Greater
|
||||
pattern T_If id = OuterToken id Inner_T_If
|
||||
pattern T_In id = OuterToken id Inner_T_In
|
||||
pattern T_Lbrace id = OuterToken id Inner_T_Lbrace
|
||||
pattern T_Less id = OuterToken id Inner_T_Less
|
||||
pattern T_LESSAND id = OuterToken id Inner_T_LESSAND
|
||||
pattern T_LESSGREAT id = OuterToken id Inner_T_LESSGREAT
|
||||
pattern T_Literal id str = OuterToken id (Inner_T_Literal str)
|
||||
pattern T_Lparen id = OuterToken id Inner_T_Lparen
|
||||
pattern T_NEWLINE id = OuterToken id Inner_T_NEWLINE
|
||||
pattern T_OR_IF id = OuterToken id Inner_T_OR_IF
|
||||
pattern T_ParamSubSpecialChar id str = OuterToken id (Inner_T_ParamSubSpecialChar str)
|
||||
pattern T_Pipe id str = OuterToken id (Inner_T_Pipe str)
|
||||
pattern T_Rbrace id = OuterToken id Inner_T_Rbrace
|
||||
pattern T_Rparen id = OuterToken id Inner_T_Rparen
|
||||
pattern T_Select id = OuterToken id Inner_T_Select
|
||||
pattern T_Semi id = OuterToken id Inner_T_Semi
|
||||
pattern T_SingleQuoted id str = OuterToken id (Inner_T_SingleQuoted str)
|
||||
pattern T_Then id = OuterToken id Inner_T_Then
|
||||
pattern T_UnparsedIndex id pos str = OuterToken id (Inner_T_UnparsedIndex pos str)
|
||||
pattern T_Until id = OuterToken id Inner_T_Until
|
||||
pattern T_While id = OuterToken id Inner_T_While
|
||||
pattern TA_Assignment id op t1 t2 = OuterToken id (Inner_TA_Assignment op t1 t2)
|
||||
pattern TA_Binary id op t1 t2 = OuterToken id (Inner_TA_Binary op t1 t2)
|
||||
pattern TA_Expansion id t = OuterToken id (Inner_TA_Expansion t)
|
||||
pattern T_AndIf id t u = OuterToken id (Inner_T_AndIf t u)
|
||||
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_Array id t = OuterToken id (Inner_T_Array t)
|
||||
pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)
|
||||
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_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1)
|
||||
pattern TA_Variable id str t = OuterToken id (Inner_TA_Variable str t)
|
||||
pattern T_Backgrounded id l = OuterToken id (Inner_T_Backgrounded l)
|
||||
pattern T_Backticked id list = OuterToken id (Inner_T_Backticked list)
|
||||
pattern T_Banged id l = OuterToken id (Inner_T_Banged l)
|
||||
pattern T_BatsTest id name t = OuterToken id (Inner_T_BatsTest name t)
|
||||
pattern T_BraceExpansion id list = OuterToken id (Inner_T_BraceExpansion list)
|
||||
pattern T_BraceGroup id l = OuterToken id (Inner_T_BraceGroup l)
|
||||
pattern TC_And id typ str t1 t2 = OuterToken id (Inner_TC_And typ str t1 t2)
|
||||
pattern T_CaseExpression id word cases = OuterToken id (Inner_T_CaseExpression word cases)
|
||||
pattern TC_Binary id typ op lhs rhs = OuterToken id (Inner_TC_Binary typ op lhs rhs)
|
||||
pattern TC_Group id typ token = OuterToken id (Inner_TC_Group typ token)
|
||||
pattern TC_Nullary id typ token = OuterToken id (Inner_TC_Nullary typ token)
|
||||
pattern T_Condition id typ token = OuterToken id (Inner_T_Condition typ token)
|
||||
pattern T_CoProcBody id t = OuterToken id (Inner_T_CoProcBody t)
|
||||
pattern T_CoProc id var body = OuterToken id (Inner_T_CoProc var body)
|
||||
pattern TC_Or id typ str t1 t2 = OuterToken id (Inner_TC_Or typ str t1 t2)
|
||||
pattern TC_Unary id typ op token = OuterToken id (Inner_TC_Unary typ op token)
|
||||
pattern T_DollarArithmetic id c = OuterToken id (Inner_T_DollarArithmetic c)
|
||||
pattern T_DollarBraceCommandExpansion id list = OuterToken id (Inner_T_DollarBraceCommandExpansion list)
|
||||
pattern T_DollarBraced id braced op = OuterToken id (Inner_T_DollarBraced braced op)
|
||||
pattern T_DollarBracket id c = OuterToken id (Inner_T_DollarBracket c)
|
||||
pattern T_DollarDoubleQuoted id list = OuterToken id (Inner_T_DollarDoubleQuoted list)
|
||||
pattern T_DollarExpansion id list = OuterToken id (Inner_T_DollarExpansion list)
|
||||
pattern T_DoubleQuoted id list = OuterToken id (Inner_T_DoubleQuoted list)
|
||||
pattern T_Extglob id str l = OuterToken id (Inner_T_Extglob str l)
|
||||
pattern T_FdRedirect id v t = OuterToken id (Inner_T_FdRedirect v t)
|
||||
pattern T_ForArithmetic id a b c group = OuterToken id (Inner_T_ForArithmetic a b c group)
|
||||
pattern T_ForIn id v w l = OuterToken id (Inner_T_ForIn v w l)
|
||||
pattern T_Function id a b name body = OuterToken id (Inner_T_Function a b name body)
|
||||
pattern T_HereDoc id d q str l = OuterToken id (Inner_T_HereDoc d q str l)
|
||||
pattern T_HereString id word = OuterToken id (Inner_T_HereString word)
|
||||
pattern T_IfExpression id conditions elses = OuterToken id (Inner_T_IfExpression conditions elses)
|
||||
pattern T_Include id script = OuterToken id (Inner_T_Include script)
|
||||
pattern T_IndexedElement id indices t = OuterToken id (Inner_T_IndexedElement indices t)
|
||||
pattern T_IoDuplicate id op num = OuterToken id (Inner_T_IoDuplicate op num)
|
||||
pattern T_IoFile id op file = OuterToken id (Inner_T_IoFile op file)
|
||||
pattern T_NormalWord id list = OuterToken id (Inner_T_NormalWord list)
|
||||
pattern T_OrIf id t u = OuterToken id (Inner_T_OrIf t u)
|
||||
pattern T_Pipeline id l1 l2 = OuterToken id (Inner_T_Pipeline l1 l2)
|
||||
pattern T_ProcSub id typ l = OuterToken id (Inner_T_ProcSub typ l)
|
||||
pattern T_Redirecting id redirs cmd = OuterToken id (Inner_T_Redirecting redirs cmd)
|
||||
pattern T_Script id shebang list = OuterToken id (Inner_T_Script shebang list)
|
||||
pattern T_SelectIn id v w l = OuterToken id (Inner_T_SelectIn v w l)
|
||||
pattern T_SimpleCommand id vars cmds = OuterToken id (Inner_T_SimpleCommand vars cmds)
|
||||
pattern T_SourceCommand id includer t_include = OuterToken id (Inner_T_SourceCommand includer t_include)
|
||||
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_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 #-}
|
||||
|
||||
instance Eq Token where
|
||||
OuterToken _ a == OuterToken _ b = a == b
|
||||
|
||||
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> m Token) -> Token -> m Token
|
||||
analyze f g i =
|
||||
round
|
||||
where
|
||||
round t@(OuterToken id it) = do
|
||||
f t
|
||||
newIt <- traverse round it
|
||||
g t
|
||||
i (OuterToken id newIt)
|
||||
|
||||
getId :: Token -> Id
|
||||
getId (OuterToken id _) = id
|
||||
|
||||
blank :: Monad m => Token -> m ()
|
||||
blank = const $ return ()
|
||||
doAnalysis :: Monad m => (Token -> m ()) -> Token -> m Token
|
||||
doAnalysis f = analyze f blank return
|
||||
doStackAnalysis :: Monad m => (Token -> m ()) -> (Token -> m ()) -> Token -> m Token
|
||||
doStackAnalysis startToken endToken = analyze startToken endToken return
|
||||
doTransform :: (Token -> Token) -> Token -> Token
|
||||
doTransform i = runIdentity . analyze blank blank (return . i)
|
||||
|
768
src/ShellCheck/ASTLib.hs
Normal file
768
src/ShellCheck/ASTLib.hs
Normal file
@@ -0,0 +1,768 @@
|
||||
{-
|
||||
Copyright 2012-2021 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 #-}
|
||||
module ShellCheck.ASTLib where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Monad.Writer
|
||||
import Control.Monad
|
||||
import Data.Char
|
||||
import Data.Functor
|
||||
import Data.Functor.Identity
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import qualified Data.Map as Map
|
||||
import Numeric (showHex)
|
||||
|
||||
import Test.QuickCheck
|
||||
|
||||
arguments (T_SimpleCommand _ _ (cmd:args)) = args
|
||||
|
||||
-- Is this a type of loop?
|
||||
isLoop t = case t of
|
||||
T_WhileExpression {} -> True
|
||||
T_UntilExpression {} -> True
|
||||
T_ForIn {} -> True
|
||||
T_ForArithmetic {} -> True
|
||||
T_SelectIn {} -> True
|
||||
_ -> False
|
||||
|
||||
-- Will this split into multiple words when used as an argument?
|
||||
willSplit x =
|
||||
case x of
|
||||
T_DollarBraced {} -> True
|
||||
T_DollarExpansion {} -> True
|
||||
T_Backticked {} -> True
|
||||
T_BraceExpansion {} -> True
|
||||
T_Glob {} -> True
|
||||
T_Extglob {} -> True
|
||||
T_DoubleQuoted _ l -> any willBecomeMultipleArgs l
|
||||
T_NormalWord _ l -> any willSplit l
|
||||
_ -> False
|
||||
|
||||
isGlob t = case t of
|
||||
T_Extglob {} -> True
|
||||
T_Glob {} -> True
|
||||
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?
|
||||
isConstant token =
|
||||
case token of
|
||||
-- This ignores some cases like ~"foo":
|
||||
T_NormalWord _ (T_Literal _ ('~':_) : _) -> False
|
||||
T_NormalWord _ l -> all isConstant l
|
||||
T_DoubleQuoted _ l -> all isConstant l
|
||||
T_SingleQuoted _ _ -> True
|
||||
T_Literal _ _ -> True
|
||||
_ -> False
|
||||
|
||||
-- Is this an empty literal?
|
||||
isEmpty token =
|
||||
case token of
|
||||
T_NormalWord _ l -> all isEmpty l
|
||||
T_DoubleQuoted _ l -> all isEmpty l
|
||||
T_SingleQuoted _ "" -> True
|
||||
T_Literal _ "" -> True
|
||||
_ -> False
|
||||
|
||||
-- Quick&lazy oversimplification of commands, throwing away details
|
||||
-- and returning a list like ["find", ".", "-name", "${VAR}*" ].
|
||||
oversimplify token =
|
||||
case token of
|
||||
(T_NormalWord _ l) -> [concat (concatMap oversimplify l)]
|
||||
(T_DoubleQuoted _ l) -> [concat (concatMap oversimplify l)]
|
||||
(T_SingleQuoted _ s) -> [s]
|
||||
(T_DollarBraced _ _ _) -> ["${VAR}"]
|
||||
(T_DollarArithmetic _ _) -> ["${VAR}"]
|
||||
(T_DollarExpansion _ _) -> ["${VAR}"]
|
||||
(T_Backticked _ _) -> ["${VAR}"]
|
||||
(T_Glob _ s) -> [s]
|
||||
(T_Pipeline _ _ [x]) -> oversimplify x
|
||||
(T_Literal _ x) -> [x]
|
||||
(T_ParamSubSpecialChar _ x) -> [x]
|
||||
(T_SimpleCommand _ vars words) -> concatMap oversimplify words
|
||||
(T_Redirecting _ _ foo) -> oversimplify foo
|
||||
(T_DollarSingleQuoted _ s) -> [s]
|
||||
(T_Annotation _ _ s) -> oversimplify s
|
||||
-- Workaround for let "foo = bar" parsing
|
||||
(TA_Sequence _ [TA_Expansion _ v]) -> concatMap oversimplify v
|
||||
_ -> []
|
||||
|
||||
|
||||
-- Turn a SimpleCommand foo -avz --bar=baz into args "a", "v", "z", "bar",
|
||||
-- each in a tuple of (token, stringFlag). Non-flag arguments are added with
|
||||
-- stringFlag == "".
|
||||
getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
|
||||
let tokenAndText = map (\x -> (x, concat $ oversimplify x)) args
|
||||
(flagArgs, rest) = break (stopCondition . snd) tokenAndText
|
||||
in
|
||||
concatMap flag flagArgs ++ map (\(t, _) -> (t, "")) rest
|
||||
where
|
||||
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
|
||||
flag (x, '-':args) = map (\v -> (x, [v])) args
|
||||
flag (x, _) = [ (x, "") ]
|
||||
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
|
||||
|
||||
-- Get all flags in a GNU way, up until --
|
||||
getAllFlags :: Token -> [(Token, String)]
|
||||
getAllFlags = getFlagsUntil (== "--")
|
||||
-- Get all flags in a BSD way, up until first non-flag argument or --
|
||||
getLeadingFlags = getFlagsUntil (\x -> x == "--" || (not $ "-" `isPrefixOf` x))
|
||||
|
||||
-- Check if a command has a flag.
|
||||
hasFlag cmd str = str `elem` (map snd $ getAllFlags cmd)
|
||||
|
||||
-- Is this token a word that starts with a dash?
|
||||
isFlag token =
|
||||
case getWordParts token of
|
||||
T_Literal _ ('-':_) : _ -> True
|
||||
_ -> False
|
||||
|
||||
-- Is this token a flag where the - is unquoted?
|
||||
isUnquotedFlag token = fromMaybe False $ do
|
||||
str <- getLeadingUnquotedString token
|
||||
return $ "-" `isPrefixOf` str
|
||||
|
||||
-- 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 [] = []
|
||||
|
||||
|
||||
-- Is this an expansion of multiple items of an array?
|
||||
isArrayExpansion (T_DollarBraced _ _ l) =
|
||||
let string = concat $ oversimplify l in
|
||||
"@" `isPrefixOf` string ||
|
||||
not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string
|
||||
isArrayExpansion _ = False
|
||||
|
||||
-- Is it possible that this arg becomes multiple args?
|
||||
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f False t
|
||||
where
|
||||
f quoted (T_DollarBraced _ _ l) =
|
||||
let string = concat $ oversimplify l in
|
||||
not quoted || "!" `isPrefixOf` string
|
||||
f quoted (T_DoubleQuoted _ parts) = any (f True) parts
|
||||
f quoted (T_NormalWord _ parts) = any (f quoted) parts
|
||||
f _ _ = False
|
||||
|
||||
-- Is it certain that this word will becomes multiple words?
|
||||
willBecomeMultipleArgs t = willConcatInAssignment t || f t
|
||||
where
|
||||
f T_Extglob {} = True
|
||||
f T_Glob {} = True
|
||||
f T_BraceExpansion {} = True
|
||||
f (T_NormalWord _ parts) = any f parts
|
||||
f _ = False
|
||||
|
||||
-- This does token cause implicit concatenation in assignments?
|
||||
willConcatInAssignment token =
|
||||
case token of
|
||||
t@T_DollarBraced {} -> isArrayExpansion t
|
||||
(T_DoubleQuoted _ parts) -> any willConcatInAssignment parts
|
||||
(T_NormalWord _ parts) -> any willConcatInAssignment parts
|
||||
_ -> False
|
||||
|
||||
-- Maybe get the literal string corresponding to this token
|
||||
getLiteralString :: Token -> Maybe String
|
||||
getLiteralString = getLiteralStringExt (const Nothing)
|
||||
|
||||
-- Definitely get a literal string, with a given default for all non-literals
|
||||
getLiteralStringDef :: String -> Token -> String
|
||||
getLiteralStringDef x = runIdentity . getLiteralStringExt (const $ return x)
|
||||
|
||||
-- Definitely get a literal string, skipping over all non-literals
|
||||
onlyLiteralString :: Token -> String
|
||||
onlyLiteralString = getLiteralStringDef ""
|
||||
|
||||
-- Maybe get a literal string, but only if it's an unquoted argument.
|
||||
getUnquotedLiteral (T_NormalWord _ list) =
|
||||
concat <$> mapM str list
|
||||
where
|
||||
str (T_Literal _ s) = return s
|
||||
str _ = 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
|
||||
-- or nothing if the word does not end in an unquoted literal.
|
||||
getTrailingUnquotedLiteral :: Token -> Maybe Token
|
||||
getTrailingUnquotedLiteral t =
|
||||
case t of
|
||||
(T_NormalWord _ list@(_:_)) ->
|
||||
from (last list)
|
||||
_ -> Nothing
|
||||
where
|
||||
from t =
|
||||
case t of
|
||||
T_Literal {} -> return t
|
||||
_ -> Nothing
|
||||
|
||||
-- Get the leading, unquoted, literal string of a token (if any).
|
||||
getLeadingUnquotedString :: Token -> Maybe String
|
||||
getLeadingUnquotedString t =
|
||||
case t of
|
||||
T_NormalWord _ ((T_Literal _ s) : rest) -> return $ s ++ from rest
|
||||
_ -> Nothing
|
||||
where
|
||||
from ((T_Literal _ s):rest) = s ++ from rest
|
||||
from _ = ""
|
||||
|
||||
-- Maybe get the literal string of this token and any globs in it.
|
||||
getGlobOrLiteralString = getLiteralStringExt f
|
||||
where
|
||||
f (T_Glob _ str) = return str
|
||||
f _ = Nothing
|
||||
|
||||
-- Maybe get the literal value of a token, using a custom function
|
||||
-- to map unrecognized Tokens into strings.
|
||||
getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String
|
||||
getLiteralStringExt more = g
|
||||
where
|
||||
allInList = fmap concat . mapM g
|
||||
g (T_DoubleQuoted _ l) = allInList l
|
||||
g (T_DollarDoubleQuoted _ l) = allInList l
|
||||
g (T_NormalWord _ l) = allInList l
|
||||
g (TA_Expansion _ l) = allInList l
|
||||
g (T_SingleQuoted _ s) = return s
|
||||
g (T_Literal _ s) = return s
|
||||
g (T_ParamSubSpecialChar _ s) = return s
|
||||
g (T_DollarSingleQuoted _ s) = return $ decodeEscapes s
|
||||
g x = more x
|
||||
|
||||
-- Bash style $'..' decoding
|
||||
decodeEscapes ('\\':c:cs) =
|
||||
case c of
|
||||
'a' -> '\a' : rest
|
||||
'b' -> '\b' : rest
|
||||
'e' -> '\x1B' : rest
|
||||
'f' -> '\f' : rest
|
||||
'n' -> '\n' : rest
|
||||
'r' -> '\r' : rest
|
||||
't' -> '\t' : rest
|
||||
'v' -> '\v' : rest
|
||||
'\'' -> '\'' : rest
|
||||
'"' -> '"' : rest
|
||||
'\\' -> '\\' : rest
|
||||
'x' ->
|
||||
case cs of
|
||||
(x:y:more) ->
|
||||
if isHexDigit x && isHexDigit y
|
||||
then chr (16*(digitToInt x) + (digitToInt y)) : rest
|
||||
else '\\':c:rest
|
||||
_ | isOctDigit c ->
|
||||
let digits = take 3 $ takeWhile isOctDigit (c:cs)
|
||||
num = parseOct digits
|
||||
in (if num < 256 then chr num else '?') : rest
|
||||
_ -> '\\' : c : rest
|
||||
where
|
||||
rest = decodeEscapes cs
|
||||
parseOct = f 0
|
||||
where
|
||||
f n "" = n
|
||||
f n (c:rest) = f (n * 8 + digitToInt c) rest
|
||||
decodeEscapes (c:cs) = c : decodeEscapes cs
|
||||
decodeEscapes [] = []
|
||||
|
||||
-- Is this token a string literal?
|
||||
isLiteral t = isJust $ getLiteralString t
|
||||
|
||||
-- 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]
|
||||
getWordParts (T_NormalWord _ l) = concatMap getWordParts l
|
||||
getWordParts (T_DoubleQuoted _ l) = l
|
||||
-- TA_Expansion is basically T_NormalWord for arithmetic expressions
|
||||
getWordParts (TA_Expansion _ l) = concatMap getWordParts l
|
||||
getWordParts other = [other]
|
||||
|
||||
-- Return a list of NormalWords that would result from brace expansion
|
||||
braceExpand (T_NormalWord id list) = take 1000 $ do
|
||||
items <- mapM part list
|
||||
return $ T_NormalWord id items
|
||||
where
|
||||
part (T_BraceExpansion id items) = do
|
||||
item <- items
|
||||
braceExpand item
|
||||
part x = return x
|
||||
|
||||
-- Maybe get a SimpleCommand from immediate wrappers like T_Redirections
|
||||
getCommand t =
|
||||
case t of
|
||||
T_Redirecting _ _ w -> getCommand w
|
||||
T_SimpleCommand _ _ (w:_) -> return t
|
||||
T_Annotation _ _ t -> getCommand t
|
||||
_ -> Nothing
|
||||
|
||||
-- Maybe get the command name string of a token representing a command
|
||||
getCommandName :: Token -> Maybe String
|
||||
getCommandName = fst . getCommandNameAndToken False
|
||||
|
||||
-- Maybe get the name+arguments of a command.
|
||||
getCommandArgv t = do
|
||||
(T_SimpleCommand _ _ args@(_:_)) <- getCommand t
|
||||
return args
|
||||
|
||||
-- Get the command name token from a command, i.e.
|
||||
-- the token representing 'ls' in 'ls -la 2> foo'.
|
||||
-- If it can't be determined, return the original token.
|
||||
getCommandTokenOrThis = snd . getCommandNameAndToken False
|
||||
|
||||
-- Given a command, get the string and token that represents the command name.
|
||||
-- If direct, return the actual command (e.g. exec in 'exec ls')
|
||||
-- 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
|
||||
return $ fromMaybe (Just s, w) $ do
|
||||
guard $ not direct
|
||||
actual <- getEffectiveCommandToken s cmd rest
|
||||
return (getLiteralString actual, actual)
|
||||
where
|
||||
getEffectiveCommandToken str cmd args =
|
||||
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.
|
||||
-- $(date +%s) = Just "date"
|
||||
getCommandNameFromExpansion :: Token -> Maybe String
|
||||
getCommandNameFromExpansion t =
|
||||
case t of
|
||||
T_DollarExpansion _ [c] -> extract c
|
||||
T_Backticked _ [c] -> extract c
|
||||
T_DollarBraceCommandExpansion _ [c] -> extract c
|
||||
_ -> Nothing
|
||||
where
|
||||
extract (T_Pipeline _ _ [cmd]) = getCommandName cmd
|
||||
extract _ = Nothing
|
||||
|
||||
-- Get the basename of a token representing a command
|
||||
getCommandBasename = fmap basename . getCommandName
|
||||
|
||||
basename = reverse . takeWhile (/= '/') . reverse
|
||||
|
||||
isAssignment t =
|
||||
case t of
|
||||
T_Redirecting _ _ w -> isAssignment w
|
||||
T_SimpleCommand _ (w:_) [] -> True
|
||||
T_Assignment {} -> True
|
||||
T_Annotation _ _ w -> isAssignment w
|
||||
_ -> False
|
||||
|
||||
isOnlyRedirection t =
|
||||
case t of
|
||||
T_Pipeline _ _ [x] -> isOnlyRedirection x
|
||||
T_Annotation _ _ w -> isOnlyRedirection w
|
||||
T_Redirecting _ (_:_) c -> isOnlyRedirection c
|
||||
T_SimpleCommand _ [] [] -> True
|
||||
_ -> False
|
||||
|
||||
isFunction t = case t of T_Function {} -> True; _ -> False
|
||||
|
||||
-- Bats tests are functions for the purpose of 'local' and such
|
||||
isFunctionLike t =
|
||||
case t of
|
||||
T_Function {} -> True
|
||||
T_BatsTest {} -> True
|
||||
_ -> False
|
||||
|
||||
|
||||
isBraceExpansion t = case t of T_BraceExpansion {} -> True; _ -> False
|
||||
|
||||
-- Get the lists of commands from tokens that contain them, such as
|
||||
-- the conditions and bodies of while loops or branches of if statements.
|
||||
getCommandSequences :: Token -> [[Token]]
|
||||
getCommandSequences t =
|
||||
case t of
|
||||
T_Script _ _ cmds -> [cmds]
|
||||
T_BraceGroup _ cmds -> [cmds]
|
||||
T_Subshell _ cmds -> [cmds]
|
||||
T_WhileExpression _ cond cmds -> [cond, cmds]
|
||||
T_UntilExpression _ cond cmds -> [cond, cmds]
|
||||
T_ForIn _ _ _ cmds -> [cmds]
|
||||
T_ForArithmetic _ _ _ _ cmds -> [cmds]
|
||||
T_IfExpression _ thens elses -> (concatMap (\(a,b) -> [a,b]) thens) ++ [elses]
|
||||
T_Annotation _ _ t -> getCommandSequences t
|
||||
|
||||
T_DollarExpansion _ cmds -> [cmds]
|
||||
T_DollarBraceCommandExpansion _ cmds -> [cmds]
|
||||
T_Backticked _ cmds -> [cmds]
|
||||
_ -> []
|
||||
|
||||
-- Get a list of names of associative arrays
|
||||
getAssociativeArrays t =
|
||||
nub . execWriter $ doAnalysis f t
|
||||
where
|
||||
f :: Token -> Writer [String] ()
|
||||
f t@T_SimpleCommand {} = sequence_ $ do
|
||||
name <- getCommandName t
|
||||
let assocNames = ["declare","local","typeset"]
|
||||
guard $ name `elem` assocNames
|
||||
let flags = getAllFlags t
|
||||
guard $ "A" `elem` map snd flags
|
||||
let args = [arg | (arg, "") <- flags]
|
||||
let names = mapMaybe (getLiteralStringExt nameAssignments) args
|
||||
return $ tell names
|
||||
f _ = return ()
|
||||
|
||||
nameAssignments t =
|
||||
case t of
|
||||
T_Assignment _ _ name _ _ -> return name
|
||||
_ -> Nothing
|
||||
|
||||
-- A Pseudoglob is a wildcard pattern used for checking if a match can succeed.
|
||||
-- For example, [[ $(cmd).jpg == [a-z] ]] will give the patterns *.jpg and ?, which
|
||||
-- can be proven never to match.
|
||||
data PseudoGlob = PGAny | PGMany | PGChar Char
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- Turn a word into a PG pattern, replacing all unknown/runtime values with
|
||||
-- PGMany.
|
||||
wordToPseudoGlob :: Token -> [PseudoGlob]
|
||||
wordToPseudoGlob = fromMaybe [PGMany] . wordToPseudoGlob' False
|
||||
|
||||
-- Turn a word into a PG pattern, but only if we can preserve
|
||||
-- exact semantics.
|
||||
wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob]
|
||||
wordToExactPseudoGlob = wordToPseudoGlob' True
|
||||
|
||||
wordToPseudoGlob' :: Bool -> Token -> Maybe [PseudoGlob]
|
||||
wordToPseudoGlob' exact word =
|
||||
simplifyPseudoGlob <$> toGlob word
|
||||
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
|
||||
T_Literal _ s -> return $ map PGChar s
|
||||
T_SingleQuoted _ s -> return $ map PGChar s
|
||||
T_Glob _ "?" -> return [PGAny]
|
||||
T_Glob _ "*" -> return [PGMany]
|
||||
T_Glob _ ('[':_) | not exact -> return [PGAny]
|
||||
_ -> if exact then fail "" else return [PGMany]
|
||||
|
||||
|
||||
-- Reorder a PseudoGlob for more efficient matching, e.g.
|
||||
-- f?*?**g -> f??*g
|
||||
simplifyPseudoGlob :: [PseudoGlob] -> [PseudoGlob]
|
||||
simplifyPseudoGlob = f
|
||||
where
|
||||
f [] = []
|
||||
f (x@(PGChar _) : rest ) = x : f rest
|
||||
f list =
|
||||
let (anys, rest) = span (\x -> x == PGMany || x == PGAny) list in
|
||||
order anys ++ f rest
|
||||
|
||||
order s = let (any, many) = partition (== PGAny) s in
|
||||
any ++ take 1 many
|
||||
|
||||
-- Check whether the two patterns can ever overlap.
|
||||
pseudoGlobsCanOverlap :: [PseudoGlob] -> [PseudoGlob] -> Bool
|
||||
pseudoGlobsCanOverlap = matchable
|
||||
where
|
||||
matchable x@(xf:xs) y@(yf:ys) =
|
||||
case (xf, yf) of
|
||||
(PGMany, _) -> matchable x ys || matchable xs y
|
||||
(_, PGMany) -> matchable x ys || matchable xs y
|
||||
(PGAny, _) -> matchable xs ys
|
||||
(_, PGAny) -> matchable xs ys
|
||||
(_, _) -> xf == yf && matchable xs ys
|
||||
|
||||
matchable [] [] = True
|
||||
matchable (PGMany : rest) [] = matchable rest []
|
||||
matchable (_:_) [] = False
|
||||
matchable [] r = matchable r []
|
||||
|
||||
-- Check whether the first pattern always overlaps the second.
|
||||
pseudoGlobIsSuperSetof :: [PseudoGlob] -> [PseudoGlob] -> Bool
|
||||
pseudoGlobIsSuperSetof = matchable
|
||||
where
|
||||
matchable x@(xf:xs) y@(yf:ys) =
|
||||
case (xf, yf) of
|
||||
(PGMany, PGMany) -> matchable x ys
|
||||
(PGMany, _) -> matchable x ys || matchable xs y
|
||||
(_, PGMany) -> False
|
||||
(PGAny, _) -> matchable xs ys
|
||||
(_, PGAny) -> False
|
||||
(_, _) -> xf == yf && matchable xs ys
|
||||
|
||||
matchable [] [] = True
|
||||
matchable (PGMany : rest) [] = matchable rest []
|
||||
matchable _ _ = False
|
||||
|
||||
wordsCanBeEqual x y = pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
|
||||
|
||||
-- Is this an expansion that can be quoted,
|
||||
-- e.g. $(foo) `foo` $foo (but not {foo,})?
|
||||
isQuoteableExpansion t = case t of
|
||||
T_DollarBraced {} -> True
|
||||
_ -> isCommandSubstitution t
|
||||
|
||||
isCommandSubstitution t = case t of
|
||||
T_DollarExpansion {} -> True
|
||||
T_DollarBraceCommandExpansion {} -> True
|
||||
T_Backticked {} -> True
|
||||
_ -> 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?
|
||||
isAnnotationIgnoringCode code t =
|
||||
case t of
|
||||
T_Annotation _ anns _ -> any hasNum anns
|
||||
_ -> False
|
||||
where
|
||||
hasNum (DisableComment from to) = code >= from && code < to
|
||||
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" == "ash"
|
||||
prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "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" -> "ash" -- busybox sh is 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`)
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
4861
src/ShellCheck/Analytics.hs
Normal file
4861
src/ShellCheck/Analytics.hs
Normal file
File diff suppressed because it is too large
Load Diff
52
src/ShellCheck/Analyzer.hs
Normal file
52
src/ShellCheck/Analyzer.hs
Normal file
@@ -0,0 +1,52 @@
|
||||
{-
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Analyzer (analyzeScript, ShellCheck.Analyzer.optionalChecks) where
|
||||
|
||||
import ShellCheck.Analytics
|
||||
import ShellCheck.AnalyzerLib
|
||||
import ShellCheck.Interface
|
||||
import Data.List
|
||||
import Data.Monoid
|
||||
import qualified ShellCheck.Checks.Commands
|
||||
import qualified ShellCheck.Checks.Custom
|
||||
import qualified ShellCheck.Checks.ShellSupport
|
||||
|
||||
|
||||
-- TODO: Clean up the cruft this is layered on
|
||||
analyzeScript :: AnalysisSpec -> AnalysisResult
|
||||
analyzeScript spec = newAnalysisResult {
|
||||
arComments =
|
||||
filterByAnnotation spec params . nub $
|
||||
runAnalytics spec
|
||||
++ runChecker params (checkers spec params)
|
||||
}
|
||||
where
|
||||
params = makeParameters spec
|
||||
|
||||
checkers spec params = mconcat $ map ($ params) [
|
||||
ShellCheck.Checks.Commands.checker spec,
|
||||
ShellCheck.Checks.Custom.checker,
|
||||
ShellCheck.Checks.ShellSupport.checker
|
||||
]
|
||||
|
||||
optionalChecks = mconcat $ [
|
||||
ShellCheck.Analytics.optionalChecks,
|
||||
ShellCheck.Checks.Commands.optionalChecks
|
||||
]
|
1034
src/ShellCheck/AnalyzerLib.hs
Normal file
1034
src/ShellCheck/AnalyzerLib.hs
Normal file
File diff suppressed because it is too large
Load Diff
488
src/ShellCheck/Checker.hs
Normal file
488
src/ShellCheck/Checker.hs
Normal file
@@ -0,0 +1,488 @@
|
||||
{-
|
||||
Copyright 2012-2020 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 #-}
|
||||
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Analyzer
|
||||
|
||||
import Data.Either
|
||||
import Data.Functor
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import Data.Ord
|
||||
import Control.Monad.Identity
|
||||
import qualified Data.Map as Map
|
||||
import qualified System.IO
|
||||
import Prelude hiding (readFile)
|
||||
import Control.Monad
|
||||
|
||||
import Test.QuickCheck.All
|
||||
|
||||
tokenToPosition startMap t = fromMaybe fail $ do
|
||||
span <- Map.lookup (tcId t) startMap
|
||||
return $ newPositionedComment {
|
||||
pcStartPos = fst span,
|
||||
pcEndPos = snd span,
|
||||
pcComment = tcComment t,
|
||||
pcFix = tcFix t
|
||||
}
|
||||
where
|
||||
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
|
||||
|
||||
shellFromFilename filename = listToMaybe candidates
|
||||
where
|
||||
shellExtensions = [(".ksh", Ksh)
|
||||
,(".bash", Bash)
|
||||
,(".bats", Bash)
|
||||
,(".dash", Dash)]
|
||||
-- The `.sh` is too generic to determine the shell:
|
||||
-- We fallback to Bash in this case and emit SC2148 if there is no shebang
|
||||
candidates =
|
||||
[sh | (ext,sh) <- shellExtensions, ext `isSuffixOf` filename]
|
||||
|
||||
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
|
||||
checkScript sys spec = do
|
||||
results <- checkScript (csScript spec)
|
||||
return emptyCheckResult {
|
||||
crFilename = csFilename spec,
|
||||
crComments = results
|
||||
}
|
||||
where
|
||||
checkScript contents = do
|
||||
result <- parseScript sys newParseSpec {
|
||||
psFilename = csFilename spec,
|
||||
psScript = contents,
|
||||
psCheckSourced = csCheckSourced spec,
|
||||
psIgnoreRC = csIgnoreRC spec,
|
||||
psShellTypeOverride = csShellTypeOverride spec
|
||||
}
|
||||
let parseMessages = prComments result
|
||||
let tokenPositions = prTokenPositions result
|
||||
let analysisSpec root =
|
||||
as {
|
||||
asScript = root,
|
||||
asShellType = csShellTypeOverride spec,
|
||||
asFallbackShell = shellFromFilename $ csFilename spec,
|
||||
asCheckSourced = csCheckSourced spec,
|
||||
asExecutionMode = Executed,
|
||||
asTokenPositions = tokenPositions,
|
||||
asOptionalChecks = csOptionalChecks spec
|
||||
} where as = newAnalysisSpec root
|
||||
let analysisMessages =
|
||||
maybe []
|
||||
(arComments . analyzeScript . analysisSpec)
|
||||
$ prRoot result
|
||||
let translator = tokenToPosition tokenPositions
|
||||
return . nub . sortMessages . filter shouldInclude $
|
||||
(parseMessages ++ map translator analysisMessages)
|
||||
|
||||
shouldInclude pc =
|
||||
severity <= csMinSeverity spec &&
|
||||
case csIncludedWarnings spec of
|
||||
Nothing -> code `notElem` csExcludedWarnings spec
|
||||
Just includedWarnings -> code `elem` includedWarnings
|
||||
where
|
||||
code = cCode (pcComment pc)
|
||||
severity = cSeverity (pcComment pc)
|
||||
|
||||
sortMessages = sortOn order
|
||||
order pc =
|
||||
let pos = pcStartPos pc
|
||||
comment = pcComment pc in
|
||||
(posFile pos,
|
||||
posLine pos,
|
||||
posColumn pos,
|
||||
cSeverity comment,
|
||||
cCode comment,
|
||||
cMessage comment)
|
||||
getPosition = pcStartPos
|
||||
|
||||
|
||||
getErrors sys spec =
|
||||
sort . map getCode . crComments $
|
||||
runIdentity (checkScript sys spec)
|
||||
where
|
||||
getCode = cCode . pcComment
|
||||
|
||||
check = checkWithIncludes []
|
||||
|
||||
checkWithSpec includes =
|
||||
getErrors (mockedSystemInterface includes)
|
||||
|
||||
checkWithIncludes includes src =
|
||||
checkWithSpec includes emptyCheckSpec {
|
||||
csScript = src,
|
||||
csExcludedWarnings = [2148]
|
||||
}
|
||||
|
||||
checkRecursive includes src =
|
||||
checkWithSpec includes emptyCheckSpec {
|
||||
csScript = src,
|
||||
csExcludedWarnings = [2148],
|
||||
csCheckSourced = True
|
||||
}
|
||||
|
||||
checkOptionIncludes includes src =
|
||||
checkWithSpec [] emptyCheckSpec {
|
||||
csScript = src,
|
||||
csIncludedWarnings = includes,
|
||||
csCheckSourced = True
|
||||
}
|
||||
|
||||
checkWithRc rc = getErrors
|
||||
(mockRcFile rc $ mockedSystemInterface [])
|
||||
|
||||
checkWithIncludesAndSourcePath includes mapper = getErrors
|
||||
(mockedSystemInterface includes) {
|
||||
siFindSource = mapper
|
||||
}
|
||||
|
||||
checkWithRcIncludesAndSourcePath rc includes mapper = getErrors
|
||||
(mockRcFile rc $ mockedSystemInterface includes) {
|
||||
siFindSource = mapper
|
||||
}
|
||||
|
||||
prop_findsParseIssue = check "echo \"$12\"" == [1037]
|
||||
|
||||
prop_commentDisablesParseIssue1 =
|
||||
null $ check "#shellcheck disable=SC1037\necho \"$12\""
|
||||
prop_commentDisablesParseIssue2 =
|
||||
null $ check "#shellcheck disable=SC1037\n#lol\necho \"$12\""
|
||||
|
||||
prop_findsAnalysisIssue =
|
||||
check "echo $1" == [2086]
|
||||
prop_commentDisablesAnalysisIssue1 =
|
||||
null $ check "#shellcheck disable=SC2086\necho $1"
|
||||
prop_commentDisablesAnalysisIssue2 =
|
||||
null $ check "#shellcheck disable=SC2086\n#lol\necho $1"
|
||||
|
||||
prop_optionDisablesIssue1 =
|
||||
null $ getErrors
|
||||
(mockedSystemInterface [])
|
||||
emptyCheckSpec {
|
||||
csScript = "echo $1",
|
||||
csExcludedWarnings = [2148, 2086]
|
||||
}
|
||||
|
||||
prop_optionDisablesIssue2 =
|
||||
null $ getErrors
|
||||
(mockedSystemInterface [])
|
||||
emptyCheckSpec {
|
||||
csScript = "echo \"$10\"",
|
||||
csExcludedWarnings = [2148, 1037]
|
||||
}
|
||||
|
||||
prop_wontParseBadShell =
|
||||
[1071] == check "#!/usr/bin/python\ntrue $1\n"
|
||||
|
||||
prop_optionDisablesBadShebang =
|
||||
null $ getErrors
|
||||
(mockedSystemInterface [])
|
||||
emptyCheckSpec {
|
||||
csScript = "#!/usr/bin/python\ntrue\n",
|
||||
csShellTypeOverride = Just Sh
|
||||
}
|
||||
|
||||
prop_annotationDisablesBadShebang =
|
||||
null $ check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
|
||||
|
||||
|
||||
prop_canParseDevNull =
|
||||
null $ check "source /dev/null"
|
||||
|
||||
prop_failsWhenNotSourcing =
|
||||
[1091, 2154] == check "source lol; echo \"$bar\""
|
||||
|
||||
prop_worksWhenSourcing =
|
||||
null $ checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\""
|
||||
|
||||
prop_worksWhenSourcingWithDashDash =
|
||||
null $ checkWithIncludes [("lib", "bar=1")] "source -- lib; echo \"$bar\""
|
||||
|
||||
prop_worksWhenDotting =
|
||||
null $ checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\""
|
||||
|
||||
-- FIXME: This should really be giving [1093], "recursively sourced"
|
||||
prop_noInfiniteSourcing =
|
||||
null $ checkWithIncludes [("lib", "source lib")] "source lib"
|
||||
|
||||
prop_canSourceBadSyntax =
|
||||
[1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
|
||||
|
||||
prop_cantSourceDynamic =
|
||||
[1090] == checkWithIncludes [("lib", "")] ". \"$1\""
|
||||
|
||||
prop_cantSourceDynamic2 =
|
||||
[1090] == checkWithIncludes [("lib", "")] "source ~/foo"
|
||||
|
||||
prop_canStripPrefixAndSource =
|
||||
null $ checkWithIncludes [("./lib", "")] "source \"$MYDIR/lib\""
|
||||
|
||||
prop_canStripPrefixAndSource2 =
|
||||
null $ checkWithIncludes [("./utils.sh", "")] "source \"$(dirname \"${BASH_SOURCE[0]}\")/utils.sh\""
|
||||
|
||||
prop_canSourceDynamicWhenRedirected =
|
||||
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
|
||||
|
||||
prop_recursiveAnalysis =
|
||||
[2086] == checkRecursive [("lib", "echo $1")] "source lib"
|
||||
|
||||
prop_recursiveParsing =
|
||||
[1037] == checkRecursive [("lib", "echo \"$10\"")] "source lib"
|
||||
|
||||
prop_nonRecursiveAnalysis =
|
||||
null $ checkWithIncludes [("lib", "echo $1")] "source lib"
|
||||
|
||||
prop_nonRecursiveParsing =
|
||||
null $ checkWithIncludes [("lib", "echo \"$10\"")] "source lib"
|
||||
|
||||
prop_sourceDirectiveDoesntFollowFile =
|
||||
null $ checkWithIncludes
|
||||
[("foo", "source bar"), ("bar", "baz=3")]
|
||||
"#shellcheck source=foo\n. \"$1\"; echo \"$baz\""
|
||||
|
||||
prop_filewideAnnotationBase = [2086] == check "#!/bin/sh\necho $1"
|
||||
prop_filewideAnnotation1 = null $
|
||||
check "#!/bin/sh\n# shellcheck disable=2086\necho $1"
|
||||
prop_filewideAnnotation2 = null $
|
||||
check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
|
||||
prop_filewideAnnotation3 = null $
|
||||
check "#!/bin/sh\n#unrelated\n# shellcheck disable=2086\ntrue\necho $1"
|
||||
prop_filewideAnnotation4 = null $
|
||||
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
|
||||
prop_filewideAnnotation5 = null $
|
||||
check "#!/bin/sh\n\n\n\n#shellcheck disable=2086\ntrue\necho $1"
|
||||
prop_filewideAnnotation6 = null $
|
||||
check "#shellcheck shell=sh\n#unrelated\n#shellcheck disable=2086\ntrue\necho $1"
|
||||
prop_filewideAnnotation7 = null $
|
||||
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
|
||||
|
||||
prop_filewideAnnotationBase2 = [2086, 2181] == check "true\n[ $? == 0 ] && echo $1"
|
||||
prop_filewideAnnotation8 = null $
|
||||
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
|
||||
|
||||
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
|
||||
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_deducesTypeFromExtension = null result
|
||||
where
|
||||
result = checkWithSpec [] emptyCheckSpec {
|
||||
csFilename = "file.ksh",
|
||||
csScript = "(( 3.14 ))"
|
||||
}
|
||||
|
||||
prop_deducesTypeFromExtension2 = result == [2079]
|
||||
where
|
||||
result = checkWithSpec [] emptyCheckSpec {
|
||||
csFilename = "file.bash",
|
||||
csScript = "(( 3.14 ))"
|
||||
}
|
||||
|
||||
prop_canDisableShebangWarning = null $ result
|
||||
where
|
||||
result = checkWithSpec [] emptyCheckSpec {
|
||||
csFilename = "file.sh",
|
||||
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]
|
||||
where
|
||||
result = checkWithSpec [] emptyCheckSpec {
|
||||
csFilename = "file.sh",
|
||||
csScript = "echo 'hello world'"
|
||||
}
|
||||
|
||||
prop_sourcedFileUsesOriginalShellExtension = result == [2079]
|
||||
where
|
||||
result = checkWithSpec [("file.ksh", "(( 3.14 ))")] emptyCheckSpec {
|
||||
csFilename = "file.bash",
|
||||
csScript = "source file.ksh",
|
||||
csCheckSourced = True
|
||||
}
|
||||
|
||||
prop_canEnableOptionalsWithSpec = result == [2244]
|
||||
where
|
||||
result = checkWithSpec [] emptyCheckSpec {
|
||||
csFilename = "file.sh",
|
||||
csScript = "#!/bin/sh\n[ \"$1\" ]",
|
||||
csOptionalChecks = ["avoid-nullary-conditions"]
|
||||
}
|
||||
|
||||
prop_optionIncludes1 =
|
||||
-- expect 2086, but not included, so nothing reported
|
||||
null $ checkOptionIncludes (Just [2080]) "#!/bin/sh\n var='a b'\n echo $var"
|
||||
|
||||
prop_optionIncludes2 =
|
||||
-- expect 2086, included, so it is reported
|
||||
[2086] == checkOptionIncludes (Just [2086]) "#!/bin/sh\n var='a b'\n echo $var"
|
||||
|
||||
prop_optionIncludes3 =
|
||||
-- expect 2086, no inclusions provided, so it is reported
|
||||
[2086] == checkOptionIncludes Nothing "#!/bin/sh\n var='a b'\n echo $var"
|
||||
|
||||
prop_optionIncludes4 =
|
||||
-- expect 2086 & 2154, only 2154 included, so only that's reported
|
||||
[2154] == checkOptionIncludes (Just [2154]) "#!/bin/sh\n var='a b'\n echo $var\n echo $bar"
|
||||
|
||||
|
||||
prop_readsRcFile = null result
|
||||
where
|
||||
result = checkWithRc "disable=2086" emptyCheckSpec {
|
||||
csScript = "#!/bin/sh\necho $1",
|
||||
csIgnoreRC = False
|
||||
}
|
||||
|
||||
prop_canUseNoRC = result == [2086]
|
||||
where
|
||||
result = checkWithRc "disable=2086" emptyCheckSpec {
|
||||
csScript = "#!/bin/sh\necho $1",
|
||||
csIgnoreRC = True
|
||||
}
|
||||
|
||||
prop_NoRCWontLookAtFile = result == [2086]
|
||||
where
|
||||
result = checkWithRc (error "Fail") emptyCheckSpec {
|
||||
csScript = "#!/bin/sh\necho $1",
|
||||
csIgnoreRC = True
|
||||
}
|
||||
|
||||
prop_brokenRcGetsWarning = result == [1134, 2086]
|
||||
where
|
||||
result = checkWithRc "rofl" emptyCheckSpec {
|
||||
csScript = "#!/bin/sh\necho $1",
|
||||
csIgnoreRC = False
|
||||
}
|
||||
|
||||
prop_canEnableOptionalsWithRc = result == [2244]
|
||||
where
|
||||
result = checkWithRc "enable=avoid-nullary-conditions" emptyCheckSpec {
|
||||
csScript = "#!/bin/sh\n[ \"$1\" ]"
|
||||
}
|
||||
|
||||
prop_sourcePathRedirectsName = result == [2086]
|
||||
where
|
||||
f "dir/myscript" _ _ "lib" = return "foo/lib"
|
||||
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
|
||||
csScript = "#!/bin/bash\nsource lib",
|
||||
csFilename = "dir/myscript",
|
||||
csCheckSourced = True
|
||||
}
|
||||
|
||||
prop_sourcePathAddsAnnotation = result == [2086]
|
||||
where
|
||||
f "dir/myscript" _ ["mypath"] "lib" = return "foo/lib"
|
||||
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
|
||||
csScript = "#!/bin/bash\n# shellcheck source-path=mypath\nsource lib",
|
||||
csFilename = "dir/myscript",
|
||||
csCheckSourced = True
|
||||
}
|
||||
|
||||
prop_sourcePathRedirectsDirective = result == [2086]
|
||||
where
|
||||
f "dir/myscript" _ _ "lib" = return "foo/lib"
|
||||
f _ _ _ _ = return "/dev/null"
|
||||
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
|
||||
csScript = "#!/bin/bash\n# shellcheck source=lib\nsource kittens",
|
||||
csFilename = "dir/myscript",
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
1390
src/ShellCheck/Checks/Commands.hs
Normal file
1390
src/ShellCheck/Checks/Commands.hs
Normal file
File diff suppressed because it is too large
Load Diff
21
src/ShellCheck/Checks/Custom.hs
Normal file
21
src/ShellCheck/Checks/Custom.hs
Normal file
@@ -0,0 +1,21 @@
|
||||
{-
|
||||
This empty file is provided for ease of patching in site specific checks.
|
||||
However, there are no guarantees regarding compatibility between versions.
|
||||
-}
|
||||
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
module ShellCheck.Checks.Custom (checker, ShellCheck.Checks.Custom.runTests) where
|
||||
|
||||
import ShellCheck.AnalyzerLib
|
||||
import Test.QuickCheck
|
||||
|
||||
checker :: Parameters -> Checker
|
||||
checker params = Checker {
|
||||
perScript = const $ return (),
|
||||
perToken = const $ return ()
|
||||
}
|
||||
|
||||
prop_CustomTestsWork = True
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
558
src/ShellCheck/Checks/ShellSupport.hs
Normal file
558
src/ShellCheck/Checks/ShellSupport.hs
Normal file
@@ -0,0 +1,558 @@
|
||||
{-
|
||||
Copyright 2012-2020 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 #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.AnalyzerLib
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.RWS
|
||||
import Data.Char
|
||||
import Data.Functor.Identity
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import qualified Data.Map as Map
|
||||
import qualified Data.Set as Set
|
||||
import Test.QuickCheck.All (forAllProperties)
|
||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||
|
||||
data ForShell = ForShell [Shell] (Token -> Analysis)
|
||||
|
||||
getChecker params list = Checker {
|
||||
perScript = nullCheck,
|
||||
perToken = foldl composeAnalyzers nullCheck $ mapMaybe include list
|
||||
}
|
||||
where
|
||||
shell = shellType params
|
||||
include (ForShell list a) = do
|
||||
guard $ shell `elem` list
|
||||
return a
|
||||
|
||||
checker params = getChecker params checks
|
||||
|
||||
checks = [
|
||||
checkForDecimals
|
||||
,checkBashisms
|
||||
,checkEchoSed
|
||||
,checkBraceExpansionVars
|
||||
,checkMultiDimensionalArrays
|
||||
,checkPS1Assignments
|
||||
]
|
||||
|
||||
testChecker (ForShell _ t) =
|
||||
Checker {
|
||||
perScript = nullCheck,
|
||||
perToken = t
|
||||
}
|
||||
verify c s = producesComments (testChecker c) s == Just True
|
||||
verifyNot c s = producesComments (testChecker c) s == Just False
|
||||
|
||||
prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
|
||||
prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
|
||||
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
|
||||
checkForDecimals = ForShell [Sh, Dash, Bash] f
|
||||
where
|
||||
f t@(TA_Expansion id _) = sequence_ $ do
|
||||
str <- getLiteralString t
|
||||
first <- str !!! 0
|
||||
guard $ isDigit first && '.' `elem` str
|
||||
return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
|
||||
f _ = return ()
|
||||
|
||||
|
||||
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
|
||||
prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"
|
||||
prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
|
||||
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
|
||||
prop_checkBashisms5 = verify checkBashisms "source file"
|
||||
prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
|
||||
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
|
||||
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
|
||||
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
|
||||
prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}"
|
||||
prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}"
|
||||
prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}"
|
||||
prop_checkBashisms13= verify checkBashisms "exec -c env"
|
||||
prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \""
|
||||
prop_checkBashisms15= verify checkBashisms "let n++"
|
||||
prop_checkBashisms16= verify checkBashisms "echo $RANDOM"
|
||||
prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))"
|
||||
prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"
|
||||
prop_checkBashisms19= verify checkBashisms "foo > file*.txt"
|
||||
prop_checkBashisms20= verify checkBashisms "read -ra foo"
|
||||
prop_checkBashisms21= verify checkBashisms "[ -a foo ]"
|
||||
prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]"
|
||||
prop_checkBashisms23= verify checkBashisms "trap mything ERR INT"
|
||||
prop_checkBashisms24= verifyNot checkBashisms "trap mything INT TERM"
|
||||
prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123"
|
||||
prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM"
|
||||
prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*"
|
||||
prop_checkBashisms28= verify checkBashisms "exec {n}>&2"
|
||||
prop_checkBashisms29= verify checkBashisms "echo ${!var}"
|
||||
prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\""
|
||||
prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\""
|
||||
prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
|
||||
prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo"
|
||||
prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo"
|
||||
prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo"
|
||||
prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
|
||||
prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
|
||||
prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM"
|
||||
prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }"
|
||||
prop_checkBashisms40= verify checkBashisms "echo $(<file)"
|
||||
prop_checkBashisms41= verify checkBashisms "echo `<file`"
|
||||
prop_checkBashisms42= verify checkBashisms "trap foo int"
|
||||
prop_checkBashisms43= verify checkBashisms "trap foo sigint"
|
||||
prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
|
||||
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
|
||||
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
|
||||
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
|
||||
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
|
||||
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
|
||||
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
|
||||
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
||||
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
||||
prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
||||
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"
|
||||
prop_checkBashisms55= verify checkBashisms "#!/bin/sh\necho ${@%foo}"
|
||||
prop_checkBashisms56= verifyNot checkBashisms "#!/bin/sh\necho ${##}"
|
||||
prop_checkBashisms57= verifyNot checkBashisms "#!/bin/dash\nulimit -c 0"
|
||||
prop_checkBashisms58= verify checkBashisms "#!/bin/sh\nulimit -c 0"
|
||||
prop_checkBashisms59 = verify checkBashisms "#!/bin/sh\njobs -s"
|
||||
prop_checkBashisms60 = verifyNot checkBashisms "#!/bin/sh\njobs -p"
|
||||
prop_checkBashisms61 = verifyNot checkBashisms "#!/bin/sh\njobs -lp"
|
||||
prop_checkBashisms62 = verify checkBashisms "#!/bin/sh\nexport -f foo"
|
||||
prop_checkBashisms63 = verifyNot checkBashisms "#!/bin/sh\nexport -p"
|
||||
prop_checkBashisms64 = verify checkBashisms "#!/bin/sh\nreadonly -a"
|
||||
prop_checkBashisms65 = verifyNot checkBashisms "#!/bin/sh\nreadonly -p"
|
||||
prop_checkBashisms66 = verifyNot checkBashisms "#!/bin/sh\ncd -P ."
|
||||
prop_checkBashisms67 = verify checkBashisms "#!/bin/sh\ncd -P -e ."
|
||||
prop_checkBashisms68 = verify checkBashisms "#!/bin/sh\numask -p"
|
||||
prop_checkBashisms69 = verifyNot checkBashisms "#!/bin/sh\numask -S"
|
||||
prop_checkBashisms70 = verify checkBashisms "#!/bin/sh\ntrap -l"
|
||||
prop_checkBashisms71 = verify checkBashisms "#!/bin/sh\ntype -a ls"
|
||||
prop_checkBashisms72 = verifyNot checkBashisms "#!/bin/sh\ntype ls"
|
||||
prop_checkBashisms73 = verify checkBashisms "#!/bin/sh\nunset -n namevar"
|
||||
prop_checkBashisms74 = verifyNot checkBashisms "#!/bin/sh\nunset -f namevar"
|
||||
prop_checkBashisms75 = verifyNot checkBashisms "#!/bin/sh\necho \"-n foo\""
|
||||
prop_checkBashisms76 = verifyNot checkBashisms "#!/bin/sh\necho \"-ne foo\""
|
||||
prop_checkBashisms77 = verifyNot checkBashisms "#!/bin/sh\necho -Q foo"
|
||||
prop_checkBashisms78 = verify checkBashisms "#!/bin/sh\necho -ne foo"
|
||||
prop_checkBashisms79 = verify checkBashisms "#!/bin/sh\nhash -l"
|
||||
prop_checkBashisms80 = verifyNot checkBashisms "#!/bin/sh\nhash -r"
|
||||
prop_checkBashisms81 = verifyNot checkBashisms "#!/bin/dash\nhash -v"
|
||||
prop_checkBashisms82 = verifyNot checkBashisms "#!/bin/sh\nset -v +o allexport -o errexit -C"
|
||||
prop_checkBashisms83 = verifyNot checkBashisms "#!/bin/sh\nset --"
|
||||
prop_checkBashisms84 = verify checkBashisms "#!/bin/sh\nset -o pipefail"
|
||||
prop_checkBashisms85 = verify checkBashisms "#!/bin/sh\nset -B"
|
||||
prop_checkBashisms86 = verifyNot checkBashisms "#!/bin/dash\nset -o emacs"
|
||||
prop_checkBashisms87 = verify checkBashisms "#!/bin/sh\nset -o emacs"
|
||||
prop_checkBashisms88 = verifyNot checkBashisms "#!/bin/sh\nset -- wget -o foo 'https://some.url'"
|
||||
prop_checkBashisms89 = verifyNot checkBashisms "#!/bin/sh\nopts=$-\nset -\"$opts\""
|
||||
prop_checkBashisms90 = verifyNot checkBashisms "#!/bin/sh\nset -o \"$opt\""
|
||||
prop_checkBashisms91 = verify checkBashisms "#!/bin/sh\nwait -n"
|
||||
prop_checkBashisms92 = verify checkBashisms "#!/bin/sh\necho $((16#FF))"
|
||||
prop_checkBashisms93 = verify checkBashisms "#!/bin/sh\necho $(( 10#$(date +%m) ))"
|
||||
prop_checkBashisms94 = verify checkBashisms "#!/bin/sh\n[ -v var ]"
|
||||
prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_"
|
||||
prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
|
||||
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"
|
||||
checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
params <- ask
|
||||
kludge params t
|
||||
where
|
||||
-- This code was copy-pasted from Analytics where params was a variable
|
||||
kludge params = bashism
|
||||
where
|
||||
isDash = shellType params == Dash
|
||||
warnMsg id code s =
|
||||
if isDash
|
||||
then err id code $ "In dash, " ++ s ++ " not supported."
|
||||
else warn id code $ "In POSIX sh, " ++ s ++ " undefined."
|
||||
|
||||
bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is"
|
||||
bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is"
|
||||
bashism (T_DollarSingleQuoted id _) = 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 _) = warnMsg id 3010 "[[ ]] is"
|
||||
bashism (T_HereString id _) = warnMsg id 3011 "here-strings are"
|
||||
bashism (TC_Binary id SingleBracket op _ _)
|
||||
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
|
||||
unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is"
|
||||
bashism (TC_Binary id SingleBracket op _ _)
|
||||
| op `elem` [ "-ot", "-nt", "-ef" ] =
|
||||
unless isDash $ warnMsg id 3013 $ op ++ " is"
|
||||
bashism (TC_Binary id SingleBracket "==" _ _) =
|
||||
warnMsg id 3014 "== in place of = is"
|
||||
bashism (TC_Binary id SingleBracket "=~" _ _) =
|
||||
warnMsg id 3015 "=~ regex matching is"
|
||||
bashism (TC_Unary id SingleBracket "-v" _) =
|
||||
warnMsg id 3016 "unary -v (in place of [ -n \"${var+x}\" ]) is"
|
||||
bashism (TC_Unary id _ "-a" _) =
|
||||
warnMsg id 3017 "unary -a in place of -e is"
|
||||
bashism (TA_Unary id op _)
|
||||
| op `elem` [ "|++", "|--", "++|", "--|"] =
|
||||
warnMsg id 3018 $ filter (/= '|') op ++ " is"
|
||||
bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
|
||||
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id 3020 "&> is"
|
||||
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id 3021 ">& is"
|
||||
bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are"
|
||||
bashism (T_FdRedirect id num _)
|
||||
| all isDigit num && length num > 1 = warnMsg id 3023 "FDs outside 0-9 are"
|
||||
bashism (T_Assignment id Append _ _ _) =
|
||||
warnMsg id 3024 "+= is"
|
||||
bashism (T_IoFile id _ word) | isNetworked =
|
||||
warnMsg id 3025 "/dev/{tcp,udp} is"
|
||||
where
|
||||
file = onlyLiteralString word
|
||||
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
|
||||
bashism (T_Glob id str) | "[^" `isInfixOf` str =
|
||||
warnMsg id 3026 "^ in place of ! in glob bracket expressions is"
|
||||
|
||||
bashism t@(TA_Variable id str _) | isBashVariable str =
|
||||
warnMsg id 3028 $ str ++ " is"
|
||||
|
||||
bashism t@(T_DollarBraced id _ token) = do
|
||||
mapM_ check expansion
|
||||
when (isBashVariable var) $
|
||||
warnMsg id 3028 $ var ++ " is"
|
||||
where
|
||||
str = concat $ oversimplify token
|
||||
var = getBracedReference str
|
||||
check (regex, code, feature) =
|
||||
when (isJust $ matchRegex regex str) $ warnMsg id code feature
|
||||
|
||||
bashism t@(T_Pipe id "|&") =
|
||||
warnMsg id 3029 "|& in place of 2>&1 | is"
|
||||
bashism (T_Array id _) =
|
||||
warnMsg id 3030 "arrays are"
|
||||
bashism (T_IoFile id _ t) | isGlob t =
|
||||
warnMsg id 3031 "redirecting to/from globs is"
|
||||
bashism (T_CoProc id _ _) =
|
||||
warnMsg id 3032 "coproc is"
|
||||
|
||||
bashism (T_Function id _ _ str _) | not (isVariableName str) =
|
||||
warnMsg id 3033 "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
|
||||
|
||||
bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
|
||||
warnMsg id 3034 "$(<file) to read files is"
|
||||
bashism (T_Backticked id [x]) | isOnlyRedirection x =
|
||||
warnMsg id 3035 "`<file` to read files is"
|
||||
|
||||
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
||||
| t `isCommand` "echo" && argString `matches` flagRegex =
|
||||
if isDash
|
||||
then
|
||||
when (argString /= "-n") $
|
||||
warnMsg (getId arg) 3036 "echo flags besides -n"
|
||||
else
|
||||
warnMsg (getId arg) 3037 "echo flags are"
|
||||
where
|
||||
argString = concat $ oversimplify arg
|
||||
flagRegex = mkRegex "^-[eEsn]+$"
|
||||
|
||||
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
||||
| getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
|
||||
warnMsg (getId arg) 3038 "exec flags are"
|
||||
bashism t@(T_SimpleCommand id _ _)
|
||||
| t `isCommand` "let" = warnMsg id 3039 "'let' is"
|
||||
bashism t@(T_SimpleCommand _ _ (cmd:args))
|
||||
| t `isCommand` "set" = unless isDash $
|
||||
checkOptions $ getLiteralArgs args
|
||||
where
|
||||
-- Get the literal options from a list of arguments,
|
||||
-- up until the first non-literal one
|
||||
getLiteralArgs :: [Token] -> [(Id, String)]
|
||||
getLiteralArgs = foldr go []
|
||||
where
|
||||
go first rest = case getLiteralString first of
|
||||
Just str -> (getId first, str) : rest
|
||||
Nothing -> []
|
||||
|
||||
-- Check a flag-option pair (such as -o errexit)
|
||||
checkOptions (flag@(fid,flag') : opt@(oid,opt') : rest)
|
||||
| flag' `matches` oFlagRegex = do
|
||||
when (opt' `notElem` longOptions) $
|
||||
warnMsg oid 3040 $ "set option " <> opt' <> " is"
|
||||
checkFlags (flag:rest)
|
||||
| otherwise = checkFlags (flag:opt:rest)
|
||||
checkOptions (flag:rest) = checkFlags (flag:rest)
|
||||
checkOptions _ = return ()
|
||||
|
||||
-- Check that each option in a sequence of flags
|
||||
-- (such as -aveo) is valid
|
||||
checkFlags (flag@(fid, flag'):rest)
|
||||
| startsOption flag' = do
|
||||
unless (flag' `matches` validFlagsRegex) $
|
||||
forM_ (tail flag') $ \letter ->
|
||||
when (letter `notElem` optionsSet) $
|
||||
warnMsg fid 3041 $ "set flag " <> ('-':letter:" is")
|
||||
checkOptions rest
|
||||
| beginsWithDoubleDash flag' = do
|
||||
warnMsg fid 3042 $ "set flag " <> flag' <> " is"
|
||||
checkOptions rest
|
||||
-- Either a word that doesn't start with a dash, or simply '--',
|
||||
-- so stop checking.
|
||||
| otherwise = return ()
|
||||
checkFlags [] = return ()
|
||||
|
||||
options = "abCefhmnuvxo"
|
||||
optionsSet = Set.fromList options
|
||||
startsOption = (`matches` mkRegex "^(\\+|-[^-])")
|
||||
oFlagRegex = mkRegex $ "^[-+][" <> options <> "]*o$"
|
||||
validFlagsRegex = mkRegex $ "^[-+]([" <> options <> "]+o?|o)$"
|
||||
beginsWithDoubleDash = (`matches` mkRegex "^--.+$")
|
||||
longOptions = Set.fromList
|
||||
[ "allexport", "errexit", "ignoreeof", "monitor", "noclobber"
|
||||
, "noexec", "noglob", "nolog", "notify" , "nounset", "verbose"
|
||||
, "vi", "xtrace" ]
|
||||
|
||||
bashism t@(T_SimpleCommand id _ (cmd:rest)) =
|
||||
let name = fromMaybe "" $ getCommandName t
|
||||
flags = getLeadingFlags t
|
||||
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) $
|
||||
warnMsg id 3044 $ "'" ++ name ++ "' is"
|
||||
sequence_ $ do
|
||||
allowed' <- Map.lookup name allowedFlags
|
||||
allowed <- allowed'
|
||||
(word, flag) <- find
|
||||
(\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
|
||||
return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is"
|
||||
|
||||
when (name == "source") $ warnMsg id 3046 "'source' in place of '.' is"
|
||||
when (name == "trap") $
|
||||
let
|
||||
check token = sequence_ $ do
|
||||
str <- getLiteralString token
|
||||
let upper = map toUpper str
|
||||
return $ do
|
||||
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
|
||||
warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is"
|
||||
when ("SIG" `isPrefixOf` upper) $
|
||||
warnMsg (getId token) 3048
|
||||
"prefixing signal names with 'SIG' is"
|
||||
when (not isDash && upper /= str) $
|
||||
warnMsg (getId token) 3049
|
||||
"using lower/mixed case for signal names is"
|
||||
in
|
||||
mapM_ check (drop 1 rest)
|
||||
|
||||
when (name == "printf") $ sequence_ $ do
|
||||
format <- rest !!! 0 -- flags are covered by allowedFlags
|
||||
let literal = onlyLiteralString format
|
||||
guard $ "%q" `isInfixOf` literal
|
||||
return $ warnMsg (getId format) 3050 "printf %q is"
|
||||
where
|
||||
unsupportedCommands = [
|
||||
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
||||
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
|
||||
"typeset"
|
||||
]
|
||||
allowedFlags = Map.fromList [
|
||||
("cd", Just ["L", "P"]),
|
||||
("exec", Just []),
|
||||
("export", Just ["p"]),
|
||||
("hash", Just $ if isDash then ["r", "v"] else ["r"]),
|
||||
("jobs", Just ["l", "p"]),
|
||||
("printf", Just []),
|
||||
("read", Just $ if isDash then ["r", "p"] else ["r"]),
|
||||
("readonly", Just ["p"]),
|
||||
("trap", Just []),
|
||||
("type", Just []),
|
||||
("ulimit", if isDash then Nothing else Just ["f"]),
|
||||
("umask", Just ["S"]),
|
||||
("unset", Just ["f", "v"]),
|
||||
("wait", Just [])
|
||||
]
|
||||
bashism t@(T_SourceCommand id src _)
|
||||
| getCommandName src == Just "source" = warnMsg id 3051 "'source' in place of '.' is"
|
||||
bashism (TA_Expansion _ (T_Literal id str : _))
|
||||
| str `matches` radix = warnMsg id 3052 "arithmetic base conversion is"
|
||||
where
|
||||
radix = mkRegex "^[0-9]+#"
|
||||
bashism _ = return ()
|
||||
|
||||
varChars="_0-9a-zA-Z"
|
||||
expansion = let re = mkRegex in [
|
||||
(re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"),
|
||||
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"),
|
||||
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"),
|
||||
(re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"),
|
||||
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", 3057, "string indexing is"),
|
||||
(re $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"),
|
||||
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is"),
|
||||
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is")
|
||||
]
|
||||
bashVars = [
|
||||
-- This list deliberately excludes $BASH_VERSION as it's often used
|
||||
-- for shell identification.
|
||||
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
||||
"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" ]
|
||||
dashVars = [ "_" ]
|
||||
isBashVariable var =
|
||||
(var `elem` bashDynamicVars
|
||||
|| var `elem` bashVars && not (isAssigned var))
|
||||
&& not (isDash && var `elem` dashVars)
|
||||
isAssigned var = any f (variableFlow params)
|
||||
where
|
||||
f x = case x of
|
||||
Assignment (_, _, name, _) -> name == var
|
||||
_ -> False
|
||||
|
||||
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
|
||||
prop_checkEchoSed1b= verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")"
|
||||
prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
|
||||
prop_checkEchoSed2b= verify checkEchoSed "rm $(sed -e 's,foo,bar,' <<< $cow)"
|
||||
checkEchoSed = ForShell [Bash, Ksh] f
|
||||
where
|
||||
f (T_Redirecting id lefts r) =
|
||||
when (any redirectHereString lefts) $
|
||||
checkSed id rcmd
|
||||
where
|
||||
redirectHereString :: Token -> Bool
|
||||
redirectHereString t = case t of
|
||||
(T_FdRedirect _ _ T_HereString{}) -> True
|
||||
_ -> False
|
||||
rcmd = oversimplify r
|
||||
|
||||
f (T_Pipeline id _ [a, b]) =
|
||||
when (acmd == ["echo", "${VAR}"]) $
|
||||
checkSed id bcmd
|
||||
where
|
||||
acmd = oversimplify a
|
||||
bcmd = oversimplify b
|
||||
|
||||
f _ = return ()
|
||||
|
||||
checkSed id ["sed", v] = checkIn id v
|
||||
checkSed id ["sed", "-e", v] = checkIn id v
|
||||
checkSed _ _ = return ()
|
||||
|
||||
-- This should have used backreferences, but TDFA doesn't support them
|
||||
sedRe = mkRegex "^s(.)([^\n]*)g?$"
|
||||
isSimpleSed s = isJust $ do
|
||||
[h:_,rest] <- matchRegex sedRe s
|
||||
let delimiters = filter (== h) rest
|
||||
guard $ length delimiters == 2
|
||||
checkIn id s =
|
||||
when (isSimpleSed s) $
|
||||
style id 2001 "See if you can use ${variable//search/replace} instead."
|
||||
|
||||
|
||||
prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}"
|
||||
prop_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}"
|
||||
prop_checkBraceExpansionVars3 = verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg"
|
||||
prop_checkBraceExpansionVars4 = verify checkBraceExpansionVars "echo {$i..100}"
|
||||
checkBraceExpansionVars = ForShell [Bash] f
|
||||
where
|
||||
f t@(T_BraceExpansion id list) = mapM_ check list
|
||||
where
|
||||
check element =
|
||||
when (any (`isInfixOf` toString element) ["$..", "..$"]) $ do
|
||||
c <- isEvaled element
|
||||
if c
|
||||
then style id 2175 "Quote this invalid brace expansion since it should be passed literally to eval."
|
||||
else warn id 2051 "Bash doesn't support variables in brace range expansions."
|
||||
f _ = return ()
|
||||
|
||||
literalExt t =
|
||||
case t of
|
||||
T_DollarBraced {} -> return "$"
|
||||
T_DollarExpansion {} -> return "$"
|
||||
T_DollarArithmetic {} -> return "$"
|
||||
_ -> return "-"
|
||||
toString t = runIdentity $ getLiteralStringExt literalExt t
|
||||
isEvaled t = do
|
||||
cmd <- getClosestCommandM t
|
||||
return $ maybe False (`isUnqualifiedCommand` "eval") cmd
|
||||
|
||||
|
||||
prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3"
|
||||
prop_checkMultiDimensionalArrays2 = verifyNot checkMultiDimensionalArrays "foo[a]=3"
|
||||
prop_checkMultiDimensionalArrays3 = verify checkMultiDimensionalArrays "foo=( [a][b]=c )"
|
||||
prop_checkMultiDimensionalArrays4 = verifyNot checkMultiDimensionalArrays "foo=( [a]=c )"
|
||||
prop_checkMultiDimensionalArrays5 = verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}"
|
||||
prop_checkMultiDimensionalArrays6 = verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}"
|
||||
checkMultiDimensionalArrays = ForShell [Bash] f
|
||||
where
|
||||
f token =
|
||||
case token of
|
||||
T_Assignment _ _ name (first:second:_) _ -> about second
|
||||
T_IndexedElement _ (first:second:_) _ -> about second
|
||||
T_DollarBraced _ _ l ->
|
||||
when (isMultiDim l) $ about token
|
||||
_ -> return ()
|
||||
about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays."
|
||||
|
||||
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
|
||||
isMultiDim l = getBracedModifier (concat $ oversimplify l) `matches` re
|
||||
|
||||
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
|
||||
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
|
||||
prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
|
||||
prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '"
|
||||
prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '"
|
||||
prop_checkPS14a= verify checkPS1Assignments "export PS1=$'\\e[3m; '"
|
||||
prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"
|
||||
prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
|
||||
prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'"
|
||||
prop_checkPS18 = verifyNot checkPS1Assignments "PS1='\\[\\e\\]'"
|
||||
checkPS1Assignments = ForShell [Bash] f
|
||||
where
|
||||
f token = case token of
|
||||
(T_Assignment _ _ "PS1" _ word) -> warnFor word
|
||||
_ -> return ()
|
||||
|
||||
warnFor word =
|
||||
let contents = concat $ oversimplify word in
|
||||
when (containsUnescaped contents) $
|
||||
info (getId word) 2025 "Make sure all escape sequences are enclosed in \\[..\\] to prevent line wrapping issues"
|
||||
containsUnescaped s =
|
||||
let unenclosed = subRegex enclosedRegex s "" in
|
||||
isJust $ matchRegex escapeRegex unenclosed
|
||||
enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager
|
||||
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
|
||||
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
142
src/ShellCheck/Data.hs
Normal file
142
src/ShellCheck/Data.hs
Normal file
@@ -0,0 +1,142 @@
|
||||
module ShellCheck.Data where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import Data.Version (showVersion)
|
||||
import Paths_ShellCheck (version)
|
||||
|
||||
shellcheckVersion = showVersion version -- VERSIONSTRING
|
||||
|
||||
internalVariables = [
|
||||
-- Generic
|
||||
"", "_", "rest", "REST",
|
||||
|
||||
-- Bash
|
||||
"BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
|
||||
"BASH_ARGV", "BASH_CMDS", "BASH_COMMAND", "BASH_EXECUTION_STRING",
|
||||
"BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL",
|
||||
"BASH_VERSINFO", "BASH_VERSION", "COMP_CWORD", "COMP_KEY",
|
||||
"COMP_LINE", "COMP_POINT", "COMP_TYPE", "COMP_WORDBREAKS",
|
||||
"COMP_WORDS", "COPROC", "DIRSTACK", "EUID", "FUNCNAME", "GROUPS",
|
||||
"HISTCMD", "HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE",
|
||||
"OLDPWD", "OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD",
|
||||
"RANDOM", "READLINE_LINE", "READLINE_POINT", "REPLY", "SECONDS",
|
||||
"SHELLOPTS", "SHLVL", "UID", "BASH_ENV", "BASH_XTRACEFD", "CDPATH",
|
||||
"COLUMNS", "COMPREPLY", "EMACS", "ENV", "FCEDIT", "FIGNORE",
|
||||
"FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",
|
||||
"HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",
|
||||
"IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
|
||||
"LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LINES", "MAIL",
|
||||
"MAILCHECK", "MAILPATH", "OPTERR", "PATH", "POSIXLY_CORRECT",
|
||||
"PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL",
|
||||
"TIMEFORMAT", "TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC",
|
||||
|
||||
-- Other
|
||||
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
|
||||
"HOSTNAME", "KRB5CCNAME", "XAUTHORITY"
|
||||
|
||||
-- Ksh
|
||||
, ".sh.version"
|
||||
|
||||
-- shflags
|
||||
, "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP",
|
||||
"FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION",
|
||||
"flags_error", "flags_return"
|
||||
]
|
||||
|
||||
specialVariablesWithoutSpaces = [
|
||||
"$", "-", "?", "!", "#"
|
||||
]
|
||||
variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
|
||||
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
|
||||
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
|
||||
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
|
||||
|
||||
-- shflags
|
||||
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
|
||||
]
|
||||
|
||||
specialVariables = specialVariablesWithoutSpaces ++ ["@", "*"]
|
||||
|
||||
unbracedVariables = specialVariables ++ [
|
||||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
|
||||
]
|
||||
|
||||
arrayVariables = [
|
||||
"BASH_ALIASES", "BASH_ARGC", "BASH_ARGV", "BASH_CMDS", "BASH_LINENO",
|
||||
"BASH_REMATCH", "BASH_SOURCE", "BASH_VERSINFO", "COMP_WORDS", "COPROC",
|
||||
"DIRSTACK", "FUNCNAME", "GROUPS", "MAPFILE", "PIPESTATUS", "COMPREPLY"
|
||||
]
|
||||
|
||||
commonCommands = [
|
||||
"admin", "alias", "ar", "asa", "at", "awk", "basename", "batch",
|
||||
"bc", "bg", "break", "c99", "cal", "cat", "cd", "cflow", "chgrp",
|
||||
"chmod", "chown", "cksum", "cmp", "colon", "comm", "command",
|
||||
"compress", "continue", "cp", "crontab", "csplit", "ctags", "cut",
|
||||
"cxref", "date", "dd", "delta", "df", "diff", "dirname", "dot",
|
||||
"du", "echo", "ed", "env", "eval", "ex", "exec", "exit", "expand",
|
||||
"export", "expr", "fc", "fg", "file", "find", "fold", "fort77",
|
||||
"fuser", "gencat", "get", "getconf", "getopts", "grep", "hash",
|
||||
"head", "iconv", "ipcrm", "ipcs", "jobs", "join", "kill", "lex",
|
||||
"link", "ln", "locale", "localedef", "logger", "logname", "lp",
|
||||
"ls", "m4", "mailx", "make", "man", "mesg", "mkdir", "mkfifo",
|
||||
"more", "mv", "newgrp", "nice", "nl", "nm", "nohup", "od", "paste",
|
||||
"patch", "pathchk", "pax", "pr", "printf", "prs", "ps", "pwd",
|
||||
"qalter", "qdel", "qhold", "qmove", "qmsg", "qrerun", "qrls",
|
||||
"qselect", "qsig", "qstat", "qsub", "read", "readonly", "renice",
|
||||
"return", "rm", "rmdel", "rmdir", "sact", "sccs", "sed", "set",
|
||||
"sh", "shift", "sleep", "sort", "split", "strings", "strip", "stty",
|
||||
"tabs", "tail", "talk", "tee", "test", "time", "times", "touch",
|
||||
"tput", "tr", "trap", "tsort", "tty", "type", "ulimit", "umask",
|
||||
"unalias", "uname", "uncompress", "unexpand", "unget", "uniq",
|
||||
"unlink", "unset", "uucp", "uudecode", "uuencode", "uustat", "uux",
|
||||
"val", "vi", "wait", "wc", "what", "who", "write", "xargs", "yacc",
|
||||
"zcat"
|
||||
]
|
||||
|
||||
nonReadingCommands = [
|
||||
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
|
||||
"cp", "du", "echo", "export", "false", "fg", "fuser", "getconf",
|
||||
"getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
|
||||
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
|
||||
"set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname"
|
||||
]
|
||||
|
||||
sampleWords = [
|
||||
"alpha", "bravo", "charlie", "delta", "echo", "foxtrot",
|
||||
"golf", "hotel", "india", "juliett", "kilo", "lima", "mike",
|
||||
"november", "oscar", "papa", "quebec", "romeo", "sierra",
|
||||
"tango", "uniform", "victor", "whiskey", "xray", "yankee",
|
||||
"zulu"
|
||||
]
|
||||
|
||||
binaryTestOps = [
|
||||
"-nt", "-ot", "-ef", "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le",
|
||||
"-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="
|
||||
]
|
||||
|
||||
arithmeticBinaryTestOps = [
|
||||
"-eq", "-ne", "-lt", "-le", "-gt", "-ge"
|
||||
]
|
||||
|
||||
unaryTestOps = [
|
||||
"!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p",
|
||||
"-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n",
|
||||
"-o", "-v", "-R"
|
||||
]
|
||||
|
||||
shellForExecutable :: String -> Maybe Shell
|
||||
shellForExecutable name =
|
||||
case name of
|
||||
"sh" -> return Sh
|
||||
"bash" -> return Bash
|
||||
"bats" -> return Bash
|
||||
"dash" -> return Dash
|
||||
"ash" -> return Dash -- There's also a warning for this.
|
||||
"ksh" -> return Ksh
|
||||
"ksh88" -> return Ksh
|
||||
"ksh93" -> return Ksh
|
||||
_ -> Nothing
|
||||
|
||||
flagsForRead = "sreu:n:N:i:p:a:t:"
|
||||
|
||||
declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"]
|
409
src/ShellCheck/Fixer.hs
Normal file
409
src/ShellCheck/Fixer.hs
Normal file
@@ -0,0 +1,409 @@
|
||||
{-
|
||||
Copyright 2018-2019 Vidar Holen, Ng Zhi An
|
||||
|
||||
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 #-}
|
||||
module ShellCheck.Fixer (applyFix, removeTabStops, mapPositions, Ranged(..), runTests) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import Control.Monad.State
|
||||
import Data.Array
|
||||
import Data.List
|
||||
import Data.Semigroup
|
||||
import GHC.Exts (sortWith)
|
||||
import Test.QuickCheck
|
||||
|
||||
-- The Ranged class is used for types that has a start and end position.
|
||||
class Ranged a where
|
||||
start :: a -> Position
|
||||
end :: a -> Position
|
||||
overlap :: a -> a -> Bool
|
||||
overlap x y =
|
||||
(yStart >= xStart && yStart < xEnd) || (yStart < xStart && yEnd > xStart)
|
||||
where
|
||||
yStart = start y
|
||||
yEnd = end y
|
||||
xStart = start x
|
||||
xEnd = end x
|
||||
-- Set a new start and end position on a Ranged
|
||||
setRange :: (Position, Position) -> a -> a
|
||||
|
||||
-- Tests auto-verify that overlap commutes
|
||||
assertOverlap x y = overlap x y && overlap y x
|
||||
assertNoOverlap x y = not (overlap x y) && not (overlap y x)
|
||||
|
||||
prop_overlap_contiguous = assertNoOverlap
|
||||
(tFromStart 10 12 "foo" 1)
|
||||
(tFromStart 12 14 "bar" 2)
|
||||
|
||||
prop_overlap_adjacent_zerowidth = assertNoOverlap
|
||||
(tFromStart 3 3 "foo" 1)
|
||||
(tFromStart 3 3 "bar" 2)
|
||||
|
||||
prop_overlap_enclosed = assertOverlap
|
||||
(tFromStart 3 5 "foo" 1)
|
||||
(tFromStart 1 10 "bar" 2)
|
||||
|
||||
prop_overlap_partial = assertOverlap
|
||||
(tFromStart 1 5 "foo" 1)
|
||||
(tFromStart 3 7 "bar" 2)
|
||||
|
||||
|
||||
instance Ranged PositionedComment where
|
||||
start = pcStartPos
|
||||
end = pcEndPos
|
||||
setRange (s, e) pc = pc {
|
||||
pcStartPos = s,
|
||||
pcEndPos = e
|
||||
}
|
||||
|
||||
instance Ranged Replacement where
|
||||
start = repStartPos
|
||||
end = repEndPos
|
||||
setRange (s, e) r = r {
|
||||
repStartPos = s,
|
||||
repEndPos = e
|
||||
}
|
||||
|
||||
-- The Monoid instance for Fix merges fixes that do not conflict.
|
||||
-- TODO: Make an efficient 'mconcat'
|
||||
instance Monoid Fix where
|
||||
mempty = newFix
|
||||
mappend = (<>)
|
||||
|
||||
instance Semigroup Fix where
|
||||
f1 <> f2 =
|
||||
-- FIXME: This might need to also discard adjacent zero-width ranges for
|
||||
-- when two fixes change the same AST node, e.g. `foo` -> "$(foo)"
|
||||
if or [ r2 `overlap` r1 | r1 <- fixReplacements f1, r2 <- fixReplacements f2 ]
|
||||
then f1
|
||||
else newFix {
|
||||
fixReplacements = fixReplacements f1 ++ fixReplacements f2
|
||||
}
|
||||
|
||||
-- Conveniently apply a transformation to positions in a Fix
|
||||
mapPositions :: (Position -> Position) -> Fix -> Fix
|
||||
mapPositions f = adjustFix
|
||||
where
|
||||
adjustReplacement rep =
|
||||
rep {
|
||||
repStartPos = f $ repStartPos rep,
|
||||
repEndPos = f $ repEndPos rep
|
||||
}
|
||||
adjustFix fix =
|
||||
fix {
|
||||
fixReplacements = map adjustReplacement $ fixReplacements fix
|
||||
}
|
||||
|
||||
-- Rewrite a Ranged from a tabstop of 8 to 1
|
||||
removeTabStops :: Ranged a => a -> Array Int String -> a
|
||||
removeTabStops range ls =
|
||||
let startColumn = realignColumn lineNo colNo range
|
||||
endColumn = realignColumn endLineNo endColNo range
|
||||
startPosition = (start range) { posColumn = startColumn }
|
||||
endPosition = (end range) { posColumn = endColumn } in
|
||||
setRange (startPosition, endPosition) range
|
||||
where
|
||||
realignColumn lineNo colNo c =
|
||||
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
|
||||
then real (ls ! fromIntegral (lineNo c)) 0 0 (colNo c)
|
||||
else colNo c
|
||||
real _ r v target | target <= v = r
|
||||
-- hit this case at the end of line, and if we don't hit the target
|
||||
-- return real + (target - v)
|
||||
real [] r v target = r + (target - v)
|
||||
real ('\t':rest) r v target = real rest (r+1) (v + 8 - (v `mod` 8)) target
|
||||
real (_:rest) r v target = real rest (r+1) (v+1) target
|
||||
lineNo = posLine . start
|
||||
endLineNo = posLine . end
|
||||
colNo = posColumn . start
|
||||
endColNo = posColumn . end
|
||||
|
||||
|
||||
-- A replacement that spans multiple line is applied by:
|
||||
-- 1. merging the affected lines into a single string using `unlines`
|
||||
-- 2. apply the replacement as if it only spanned a single line
|
||||
-- The tricky part is adjusting the end column of the replacement
|
||||
-- (the end line doesn't matter because there is only one line)
|
||||
--
|
||||
-- aaS <--- start of replacement (row 1 column 3)
|
||||
-- bbbb
|
||||
-- cEc
|
||||
-- \------- end of replacement (row 3 column 2)
|
||||
--
|
||||
-- a flattened string will look like:
|
||||
--
|
||||
-- "aaS\nbbbb\ncEc\n"
|
||||
--
|
||||
-- The column of E has to be adjusted by:
|
||||
-- 1. lengths of lines to be replaced, except the end row itself
|
||||
-- 2. end column of the replacement
|
||||
-- 3. number of '\n' by `unlines`
|
||||
multiToSingleLine :: [Fix] -> Array Int String -> ([Fix], String)
|
||||
multiToSingleLine fixes lines =
|
||||
(map (mapPositions adjust) fixes, unlines $ elems lines)
|
||||
where
|
||||
-- A prefix sum tree from line number to column shift.
|
||||
-- FIXME: The tree will be totally unbalanced.
|
||||
shiftTree :: PSTree Int
|
||||
shiftTree =
|
||||
foldl (\t (n,s) -> addPSValue (n+1) (length s + 1) t) newPSTree $
|
||||
assocs lines
|
||||
singleString = unlines $ elems lines
|
||||
adjust pos =
|
||||
pos {
|
||||
posLine = 1,
|
||||
posColumn = (posColumn pos) +
|
||||
(fromIntegral $ getPrefixSum (fromIntegral $ posLine pos) shiftTree)
|
||||
}
|
||||
|
||||
-- Apply a fix and return resulting lines.
|
||||
-- The number of lines can increase or decrease with no obvious mapping back, so
|
||||
-- the function does not return an array.
|
||||
applyFix :: Fix -> Array Int String -> [String]
|
||||
applyFix fix fileLines =
|
||||
let
|
||||
untabbed = fix {
|
||||
fixReplacements =
|
||||
map (\c -> removeTabStops c fileLines) $
|
||||
fixReplacements fix
|
||||
}
|
||||
(adjustedFixes, singleLine) = multiToSingleLine [untabbed] fileLines
|
||||
in
|
||||
lines . runFixer $ applyFixes2 adjustedFixes singleLine
|
||||
|
||||
|
||||
-- start and end comes from pos, which is 1 based
|
||||
prop_doReplace1 = doReplace 0 0 "1234" "A" == "A1234" -- technically not valid
|
||||
prop_doReplace2 = doReplace 1 1 "1234" "A" == "A1234"
|
||||
prop_doReplace3 = doReplace 1 2 "1234" "A" == "A234"
|
||||
prop_doReplace4 = doReplace 3 3 "1234" "A" == "12A34"
|
||||
prop_doReplace5 = doReplace 4 4 "1234" "A" == "123A4"
|
||||
prop_doReplace6 = doReplace 5 5 "1234" "A" == "1234A"
|
||||
doReplace start end o r =
|
||||
let si = fromIntegral (start-1)
|
||||
ei = fromIntegral (end-1)
|
||||
(x, xs) = splitAt si o
|
||||
z = drop (ei - si) xs
|
||||
in
|
||||
x ++ r ++ z
|
||||
|
||||
-- Fail if the 'expected' string is not result when applying 'fixes' to 'original'.
|
||||
testFixes :: String -> String -> [Fix] -> Bool
|
||||
testFixes expected original fixes =
|
||||
actual == expected
|
||||
where
|
||||
actual = runFixer (applyFixes2 fixes original)
|
||||
|
||||
|
||||
-- A Fixer allows doing repeated modifications of a string where each
|
||||
-- replacement automatically accounts for shifts from previous ones.
|
||||
type Fixer a = State (PSTree Int) a
|
||||
|
||||
-- Apply a single replacement using its indices into the original string.
|
||||
-- It does not handle multiple lines, all line indices must be 1.
|
||||
applyReplacement2 :: Replacement -> String -> Fixer String
|
||||
applyReplacement2 rep string = do
|
||||
tree <- get
|
||||
let transform pos = pos + getPrefixSum pos tree
|
||||
let originalPos = (repStartPos rep, repEndPos rep)
|
||||
(oldStart, oldEnd) = tmap (fromInteger . posColumn) originalPos
|
||||
(newStart, newEnd) = tmap transform (oldStart, oldEnd)
|
||||
|
||||
let (l1, l2) = tmap posLine originalPos in
|
||||
when (l1 /= 1 || l2 /= 1) $
|
||||
error "ShellCheck internal error, please report: bad cross-line fix"
|
||||
|
||||
let replacer = repString rep
|
||||
let shift = (length replacer) - (oldEnd - oldStart)
|
||||
let insertionPoint =
|
||||
case repInsertionPoint rep of
|
||||
InsertBefore -> oldStart
|
||||
InsertAfter -> oldEnd+1
|
||||
put $ addPSValue insertionPoint shift tree
|
||||
|
||||
return $ doReplace newStart newEnd string replacer
|
||||
where
|
||||
tmap f (a,b) = (f a, f b)
|
||||
|
||||
-- Apply a list of Replacements in the correct order
|
||||
applyReplacements2 :: [Replacement] -> String -> Fixer String
|
||||
applyReplacements2 reps str =
|
||||
foldM (flip applyReplacement2) str $
|
||||
reverse $ sortWith repPrecedence reps
|
||||
|
||||
-- Apply all fixes with replacements in the correct order
|
||||
applyFixes2 :: [Fix] -> String -> Fixer String
|
||||
applyFixes2 fixes = applyReplacements2 (concatMap fixReplacements fixes)
|
||||
|
||||
-- Get the final value of a Fixer.
|
||||
runFixer :: Fixer a -> a
|
||||
runFixer f = evalState f newPSTree
|
||||
|
||||
|
||||
|
||||
-- A Prefix Sum Tree that lets you look up the sum of values at and below an index.
|
||||
-- It's implemented essentially as a Fenwick tree without the bit-based balancing.
|
||||
-- The last Num is the sum of the left branch plus current element.
|
||||
data PSTree n = PSBranch n (PSTree n) (PSTree n) n | PSLeaf
|
||||
deriving (Show)
|
||||
|
||||
newPSTree :: Num n => PSTree n
|
||||
newPSTree = PSLeaf
|
||||
|
||||
-- Get the sum of values whose keys are <= 'target'
|
||||
getPrefixSum :: (Ord n, Num n) => n -> PSTree n -> n
|
||||
getPrefixSum = f 0
|
||||
where
|
||||
f sum _ PSLeaf = sum
|
||||
f sum target (PSBranch pivot left right cumulative) =
|
||||
case target `compare` pivot of
|
||||
LT -> f sum target left
|
||||
GT -> f (sum+cumulative) target right
|
||||
EQ -> sum+cumulative
|
||||
|
||||
-- Add a value to the Prefix Sum tree at the given index.
|
||||
-- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5
|
||||
addPSValue :: (Ord n, Num n) => n -> n -> PSTree n -> PSTree n
|
||||
addPSValue key value tree = if value == 0 then tree else f tree
|
||||
where
|
||||
f PSLeaf = PSBranch key PSLeaf PSLeaf value
|
||||
f (PSBranch pivot left right sum) =
|
||||
case key `compare` pivot of
|
||||
LT -> PSBranch pivot (f left) right (sum + value)
|
||||
GT -> PSBranch pivot left (f right) sum
|
||||
EQ -> PSBranch pivot left right (sum + value)
|
||||
|
||||
prop_pstreeSumsCorrectly kvs targets =
|
||||
let
|
||||
-- Trivial O(n * m) implementation
|
||||
dumbPrefixSums :: [(Int, Int)] -> [Int] -> [Int]
|
||||
dumbPrefixSums kvs targets =
|
||||
let prefixSum target = sum [v | (k,v) <- kvs, k <= target]
|
||||
in map prefixSum targets
|
||||
-- PSTree O(n * log m) implementation
|
||||
smartPrefixSums :: [(Int, Int)] -> [Int] -> [Int]
|
||||
smartPrefixSums kvs targets =
|
||||
let tree = foldl (\tree (pos, shift) -> addPSValue pos shift tree) PSLeaf kvs
|
||||
in map (\x -> getPrefixSum x tree) targets
|
||||
in smartPrefixSums kvs targets == dumbPrefixSums kvs targets
|
||||
|
||||
|
||||
-- Semi-convenient functions for constructing tests.
|
||||
testFix :: [Replacement] -> Fix
|
||||
testFix list = newFix {
|
||||
fixReplacements = list
|
||||
}
|
||||
|
||||
tFromStart :: Int -> Int -> String -> Int -> Replacement
|
||||
tFromStart start end repl order =
|
||||
newReplacement {
|
||||
repStartPos = newPosition {
|
||||
posLine = 1,
|
||||
posColumn = fromIntegral start
|
||||
},
|
||||
repEndPos = newPosition {
|
||||
posLine = 1,
|
||||
posColumn = fromIntegral end
|
||||
},
|
||||
repString = repl,
|
||||
repPrecedence = order,
|
||||
repInsertionPoint = InsertAfter
|
||||
}
|
||||
|
||||
tFromEnd start end repl order =
|
||||
(tFromStart start end repl order) {
|
||||
repInsertionPoint = InsertBefore
|
||||
}
|
||||
|
||||
prop_simpleFix1 = testFixes "hello world" "hell world" [
|
||||
testFix [
|
||||
tFromEnd 5 5 "o" 1
|
||||
]]
|
||||
|
||||
prop_anchorsLeft = testFixes "-->foobar<--" "--><--" [
|
||||
testFix [
|
||||
tFromStart 4 4 "foo" 1,
|
||||
tFromStart 4 4 "bar" 2
|
||||
]]
|
||||
|
||||
prop_anchorsRight = testFixes "-->foobar<--" "--><--" [
|
||||
testFix [
|
||||
tFromEnd 4 4 "bar" 1,
|
||||
tFromEnd 4 4 "foo" 2
|
||||
]]
|
||||
|
||||
prop_anchorsBoth1 = testFixes "-->foobar<--" "--><--" [
|
||||
testFix [
|
||||
tFromStart 4 4 "bar" 2,
|
||||
tFromEnd 4 4 "foo" 1
|
||||
]]
|
||||
|
||||
prop_anchorsBoth2 = testFixes "-->foobar<--" "--><--" [
|
||||
testFix [
|
||||
tFromEnd 4 4 "foo" 2,
|
||||
tFromStart 4 4 "bar" 1
|
||||
]]
|
||||
|
||||
prop_composeFixes1 = testFixes "cd \"$1\" || exit" "cd $1" [
|
||||
testFix [
|
||||
tFromStart 4 4 "\"" 10,
|
||||
tFromEnd 6 6 "\"" 10
|
||||
],
|
||||
testFix [
|
||||
tFromEnd 6 6 " || exit" 5
|
||||
]]
|
||||
|
||||
prop_composeFixes2 = testFixes "$(\"$1\")" "`$1`" [
|
||||
testFix [
|
||||
tFromStart 1 2 "$(" 5,
|
||||
tFromEnd 4 5 ")" 5
|
||||
],
|
||||
testFix [
|
||||
tFromStart 2 2 "\"" 10,
|
||||
tFromEnd 4 4 "\"" 10
|
||||
]]
|
||||
|
||||
prop_composeFixes3 = testFixes "(x)[x]" "xx" [
|
||||
testFix [
|
||||
tFromStart 1 1 "(" 4,
|
||||
tFromEnd 2 2 ")" 3,
|
||||
tFromStart 2 2 "[" 2,
|
||||
tFromEnd 3 3 "]" 1
|
||||
]]
|
||||
|
||||
prop_composeFixes4 = testFixes "(x)[x]" "xx" [
|
||||
testFix [
|
||||
tFromStart 1 1 "(" 4,
|
||||
tFromStart 2 2 "[" 3,
|
||||
tFromEnd 2 2 ")" 2,
|
||||
tFromEnd 3 3 "]" 1
|
||||
]]
|
||||
|
||||
prop_composeFixes5 = testFixes "\"$(x)\"" "`x`" [
|
||||
testFix [
|
||||
tFromStart 1 2 "$(" 2,
|
||||
tFromEnd 3 4 ")" 2,
|
||||
tFromStart 1 1 "\"" 1,
|
||||
tFromEnd 4 4 "\"" 1
|
||||
]]
|
||||
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
95
src/ShellCheck/Formatter/CheckStyle.hs
Normal file
95
src/ShellCheck/Formatter/CheckStyle.hs
Normal file
@@ -0,0 +1,95 @@
|
||||
{-
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.CheckStyle (format) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Data.Char
|
||||
import Data.List
|
||||
import GHC.Exts
|
||||
import System.IO
|
||||
|
||||
format :: IO Formatter
|
||||
format = return Formatter {
|
||||
header = do
|
||||
putStrLn "<?xml version='1.0' encoding='UTF-8'?>"
|
||||
putStrLn "<checkstyle version='4.3'>",
|
||||
|
||||
onFailure = outputError,
|
||||
onResult = outputResults,
|
||||
|
||||
footer = putStrLn "</checkstyle>"
|
||||
}
|
||||
|
||||
outputResults cr sys =
|
||||
if null comments
|
||||
then outputFile (crFilename cr) "" []
|
||||
else mapM_ outputGroup fileGroups
|
||||
where
|
||||
comments = crComments cr
|
||||
fileGroups = groupWith sourceFile comments
|
||||
outputGroup group = do
|
||||
let filename = sourceFile (head group)
|
||||
result <- siReadFile sys (Just True) filename
|
||||
let contents = either (const "") id result
|
||||
outputFile filename contents group
|
||||
|
||||
outputFile filename contents warnings = do
|
||||
let comments = makeNonVirtual warnings contents
|
||||
putStrLn . formatFile filename $ comments
|
||||
|
||||
formatFile name comments = concat [
|
||||
"<file ", attr "name" name, ">\n",
|
||||
concatMap formatComment comments,
|
||||
"</file>"
|
||||
]
|
||||
|
||||
formatComment c = concat [
|
||||
"<error ",
|
||||
attr "line" $ show . lineNo $ c,
|
||||
attr "column" $ show . colNo $ c,
|
||||
attr "severity" . severity $ severityText c,
|
||||
attr "message" $ messageText c,
|
||||
attr "source" $ "ShellCheck.SC" ++ show (codeNo c),
|
||||
"/>\n"
|
||||
]
|
||||
|
||||
outputError file error = putStrLn $ concat [
|
||||
"<file ", attr "name" file, ">\n",
|
||||
"<error ",
|
||||
attr "line" "1",
|
||||
attr "column" "1",
|
||||
attr "severity" "error",
|
||||
attr "message" error,
|
||||
attr "source" "ShellCheck",
|
||||
"/>\n",
|
||||
"</file>"
|
||||
]
|
||||
|
||||
|
||||
attr s v = concat [ s, "='", escape v, "' " ]
|
||||
escape = concatMap escape'
|
||||
escape' c = if isOk c then [c] else "&#" ++ show (ord c) ++ ";"
|
||||
isOk x = any ($x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")]
|
||||
|
||||
severity "error" = "error"
|
||||
severity "warning" = "warning"
|
||||
severity _ = "info"
|
255
src/ShellCheck/Formatter/Diff.hs
Normal file
255
src/ShellCheck/Formatter/Diff.hs
Normal file
@@ -0,0 +1,255 @@
|
||||
{-
|
||||
Copyright 2019 Vidar 'koala_man' 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 #-}
|
||||
module ShellCheck.Formatter.Diff (format, ShellCheck.Formatter.Diff.runTests) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Fixer
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Control.Monad
|
||||
import Data.Algorithm.Diff
|
||||
import Data.Array
|
||||
import Data.IORef
|
||||
import Data.List
|
||||
import qualified Data.Monoid as Monoid
|
||||
import Data.Maybe
|
||||
import qualified Data.Map as M
|
||||
import GHC.Exts (sortWith)
|
||||
import System.IO
|
||||
import System.FilePath
|
||||
|
||||
import Test.QuickCheck
|
||||
|
||||
format :: FormatterOptions -> IO Formatter
|
||||
format options = do
|
||||
foundIssues <- newIORef False
|
||||
reportedIssues <- newIORef False
|
||||
shouldColor <- shouldOutputColor (foColorOption options)
|
||||
let color = if shouldColor then colorize else nocolor
|
||||
return Formatter {
|
||||
header = return (),
|
||||
footer = checkFooter foundIssues reportedIssues color,
|
||||
onFailure = reportFailure color,
|
||||
onResult = reportResult foundIssues reportedIssues color
|
||||
}
|
||||
|
||||
|
||||
contextSize = 3
|
||||
red = 31
|
||||
green = 32
|
||||
yellow = 33
|
||||
cyan = 36
|
||||
bold = 1
|
||||
|
||||
nocolor n = id
|
||||
colorize n s = (ansi n) ++ s ++ (ansi 0)
|
||||
ansi n = "\x1B[" ++ show n ++ "m"
|
||||
|
||||
printErr :: ColorFunc -> String -> IO ()
|
||||
printErr color = hPutStrLn stderr . color bold . color red
|
||||
reportFailure color file msg = printErr color $ file ++ ": " ++ msg
|
||||
|
||||
checkFooter foundIssues reportedIssues color = do
|
||||
found <- readIORef foundIssues
|
||||
output <- readIORef reportedIssues
|
||||
when (found && not output) $
|
||||
printErr color "Issues were detected, but none were auto-fixable. Use another format to see them."
|
||||
|
||||
type ColorFunc = (Int -> String -> String)
|
||||
data LFStatus = LinefeedMissing | LinefeedOk
|
||||
data DiffDoc a = DiffDoc String LFStatus [DiffRegion a]
|
||||
data DiffRegion a = DiffRegion (Int, Int) (Int, Int) [Diff a]
|
||||
|
||||
reportResult :: (IORef Bool) -> (IORef Bool) -> ColorFunc -> CheckResult -> SystemInterface IO -> IO ()
|
||||
reportResult foundIssues reportedIssues color result sys = do
|
||||
let comments = crComments result
|
||||
unless (null comments) $ writeIORef foundIssues True
|
||||
let suggestedFixes = mapMaybe pcFix comments
|
||||
let fixmap = buildFixMap suggestedFixes
|
||||
mapM_ output $ M.toList fixmap
|
||||
where
|
||||
output (name, fix) = do
|
||||
file <- siReadFile sys (Just True) name
|
||||
case file of
|
||||
Right contents -> do
|
||||
putStrLn $ formatDoc color $ makeDiff name contents fix
|
||||
writeIORef reportedIssues True
|
||||
Left msg -> reportFailure color name msg
|
||||
|
||||
hasTrailingLinefeed str =
|
||||
case str of
|
||||
[] -> True
|
||||
_ -> last str == '\n'
|
||||
|
||||
coversLastLine regions =
|
||||
case regions of
|
||||
[] -> False
|
||||
_ -> (fst $ last regions)
|
||||
|
||||
-- TODO: Factor this out into a unified diff library because we're doing a lot
|
||||
-- of the heavy lifting anyways.
|
||||
makeDiff :: String -> String -> Fix -> DiffDoc String
|
||||
makeDiff name contents fix = do
|
||||
let hunks = groupDiff $ computeDiff contents fix
|
||||
let lf = if coversLastLine hunks && not (hasTrailingLinefeed contents)
|
||||
then LinefeedMissing
|
||||
else LinefeedOk
|
||||
DiffDoc name lf $ findRegions hunks
|
||||
|
||||
computeDiff :: String -> Fix -> [Diff String]
|
||||
computeDiff contents fix =
|
||||
let old = lines contents
|
||||
array = listArray (1, fromIntegral $ (length old)) old
|
||||
new = applyFix fix array
|
||||
in getDiff old new
|
||||
|
||||
-- Group changes into hunks
|
||||
groupDiff :: [Diff a] -> [(Bool, [Diff a])]
|
||||
groupDiff = filter (\(_, l) -> not (null l)) . hunt []
|
||||
where
|
||||
-- Churn through 'Both's until we find a difference
|
||||
hunt current [] = [(False, reverse current)]
|
||||
hunt current (x@Both {}:rest) = hunt (x:current) rest
|
||||
hunt current list =
|
||||
let (context, previous) = splitAt contextSize current
|
||||
in (False, reverse previous) : gather context 0 list
|
||||
|
||||
-- Pick out differences until we find a run of Both's
|
||||
gather current n [] =
|
||||
let (extras, patch) = splitAt (max 0 $ n - contextSize) current
|
||||
in [(True, reverse patch), (False, reverse extras)]
|
||||
|
||||
gather current n list@(Both {}:_) | n == contextSize*2 =
|
||||
let (context, previous) = splitAt contextSize current
|
||||
in (True, reverse previous) : hunt context list
|
||||
|
||||
gather current n (x@Both {}:rest) = gather (x:current) (n+1) rest
|
||||
gather current n (x:rest) = gather (x:current) 0 rest
|
||||
|
||||
-- Get line numbers for hunks
|
||||
findRegions :: [(Bool, [Diff String])] -> [DiffRegion String]
|
||||
findRegions = find' 1 1
|
||||
where
|
||||
find' _ _ [] = []
|
||||
find' left right ((output, run):rest) =
|
||||
let (dl, dr) = countDelta run
|
||||
remainder = find' (left+dl) (right+dr) rest
|
||||
in
|
||||
if output
|
||||
then DiffRegion (left, dl) (right, dr) run : remainder
|
||||
else remainder
|
||||
|
||||
-- Get left/right line counts for a hunk
|
||||
countDelta :: [Diff a] -> (Int, Int)
|
||||
countDelta = count' 0 0
|
||||
where
|
||||
count' left right [] = (left, right)
|
||||
count' left right (x:rest) =
|
||||
case x of
|
||||
Both {} -> count' (left+1) (right+1) rest
|
||||
First {} -> count' (left+1) right rest
|
||||
Second {} -> count' left (right+1) rest
|
||||
|
||||
formatRegion :: ColorFunc -> LFStatus -> DiffRegion String -> String
|
||||
formatRegion color lf (DiffRegion left right diffs) =
|
||||
let header = color cyan ("@@ -" ++ (tup left) ++ " +" ++ (tup right) ++" @@")
|
||||
in
|
||||
unlines $ header : reverse (getStrings lf (reverse diffs))
|
||||
where
|
||||
noLF = "\\ No newline at end of file"
|
||||
|
||||
getStrings LinefeedOk list = map format list
|
||||
getStrings LinefeedMissing list@((Both _ _):_) = noLF : map format list
|
||||
getStrings LinefeedMissing list@((First _):_) = noLF : map format list
|
||||
getStrings LinefeedMissing (last:rest) = format last : getStrings LinefeedMissing rest
|
||||
|
||||
tup (a,b) = (show a) ++ "," ++ (show b)
|
||||
format (Both x _) = ' ':x
|
||||
format (First x) = color red $ '-':x
|
||||
format (Second x) = color green $ '+':x
|
||||
|
||||
splitLast [] = ([], [])
|
||||
splitLast x =
|
||||
let (last, rest) = splitAt 1 $ reverse x
|
||||
in (reverse rest, last)
|
||||
|
||||
formatDoc color (DiffDoc name lf regions) =
|
||||
let (most, last) = splitLast regions
|
||||
in
|
||||
(color bold $ "--- " ++ ("a" </> name)) ++ "\n" ++
|
||||
(color bold $ "+++ " ++ ("b" </> name)) ++ "\n" ++
|
||||
concatMap (formatRegion color LinefeedOk) most ++
|
||||
concatMap (formatRegion color lf) last
|
||||
|
||||
-- Create a Map from filename to Fix
|
||||
buildFixMap :: [Fix] -> M.Map String Fix
|
||||
buildFixMap fixes = perFile
|
||||
where
|
||||
splitFixes = concatMap splitFixByFile fixes
|
||||
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 = map makeFix $ groupBy sameFile (fixReplacements fix)
|
||||
where
|
||||
sameFile rep1 rep2 = (posFile $ repStartPos rep1) == (posFile $ repStartPos rep2)
|
||||
makeFix reps = newFix { fixReplacements = reps }
|
||||
|
||||
groupByMap :: (Ord k, Monoid v) => (v -> k) -> [v] -> M.Map k v
|
||||
groupByMap f = M.fromListWith Monoid.mappend . map (\x -> (f x, x))
|
||||
|
||||
-- For building unit tests
|
||||
b n = Both n n
|
||||
l = First
|
||||
r = Second
|
||||
|
||||
prop_identifiesProperContext = groupDiff [b 1, b 2, b 3, b 4, l 5, b 6, b 7, b 8, b 9] ==
|
||||
[(False, [b 1]), -- Omitted
|
||||
(True, [b 2, b 3, b 4, l 5, b 6, b 7, b 8]), -- A change with three lines of context
|
||||
(False, [b 9])] -- Omitted
|
||||
|
||||
prop_includesContextFromStartIfNecessary = groupDiff [b 4, l 5, b 6, b 7, b 8, b 9] ==
|
||||
[ -- Nothing omitted
|
||||
(True, [b 4, l 5, b 6, b 7, b 8]), -- A change with three lines of context
|
||||
(False, [b 9])] -- Omitted
|
||||
|
||||
prop_includesContextUntilEndIfNecessary = groupDiff [b 4, l 5] ==
|
||||
[ -- Nothing omitted
|
||||
(True, [b 4, l 5])
|
||||
] -- Nothing Omitted
|
||||
|
||||
prop_splitsIntoMultipleHunks = groupDiff [l 1, b 1, b 2, b 3, b 4, b 5, b 6, b 7, r 8] ==
|
||||
[ -- Nothing omitted
|
||||
(True, [l 1, b 1, b 2, b 3]),
|
||||
(False, [b 4]),
|
||||
(True, [b 5, b 6, b 7, r 8])
|
||||
] -- Nothing Omitted
|
||||
|
||||
prop_splitsIntoMultipleHunksUnlessTouching = groupDiff [l 1, b 1, b 2, b 3, b 4, b 5, b 6, r 7] ==
|
||||
[
|
||||
(True, [l 1, b 1, b 2, b 3, b 4, b 5, b 6, r 7])
|
||||
]
|
||||
|
||||
prop_countDeltasWorks = countDelta [b 1, l 2, r 3, r 4, b 5] == (3,4)
|
||||
prop_countDeltasWorks2 = countDelta [] == (0,0)
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
82
src/ShellCheck/Formatter/Format.hs
Normal file
82
src/ShellCheck/Formatter/Format.hs
Normal file
@@ -0,0 +1,82 @@
|
||||
{-
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.Format where
|
||||
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Fixer
|
||||
|
||||
import Control.Monad
|
||||
import Data.Array
|
||||
import Data.List
|
||||
import System.IO
|
||||
import System.Info
|
||||
import System.Environment
|
||||
|
||||
-- A formatter that carries along an arbitrary piece of data
|
||||
data Formatter = Formatter {
|
||||
header :: IO (),
|
||||
onResult :: CheckResult -> SystemInterface IO -> IO (),
|
||||
onFailure :: FilePath -> ErrorMessage -> IO (),
|
||||
footer :: IO ()
|
||||
}
|
||||
|
||||
sourceFile = posFile . pcStartPos
|
||||
lineNo = posLine . pcStartPos
|
||||
endLineNo = posLine . pcEndPos
|
||||
colNo = posColumn . pcStartPos
|
||||
endColNo = posColumn . pcEndPos
|
||||
codeNo = cCode . pcComment
|
||||
messageText = cMessage . pcComment
|
||||
|
||||
severityText :: PositionedComment -> String
|
||||
severityText pc =
|
||||
case cSeverity (pcComment pc) of
|
||||
ErrorC -> "error"
|
||||
WarningC -> "warning"
|
||||
InfoC -> "info"
|
||||
StyleC -> "style"
|
||||
|
||||
-- Realign comments from a tabstop of 8 to 1
|
||||
makeNonVirtual comments contents =
|
||||
map fix comments
|
||||
where
|
||||
list = lines contents
|
||||
arr = listArray (1, length list) list
|
||||
untabbedFix f = newFix {
|
||||
fixReplacements = map (\r -> removeTabStops r arr) (fixReplacements f)
|
||||
}
|
||||
fix c = (removeTabStops c arr) {
|
||||
pcFix = fmap untabbedFix (pcFix c)
|
||||
}
|
||||
|
||||
|
||||
shouldOutputColor :: ColorOption -> IO Bool
|
||||
shouldOutputColor colorOption =
|
||||
case colorOption of
|
||||
ColorAlways -> return True
|
||||
ColorNever -> return False
|
||||
ColorAuto -> do
|
||||
isTerminal <- hIsTerminalDevice stdout
|
||||
term <- lookupEnv "TERM"
|
||||
let windows = "mingw" `isPrefixOf` os
|
||||
let dumbTerm = term `elem` [Just "dumb", Just "", Nothing]
|
||||
let isUsableTty = isTerminal && not windows && not dumbTerm
|
||||
return isUsableTty
|
65
src/ShellCheck/Formatter/GCC.hs
Normal file
65
src/ShellCheck/Formatter/GCC.hs
Normal file
@@ -0,0 +1,65 @@
|
||||
{-
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.GCC (format) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Data.List
|
||||
import GHC.Exts
|
||||
import System.IO
|
||||
|
||||
format :: IO Formatter
|
||||
format = return Formatter {
|
||||
header = return (),
|
||||
footer = return (),
|
||||
onFailure = outputError,
|
||||
onResult = outputAll
|
||||
}
|
||||
|
||||
outputError file error = hPutStrLn stderr $ file ++ ": " ++ error
|
||||
|
||||
outputAll cr sys = mapM_ f groups
|
||||
where
|
||||
comments = crComments cr
|
||||
groups = groupWith sourceFile comments
|
||||
f :: [PositionedComment] -> IO ()
|
||||
f group = do
|
||||
let filename = sourceFile (head group)
|
||||
result <- siReadFile sys (Just True) filename
|
||||
let contents = either (const "") id result
|
||||
outputResult filename contents group
|
||||
|
||||
outputResult filename contents warnings = do
|
||||
let comments = makeNonVirtual warnings contents
|
||||
mapM_ (putStrLn . formatComment filename) comments
|
||||
|
||||
formatComment filename c = concat [
|
||||
filename, ":",
|
||||
show $ lineNo c, ":",
|
||||
show $ colNo c, ": ",
|
||||
case severityText c of
|
||||
"error" -> "error"
|
||||
"warning" -> "warning"
|
||||
_ -> "note",
|
||||
": ",
|
||||
concat . lines $ messageText c,
|
||||
" [SC", show $ codeNo c, "]"
|
||||
]
|
110
src/ShellCheck/Formatter/JSON.hs
Normal file
110
src/ShellCheck/Formatter/JSON.hs
Normal file
@@ -0,0 +1,110 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.JSON (format) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Data.Aeson
|
||||
import Data.IORef
|
||||
import Data.Monoid
|
||||
import GHC.Exts
|
||||
import System.IO
|
||||
import qualified Data.ByteString.Lazy.Char8 as BL
|
||||
|
||||
format :: IO Formatter
|
||||
format = do
|
||||
ref <- newIORef []
|
||||
return Formatter {
|
||||
header = return (),
|
||||
onResult = collectResult ref,
|
||||
onFailure = outputError,
|
||||
footer = finish ref
|
||||
}
|
||||
|
||||
instance ToJSON Replacement where
|
||||
toJSON replacement =
|
||||
let start = repStartPos replacement
|
||||
end = repEndPos replacement
|
||||
str = repString replacement in
|
||||
object [
|
||||
"precedence" .= repPrecedence replacement,
|
||||
"insertionPoint" .=
|
||||
case repInsertionPoint replacement of
|
||||
InsertBefore -> "beforeStart" :: String
|
||||
InsertAfter -> "afterEnd",
|
||||
"line" .= posLine start,
|
||||
"column" .= posColumn start,
|
||||
"endLine" .= posLine end,
|
||||
"endColumn" .= posColumn end,
|
||||
"replacement" .= str
|
||||
]
|
||||
|
||||
instance ToJSON PositionedComment where
|
||||
toJSON comment =
|
||||
let start = pcStartPos comment
|
||||
end = pcEndPos comment
|
||||
c = pcComment comment in
|
||||
object [
|
||||
"file" .= posFile start,
|
||||
"line" .= posLine start,
|
||||
"endLine" .= posLine end,
|
||||
"column" .= posColumn start,
|
||||
"endColumn" .= posColumn end,
|
||||
"level" .= severityText comment,
|
||||
"code" .= cCode c,
|
||||
"message" .= cMessage c,
|
||||
"fix" .= pcFix comment
|
||||
]
|
||||
|
||||
toEncoding comment =
|
||||
let start = pcStartPos comment
|
||||
end = pcEndPos comment
|
||||
c = pcComment comment in
|
||||
pairs (
|
||||
"file" .= posFile start
|
||||
<> "line" .= posLine start
|
||||
<> "endLine" .= posLine end
|
||||
<> "column" .= posColumn start
|
||||
<> "endColumn" .= posColumn end
|
||||
<> "level" .= severityText comment
|
||||
<> "code" .= cCode c
|
||||
<> "message" .= cMessage c
|
||||
<> "fix" .= pcFix comment
|
||||
)
|
||||
|
||||
instance ToJSON Fix where
|
||||
toJSON fix = object [
|
||||
"replacements" .= fixReplacements fix
|
||||
]
|
||||
|
||||
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
|
||||
|
||||
collectResult ref cr sys = mapM_ f groups
|
||||
where
|
||||
comments = crComments cr
|
||||
groups = groupWith sourceFile comments
|
||||
f :: [PositionedComment] -> IO ()
|
||||
f group = modifyIORef ref (\x -> comments ++ x)
|
||||
|
||||
finish ref = do
|
||||
list <- readIORef ref
|
||||
BL.putStrLn $ encode list
|
127
src/ShellCheck/Formatter/JSON1.hs
Normal file
127
src/ShellCheck/Formatter/JSON1.hs
Normal file
@@ -0,0 +1,127 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.JSON1 (format) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Data.Aeson
|
||||
import Data.IORef
|
||||
import Data.Monoid
|
||||
import GHC.Exts
|
||||
import System.IO
|
||||
import qualified Data.ByteString.Lazy.Char8 as BL
|
||||
|
||||
format :: IO Formatter
|
||||
format = do
|
||||
ref <- newIORef []
|
||||
return Formatter {
|
||||
header = return (),
|
||||
onResult = collectResult ref,
|
||||
onFailure = outputError,
|
||||
footer = finish ref
|
||||
}
|
||||
|
||||
data Json1Output = Json1Output {
|
||||
comments :: [PositionedComment]
|
||||
}
|
||||
|
||||
instance ToJSON Json1Output where
|
||||
toJSON result = object [
|
||||
"comments" .= comments result
|
||||
]
|
||||
toEncoding result = pairs (
|
||||
"comments" .= comments result
|
||||
)
|
||||
|
||||
instance ToJSON Replacement where
|
||||
toJSON replacement =
|
||||
let start = repStartPos replacement
|
||||
end = repEndPos replacement
|
||||
str = repString replacement in
|
||||
object [
|
||||
"precedence" .= repPrecedence replacement,
|
||||
"insertionPoint" .=
|
||||
case repInsertionPoint replacement of
|
||||
InsertBefore -> "beforeStart" :: String
|
||||
InsertAfter -> "afterEnd",
|
||||
"line" .= posLine start,
|
||||
"column" .= posColumn start,
|
||||
"endLine" .= posLine end,
|
||||
"endColumn" .= posColumn end,
|
||||
"replacement" .= str
|
||||
]
|
||||
|
||||
instance ToJSON PositionedComment where
|
||||
toJSON comment =
|
||||
let start = pcStartPos comment
|
||||
end = pcEndPos comment
|
||||
c = pcComment comment in
|
||||
object [
|
||||
"file" .= posFile start,
|
||||
"line" .= posLine start,
|
||||
"endLine" .= posLine end,
|
||||
"column" .= posColumn start,
|
||||
"endColumn" .= posColumn end,
|
||||
"level" .= severityText comment,
|
||||
"code" .= cCode c,
|
||||
"message" .= cMessage c,
|
||||
"fix" .= pcFix comment
|
||||
]
|
||||
|
||||
toEncoding comment =
|
||||
let start = pcStartPos comment
|
||||
end = pcEndPos comment
|
||||
c = pcComment comment in
|
||||
pairs (
|
||||
"file" .= posFile start
|
||||
<> "line" .= posLine start
|
||||
<> "endLine" .= posLine end
|
||||
<> "column" .= posColumn start
|
||||
<> "endColumn" .= posColumn end
|
||||
<> "level" .= severityText comment
|
||||
<> "code" .= cCode c
|
||||
<> "message" .= cMessage c
|
||||
<> "fix" .= pcFix comment
|
||||
)
|
||||
|
||||
instance ToJSON Fix where
|
||||
toJSON fix = object [
|
||||
"replacements" .= fixReplacements fix
|
||||
]
|
||||
|
||||
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
|
||||
|
||||
collectResult ref cr sys = mapM_ f groups
|
||||
where
|
||||
comments = crComments cr
|
||||
groups = groupWith sourceFile comments
|
||||
f :: [PositionedComment] -> IO ()
|
||||
f group = do
|
||||
let filename = sourceFile (head group)
|
||||
result <- siReadFile sys (Just True) filename
|
||||
let contents = either (const "") id result
|
||||
let comments' = makeNonVirtual comments contents
|
||||
modifyIORef ref (\x -> comments' ++ x)
|
||||
|
||||
finish ref = do
|
||||
list <- readIORef ref
|
||||
BL.putStrLn $ encode $ Json1Output { comments = list }
|
36
src/ShellCheck/Formatter/Quiet.hs
Normal file
36
src/ShellCheck/Formatter/Quiet.hs
Normal file
@@ -0,0 +1,36 @@
|
||||
{-
|
||||
Copyright 2019 Austin Voecks
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.Quiet (format) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Control.Monad
|
||||
import Data.IORef
|
||||
import System.Exit
|
||||
|
||||
format :: FormatterOptions -> IO Formatter
|
||||
format options =
|
||||
return Formatter {
|
||||
header = return (),
|
||||
footer = return (),
|
||||
onFailure = \ _ _ -> exitFailure,
|
||||
onResult = \ result _ -> unless (null $ crComments result) exitFailure
|
||||
}
|
196
src/ShellCheck/Formatter/TTY.hs
Normal file
196
src/ShellCheck/Formatter/TTY.hs
Normal file
@@ -0,0 +1,196 @@
|
||||
{-
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.TTY (format) where
|
||||
|
||||
import ShellCheck.Fixer
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Control.Monad
|
||||
import Data.Array
|
||||
import Data.Foldable
|
||||
import Data.Ord
|
||||
import Data.IORef
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import GHC.Exts
|
||||
import System.IO
|
||||
import System.Info
|
||||
|
||||
wikiLink = "https://www.shellcheck.net/wiki/"
|
||||
|
||||
-- An arbitrary Ord thing to order warnings
|
||||
type Ranking = (Char, Severity, Integer)
|
||||
-- Ansi coloring function
|
||||
type ColorFunc = (String -> String -> String)
|
||||
|
||||
format :: FormatterOptions -> IO Formatter
|
||||
format options = do
|
||||
topErrorRef <- newIORef []
|
||||
return Formatter {
|
||||
header = return (),
|
||||
footer = outputWiki topErrorRef,
|
||||
onFailure = outputError options,
|
||||
onResult = outputResult options topErrorRef
|
||||
}
|
||||
|
||||
colorForLevel level =
|
||||
case level of
|
||||
"error" -> 31 -- red
|
||||
"warning" -> 33 -- yellow
|
||||
"info" -> 32 -- green
|
||||
"style" -> 32 -- green
|
||||
"verbose" -> 32 -- green
|
||||
"message" -> 1 -- bold
|
||||
"source" -> 0 -- none
|
||||
_ -> 0 -- none
|
||||
|
||||
rankError :: PositionedComment -> Ranking
|
||||
rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err)
|
||||
where
|
||||
ranking =
|
||||
if cCode (pcComment err) `elem` uninteresting
|
||||
then 'Z'
|
||||
else 'A'
|
||||
|
||||
-- A list of the most generic, least directly helpful
|
||||
-- error codes to downrank.
|
||||
uninteresting = [
|
||||
1009, -- Mentioned parser error was..
|
||||
1019, -- Expected this to be an argument
|
||||
1036, -- ( is invalid here
|
||||
1047, -- Expected 'fi'
|
||||
1062, -- Expected 'done'
|
||||
1070, -- Parsing stopped here (generic)
|
||||
1072, -- Missing/unexpected ..
|
||||
1073, -- Couldn't parse this ..
|
||||
1088, -- Parsing stopped here (paren)
|
||||
1089 -- Parsing stopped here (keyword)
|
||||
]
|
||||
|
||||
appendComments errRef comments max = do
|
||||
previous <- readIORef errRef
|
||||
let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments
|
||||
writeIORef errRef . take max . nubBy equal . sort $ previous ++ current
|
||||
where
|
||||
fst3 (x,_,_) = x
|
||||
equal x y = fst3 x == fst3 y
|
||||
|
||||
outputWiki :: IORef [(Ranking, Integer, String)] -> IO ()
|
||||
outputWiki errRef = do
|
||||
issues <- readIORef errRef
|
||||
unless (null issues) $ do
|
||||
putStrLn "For more information:"
|
||||
mapM_ showErr issues
|
||||
where
|
||||
showErr (_, code, msg) =
|
||||
putStrLn $ " " ++ wikiLink ++ "SC" ++ show code ++ " -- " ++ shorten msg
|
||||
limit = 36
|
||||
shorten msg =
|
||||
if length msg < limit
|
||||
then msg
|
||||
else (take (limit-3) msg) ++ "..."
|
||||
|
||||
outputError options file error = do
|
||||
color <- getColorFunc $ foColorOption options
|
||||
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
|
||||
|
||||
outputResult options ref result sys = do
|
||||
color <- getColorFunc $ foColorOption options
|
||||
let comments = crComments result
|
||||
appendComments ref comments (fromIntegral $ foWikiLinkCount options)
|
||||
let fileGroups = groupWith sourceFile comments
|
||||
mapM_ (outputForFile color sys) fileGroups
|
||||
|
||||
outputForFile color sys comments = do
|
||||
let fileName = sourceFile (head comments)
|
||||
result <- siReadFile sys (Just True) fileName
|
||||
let contents = either (const "") id result
|
||||
let fileLinesList = lines contents
|
||||
let lineCount = length fileLinesList
|
||||
let fileLines = listArray (1, lineCount) fileLinesList
|
||||
let groups = groupWith lineNo comments
|
||||
forM_ groups $ \commentsForLine -> do
|
||||
let lineNum = fromIntegral $ lineNo (head commentsForLine)
|
||||
let line = if lineNum < 1 || lineNum > lineCount
|
||||
then ""
|
||||
else fileLines ! fromIntegral lineNum
|
||||
putStrLn ""
|
||||
putStrLn $ color "message" $
|
||||
"In " ++ fileName ++" line " ++ show lineNum ++ ":"
|
||||
putStrLn (color "source" line)
|
||||
forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c
|
||||
putStrLn ""
|
||||
showFixedString color commentsForLine (fromIntegral lineNum) fileLines
|
||||
|
||||
-- Pick out only the lines necessary to show a fix in action
|
||||
sliceFile :: Fix -> Array Int String -> (Fix, Array Int String)
|
||||
sliceFile fix lines =
|
||||
(mapPositions adjust fix, sliceLines lines)
|
||||
where
|
||||
(minLine, maxLine) =
|
||||
foldl (\(mm, mx) pos -> ((min mm $ fromIntegral $ posLine pos), (max mx $ fromIntegral $ posLine pos)))
|
||||
(maxBound, minBound) $
|
||||
concatMap (\x -> [repStartPos x, repEndPos x]) $ fixReplacements fix
|
||||
sliceLines :: Array Int String -> Array Int String
|
||||
sliceLines = ixmap (1, maxLine - minLine + 1) (\x -> x + minLine - 1)
|
||||
adjust pos =
|
||||
pos {
|
||||
posLine = posLine pos - (fromIntegral minLine) + 1
|
||||
}
|
||||
|
||||
showFixedString :: ColorFunc -> [PositionedComment] -> Int -> Array Int String -> IO ()
|
||||
showFixedString color comments lineNum fileLines =
|
||||
let line = fileLines ! fromIntegral lineNum in
|
||||
case mapMaybe pcFix comments of
|
||||
[] -> return ()
|
||||
fixes -> do
|
||||
-- Folding automatically removes overlap
|
||||
let mergedFix = fold fixes
|
||||
-- We show the complete, associated fixes, whether or not it includes this
|
||||
-- and/or other unrelated lines.
|
||||
let (excerptFix, excerpt) = sliceFile mergedFix fileLines
|
||||
-- in the spirit of error prone
|
||||
putStrLn $ color "message" "Did you mean: "
|
||||
putStrLn $ unlines $ applyFix excerptFix excerpt
|
||||
|
||||
cuteIndent :: PositionedComment -> String
|
||||
cuteIndent comment =
|
||||
replicate (fromIntegral $ colNo comment - 1) ' ' ++
|
||||
makeArrow ++ " " ++ code (codeNo comment) ++ " (" ++ severityText comment ++ "): " ++ messageText comment
|
||||
where
|
||||
arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^"
|
||||
makeArrow =
|
||||
let sameLine = lineNo comment == endLineNo comment
|
||||
delta = endColNo comment - colNo comment
|
||||
in
|
||||
if sameLine && delta > 2 && delta < 32 then arrow delta else "^--"
|
||||
|
||||
code num = "SC" ++ show num
|
||||
|
||||
getColorFunc :: ColorOption -> IO ColorFunc
|
||||
getColorFunc colorOption = do
|
||||
useColor <- shouldOutputColor colorOption
|
||||
return $ if useColor then colorComment else const id
|
||||
where
|
||||
colorComment level comment =
|
||||
ansi (colorForLevel level) ++ comment ++ clear
|
||||
clear = ansi 0
|
||||
ansi n = "\x1B[" ++ show n ++ "m"
|
329
src/ShellCheck/Interface.hs
Normal file
329
src/ShellCheck/Interface.hs
Normal file
@@ -0,0 +1,329 @@
|
||||
{-
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
|
||||
module ShellCheck.Interface
|
||||
(
|
||||
SystemInterface(..)
|
||||
, CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csOptionalChecks)
|
||||
, CheckResult(crFilename, crComments)
|
||||
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
|
||||
, ParseResult(prComments, prTokenPositions, prRoot)
|
||||
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asOptionalChecks)
|
||||
, AnalysisResult(arComments)
|
||||
, FormatterOptions(foColorOption, foWikiLinkCount)
|
||||
, Shell(Ksh, Sh, Bash, Dash)
|
||||
, ExecutionMode(Executed, Sourced)
|
||||
, ErrorMessage
|
||||
, Code
|
||||
, Severity(ErrorC, WarningC, InfoC, StyleC)
|
||||
, Position(posFile, posLine, posColumn)
|
||||
, Comment(cSeverity, cCode, cMessage)
|
||||
, PositionedComment(pcStartPos , pcEndPos , pcComment, pcFix)
|
||||
, ColorOption(ColorAuto, ColorAlways, ColorNever)
|
||||
, TokenComment(tcId, tcComment, tcFix)
|
||||
, emptyCheckResult
|
||||
, newParseResult
|
||||
, newAnalysisSpec
|
||||
, newAnalysisResult
|
||||
, newFormatterOptions
|
||||
, newPosition
|
||||
, newTokenComment
|
||||
, mockedSystemInterface
|
||||
, mockRcFile
|
||||
, newParseSpec
|
||||
, emptyCheckSpec
|
||||
, newPositionedComment
|
||||
, newComment
|
||||
, Fix(fixReplacements)
|
||||
, newFix
|
||||
, InsertionPoint(InsertBefore, InsertAfter)
|
||||
, Replacement(repStartPos, repEndPos, repString, repPrecedence, repInsertionPoint)
|
||||
, newReplacement
|
||||
, CheckDescription(cdName, cdDescription, cdPositive, cdNegative)
|
||||
, newCheckDescription
|
||||
) where
|
||||
|
||||
import ShellCheck.AST
|
||||
|
||||
import Control.DeepSeq
|
||||
import Control.Monad.Identity
|
||||
import Data.List
|
||||
import Data.Monoid
|
||||
import Data.Ord
|
||||
import Data.Semigroup
|
||||
import GHC.Generics (Generic)
|
||||
import qualified Data.Map as Map
|
||||
|
||||
|
||||
data SystemInterface m = SystemInterface {
|
||||
-- | Given:
|
||||
-- What annotations say about including external files (if anything)
|
||||
-- A resolved filename from siFindSource
|
||||
-- Read the file or return an error
|
||||
siReadFile :: Maybe Bool -> String -> m (Either ErrorMessage String),
|
||||
-- | Given:
|
||||
-- the current script,
|
||||
-- what annotations say about including external files (if anything)
|
||||
-- a list of source-path annotations in effect,
|
||||
-- and a sourced file,
|
||||
-- find the sourced file
|
||||
siFindSource :: String -> Maybe Bool -> [String] -> String -> m FilePath,
|
||||
-- | Get the configuration file (name, contents) for a filename
|
||||
siGetConfig :: String -> m (Maybe (FilePath, String))
|
||||
}
|
||||
|
||||
-- ShellCheck input and output
|
||||
data CheckSpec = CheckSpec {
|
||||
csFilename :: String,
|
||||
csScript :: String,
|
||||
csCheckSourced :: Bool,
|
||||
csIgnoreRC :: Bool,
|
||||
csExcludedWarnings :: [Integer],
|
||||
csIncludedWarnings :: Maybe [Integer],
|
||||
csShellTypeOverride :: Maybe Shell,
|
||||
csMinSeverity :: Severity,
|
||||
csOptionalChecks :: [String]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
data CheckResult = CheckResult {
|
||||
crFilename :: String,
|
||||
crComments :: [PositionedComment]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
emptyCheckResult :: CheckResult
|
||||
emptyCheckResult = CheckResult {
|
||||
crFilename = "",
|
||||
crComments = []
|
||||
}
|
||||
|
||||
emptyCheckSpec :: CheckSpec
|
||||
emptyCheckSpec = CheckSpec {
|
||||
csFilename = "",
|
||||
csScript = "",
|
||||
csCheckSourced = False,
|
||||
csIgnoreRC = False,
|
||||
csExcludedWarnings = [],
|
||||
csIncludedWarnings = Nothing,
|
||||
csShellTypeOverride = Nothing,
|
||||
csMinSeverity = StyleC,
|
||||
csOptionalChecks = []
|
||||
}
|
||||
|
||||
newParseSpec :: ParseSpec
|
||||
newParseSpec = ParseSpec {
|
||||
psFilename = "",
|
||||
psScript = "",
|
||||
psCheckSourced = False,
|
||||
psIgnoreRC = False,
|
||||
psShellTypeOverride = Nothing
|
||||
}
|
||||
|
||||
-- Parser input and output
|
||||
data ParseSpec = ParseSpec {
|
||||
psFilename :: String,
|
||||
psScript :: String,
|
||||
psCheckSourced :: Bool,
|
||||
psIgnoreRC :: Bool,
|
||||
psShellTypeOverride :: Maybe Shell
|
||||
} deriving (Show, Eq)
|
||||
|
||||
data ParseResult = ParseResult {
|
||||
prComments :: [PositionedComment],
|
||||
prTokenPositions :: Map.Map Id (Position, Position),
|
||||
prRoot :: Maybe Token
|
||||
} deriving (Show, Eq)
|
||||
|
||||
newParseResult :: ParseResult
|
||||
newParseResult = ParseResult {
|
||||
prComments = [],
|
||||
prTokenPositions = Map.empty,
|
||||
prRoot = Nothing
|
||||
}
|
||||
|
||||
-- Analyzer input and output
|
||||
data AnalysisSpec = AnalysisSpec {
|
||||
asScript :: Token,
|
||||
asShellType :: Maybe Shell,
|
||||
asFallbackShell :: Maybe Shell,
|
||||
asExecutionMode :: ExecutionMode,
|
||||
asCheckSourced :: Bool,
|
||||
asOptionalChecks :: [String],
|
||||
asTokenPositions :: Map.Map Id (Position, Position)
|
||||
}
|
||||
|
||||
newAnalysisSpec token = AnalysisSpec {
|
||||
asScript = token,
|
||||
asShellType = Nothing,
|
||||
asFallbackShell = Nothing,
|
||||
asExecutionMode = Executed,
|
||||
asCheckSourced = False,
|
||||
asOptionalChecks = [],
|
||||
asTokenPositions = Map.empty
|
||||
}
|
||||
|
||||
newtype AnalysisResult = AnalysisResult {
|
||||
arComments :: [TokenComment]
|
||||
}
|
||||
|
||||
newAnalysisResult = AnalysisResult {
|
||||
arComments = []
|
||||
}
|
||||
|
||||
-- Formatter options
|
||||
data FormatterOptions = FormatterOptions {
|
||||
foColorOption :: ColorOption,
|
||||
foWikiLinkCount :: Integer
|
||||
}
|
||||
|
||||
newFormatterOptions = FormatterOptions {
|
||||
foColorOption = ColorAuto,
|
||||
foWikiLinkCount = 3
|
||||
}
|
||||
|
||||
data CheckDescription = CheckDescription {
|
||||
cdName :: String,
|
||||
cdDescription :: String,
|
||||
cdPositive :: String,
|
||||
cdNegative :: String
|
||||
}
|
||||
|
||||
newCheckDescription = CheckDescription {
|
||||
cdName = "",
|
||||
cdDescription = "",
|
||||
cdPositive = "",
|
||||
cdNegative = ""
|
||||
}
|
||||
|
||||
-- Supporting data types
|
||||
data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
|
||||
data ExecutionMode = Executed | Sourced deriving (Show, Eq)
|
||||
|
||||
type ErrorMessage = String
|
||||
type Code = Integer
|
||||
|
||||
data Severity = ErrorC | WarningC | InfoC | StyleC
|
||||
deriving (Show, Eq, Ord, Generic, NFData)
|
||||
data Position = Position {
|
||||
posFile :: String, -- Filename
|
||||
posLine :: Integer, -- 1 based source line
|
||||
posColumn :: Integer -- 1 based source column, where tabs are 8
|
||||
} deriving (Show, Eq, Generic, NFData, Ord)
|
||||
|
||||
newPosition :: Position
|
||||
newPosition = Position {
|
||||
posFile = "",
|
||||
posLine = 1,
|
||||
posColumn = 1
|
||||
}
|
||||
|
||||
data Comment = Comment {
|
||||
cSeverity :: Severity,
|
||||
cCode :: Code,
|
||||
cMessage :: String
|
||||
} deriving (Show, Eq, Generic, NFData)
|
||||
|
||||
newComment :: Comment
|
||||
newComment = Comment {
|
||||
cSeverity = StyleC,
|
||||
cCode = 0,
|
||||
cMessage = ""
|
||||
}
|
||||
|
||||
-- only support single line for now
|
||||
data Replacement = Replacement {
|
||||
repStartPos :: Position,
|
||||
repEndPos :: Position,
|
||||
repString :: String,
|
||||
-- Order in which the replacements should happen: highest precedence first.
|
||||
repPrecedence :: Int,
|
||||
-- Whether to insert immediately before or immediately after the specified region.
|
||||
repInsertionPoint :: InsertionPoint
|
||||
} deriving (Show, Eq, Generic, NFData)
|
||||
|
||||
data InsertionPoint = InsertBefore | InsertAfter
|
||||
deriving (Show, Eq, Generic, NFData)
|
||||
|
||||
newReplacement = Replacement {
|
||||
repStartPos = newPosition,
|
||||
repEndPos = newPosition,
|
||||
repString = "",
|
||||
repPrecedence = 1,
|
||||
repInsertionPoint = InsertAfter
|
||||
}
|
||||
|
||||
data Fix = Fix {
|
||||
fixReplacements :: [Replacement]
|
||||
} deriving (Show, Eq, Generic, NFData)
|
||||
|
||||
newFix = Fix {
|
||||
fixReplacements = []
|
||||
}
|
||||
|
||||
data PositionedComment = PositionedComment {
|
||||
pcStartPos :: Position,
|
||||
pcEndPos :: Position,
|
||||
pcComment :: Comment,
|
||||
pcFix :: Maybe Fix
|
||||
} deriving (Show, Eq, Generic, NFData)
|
||||
|
||||
newPositionedComment :: PositionedComment
|
||||
newPositionedComment = PositionedComment {
|
||||
pcStartPos = newPosition,
|
||||
pcEndPos = newPosition,
|
||||
pcComment = newComment,
|
||||
pcFix = Nothing
|
||||
}
|
||||
|
||||
data TokenComment = TokenComment {
|
||||
tcId :: Id,
|
||||
tcComment :: Comment,
|
||||
tcFix :: Maybe Fix
|
||||
} deriving (Show, Eq, Generic, NFData)
|
||||
|
||||
newTokenComment = TokenComment {
|
||||
tcId = Id 0,
|
||||
tcComment = newComment,
|
||||
tcFix = Nothing
|
||||
}
|
||||
|
||||
data ColorOption =
|
||||
ColorAuto
|
||||
| ColorAlways
|
||||
| ColorNever
|
||||
deriving (Ord, Eq, Show)
|
||||
|
||||
-- For testing
|
||||
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
|
||||
mockedSystemInterface files = SystemInterface {
|
||||
siReadFile = rf,
|
||||
siFindSource = fs,
|
||||
siGetConfig = const $ return Nothing
|
||||
}
|
||||
where
|
||||
rf _ file = return $
|
||||
case find ((== file) . fst) files of
|
||||
Nothing -> Left "File not included in mock."
|
||||
Just (_, contents) -> Right contents
|
||||
fs _ _ _ file = return file
|
||||
|
||||
mockRcFile rcfile mock = mock {
|
||||
siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
|
||||
}
|
||||
|
3574
src/ShellCheck/Parser.hs
Normal file
3574
src/ShellCheck/Parser.hs
Normal file
File diff suppressed because it is too large
Load Diff
80
src/ShellCheck/Regex.hs
Normal file
80
src/ShellCheck/Regex.hs
Normal file
@@ -0,0 +1,80 @@
|
||||
{-
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
|
||||
-- Basically Text.Regex based on regex-tdfa instead of the buggy regex-posix.
|
||||
module ShellCheck.Regex where
|
||||
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import Control.Monad
|
||||
import Text.Regex.TDFA
|
||||
|
||||
-- Precompile the regex
|
||||
mkRegex :: String -> Regex
|
||||
mkRegex str =
|
||||
let make :: String -> Regex
|
||||
make = makeRegex
|
||||
in
|
||||
make str
|
||||
|
||||
-- Does the regex match?
|
||||
matches :: String -> Regex -> Bool
|
||||
matches = flip match
|
||||
|
||||
-- Get all subgroups of the first match
|
||||
matchRegex :: Regex -> String -> Maybe [String]
|
||||
matchRegex re str = do
|
||||
(_, _, _, groups) <- matchM re str :: Maybe (String,String,String,[String])
|
||||
return groups
|
||||
|
||||
-- Get all full matches
|
||||
matchAllStrings :: Regex -> String -> [String]
|
||||
matchAllStrings re = unfoldr f
|
||||
where
|
||||
f :: String -> Maybe (String, String)
|
||||
f str = do
|
||||
(_, match, rest, _) <- matchM re str :: Maybe (String, String, String, [String])
|
||||
return (match, rest)
|
||||
|
||||
-- Get all subgroups from all matches
|
||||
matchAllSubgroups :: Regex -> String -> [[String]]
|
||||
matchAllSubgroups re = unfoldr f
|
||||
where
|
||||
f :: String -> Maybe ([String], String)
|
||||
f str = do
|
||||
(_, _, rest, groups) <- matchM re str :: Maybe (String, String, String, [String])
|
||||
return (groups, rest)
|
||||
|
||||
-- Replace regex in input with string
|
||||
subRegex :: Regex -> String -> String -> String
|
||||
subRegex re input replacement = f input
|
||||
where
|
||||
f str = fromMaybe str $ do
|
||||
(before, match, after) <- matchM re str :: Maybe (String, String, String)
|
||||
when (null match) $ error ("Internal error: substituted empty in " ++ str)
|
||||
return $ before ++ replacement ++ f after
|
||||
|
||||
-- Split a string based on a regex.
|
||||
splitOn :: String -> Regex -> [String]
|
||||
splitOn input re =
|
||||
case matchM re input :: Maybe (String, String, String) of
|
||||
Just (before, match, after) -> before : after `splitOn` re
|
||||
Nothing -> [input]
|
35
stack.yaml
Normal file
35
stack.yaml
Normal file
@@ -0,0 +1,35 @@
|
||||
# This file was automatically generated by stack init
|
||||
# For more information, see: https://docs.haskellstack.org/en/stable/yaml_configuration/
|
||||
|
||||
# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
|
||||
resolver: lts-18.15
|
||||
|
||||
# Local packages, usually specified by relative directory name
|
||||
packages:
|
||||
- '.'
|
||||
# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3)
|
||||
extra-deps: []
|
||||
|
||||
# Override default flag values for local packages and extra-deps
|
||||
flags: {}
|
||||
|
||||
# Extra package databases containing global packages
|
||||
extra-package-dbs: []
|
||||
|
||||
# Control whether we use the GHC we find on the path
|
||||
# system-ghc: true
|
||||
|
||||
# Require a specific version of stack, using version ranges
|
||||
# require-stack-version: -any # Default
|
||||
# require-stack-version: >= 1.0.0
|
||||
|
||||
# Override the architecture used by stack, especially useful on Windows
|
||||
# arch: i386
|
||||
# arch: x86_64
|
||||
|
||||
# Extra directories used by stack for building
|
||||
# extra-include-dirs: [/path/to/dir]
|
||||
# extra-lib-dirs: [/path/to/dir]
|
||||
|
||||
# Allow a newer minor version of GHC than the snapshot specifies
|
||||
# compiler-check: newer-minor
|
78
striptests
Executable file
78
striptests
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
# This file strips all unit tests from ShellCheck, removing
|
||||
# the dependency on QuickCheck and Template Haskell and
|
||||
# reduces the binary size considerably.
|
||||
set -o pipefail
|
||||
|
||||
sponge() {
|
||||
local data
|
||||
data="$(cat)"
|
||||
printf '%s\n' "$data" > "$1"
|
||||
}
|
||||
|
||||
modify() {
|
||||
if ! "${@:2}" < "$1" | sponge "$1"
|
||||
then
|
||||
{
|
||||
printf 'Failed to modify %s: ' "$1"
|
||||
printf '%q ' "${@:2}"
|
||||
printf '\n'
|
||||
} >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
detestify() {
|
||||
printf '%s\n' '-- AUTOGENERATED from ShellCheck by striptests. Do not modify.'
|
||||
awk '
|
||||
BEGIN {
|
||||
state = 0;
|
||||
}
|
||||
|
||||
/STRIP/ { next; }
|
||||
/LANGUAGE TemplateHaskell/ { next; }
|
||||
/^import.*Test\./ { next; }
|
||||
|
||||
/^module/ {
|
||||
sub(/,[^,)]*runTests/, "");
|
||||
}
|
||||
|
||||
# Delete tests
|
||||
/^prop_/ { state = 1; next; }
|
||||
|
||||
# ..and any blank lines following them.
|
||||
state == 1 && /^ / { next; }
|
||||
|
||||
# Template Haskell marker
|
||||
/^return / {
|
||||
exit;
|
||||
}
|
||||
|
||||
{ state = 0; print; }
|
||||
'
|
||||
}
|
||||
|
||||
|
||||
|
||||
if [[ ! -e 'ShellCheck.cabal' ]]
|
||||
then
|
||||
echo "Run me from the ShellCheck directory." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -d '.git' ]] && ! git diff --exit-code > /dev/null 2>&1
|
||||
then
|
||||
echo "You have local changes! These may be overwritten." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
modify 'ShellCheck.cabal' sed -e '
|
||||
/QuickCheck/d
|
||||
/^test-suite/{ s/.*//; q; }
|
||||
'
|
||||
|
||||
find . -name '.git' -prune -o -type f -name '*.hs' -print |
|
||||
while IFS= read -r file
|
||||
do
|
||||
modify "$file" detestify
|
||||
done
|
50
test/buildtest
Executable file
50
test/buildtest
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# This script configures, builds and runs tests.
|
||||
# It's meant for automatic cross-distro testing.
|
||||
|
||||
die() { echo "$*" >&2; exit 1; }
|
||||
|
||||
[ -e "ShellCheck.cabal" ] ||
|
||||
die "ShellCheck.cabal not in current dir"
|
||||
command -v cabal ||
|
||||
die "cabal is missing"
|
||||
|
||||
cabal update ||
|
||||
die "can't update"
|
||||
|
||||
if [ -e /etc/arch-release ]
|
||||
then
|
||||
# Arch has an unconventional packaging setup
|
||||
flags=(--disable-library-vanilla --enable-shared --enable-executable-dynamic --ghc-options=-dynamic)
|
||||
else
|
||||
flags=()
|
||||
fi
|
||||
|
||||
cabal install --dependencies-only --enable-tests "${flags[@]}" ||
|
||||
cabal install --dependencies-only "${flags[@]}" ||
|
||||
die "can't install dependencies"
|
||||
cabal configure --enable-tests "${flags[@]}" ||
|
||||
die "configure failed"
|
||||
cabal build ||
|
||||
die "build failed"
|
||||
cabal test ||
|
||||
die "test failed"
|
||||
cabal haddock ||
|
||||
die "haddock failed"
|
||||
|
||||
sc="$(find . -name shellcheck -type f -perm -111)"
|
||||
[ -x "$sc" ] || die "Can't find executable"
|
||||
|
||||
"$sc" - << 'EOF' || die "execution failed"
|
||||
#!/bin/sh
|
||||
echo "Hello World"
|
||||
EOF
|
||||
|
||||
"$sc" - << 'EOF' && die "negative execution failed"
|
||||
#!/bin/sh
|
||||
echo $1
|
||||
EOF
|
||||
|
||||
|
||||
echo "Success"
|
||||
exit 0
|
76
test/check_release
Executable file
76
test/check_release
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2257
|
||||
|
||||
failed=0
|
||||
fail() {
|
||||
echo "$(tput setaf 1)$*$(tput sgr0)"
|
||||
failed=1
|
||||
}
|
||||
|
||||
if git diff | grep -q ""
|
||||
then
|
||||
fail "There are uncommitted changes"
|
||||
fi
|
||||
|
||||
current=$(git tag --points-at)
|
||||
if [[ -z "$current" ]]
|
||||
then
|
||||
fail "No git tag on the current commit"
|
||||
echo "Create one with: git tag -a v0.0.0"
|
||||
fi
|
||||
|
||||
if [[ "$current" != v* ]]
|
||||
then
|
||||
fail "Bad tag format: expected v0.0.0"
|
||||
fi
|
||||
|
||||
if [[ "$(git cat-file -t "$current")" != "tag" ]]
|
||||
then
|
||||
fail "Current tag is not annotated (required for Snap)."
|
||||
fi
|
||||
|
||||
if [[ "$(git tag --points-at master)" != "$current" ]]
|
||||
then
|
||||
fail "You are not on master"
|
||||
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 "* ]]
|
||||
then
|
||||
fail "Expected git log message to be 'Stable version ...'"
|
||||
fi
|
||||
|
||||
i=1 j=1
|
||||
cat << EOF
|
||||
|
||||
Manual Checklist
|
||||
|
||||
$((i++)). Make sure none of the automated checks above failed
|
||||
$((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++)). Run test/distrotest to ensure that most distros can build OOTB.
|
||||
$((i++)). Format and read over the manual for bad formatting and outdated info.
|
||||
$((i++)). Make sure the Hackage package builds.
|
||||
|
||||
Release Steps
|
||||
|
||||
$((j++)). \`cabal sdist\` to generate a Hackage package
|
||||
$((j++)). \`git push --follow-tags\` to push commit
|
||||
$((j++)). Wait for GitHub Actions to build.
|
||||
$((j++)). Verify release:
|
||||
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
|
||||
$((j++)). If no disaster, upload to Hackage: http://hackage.haskell.org/upload
|
||||
$((j++)). Push a new commit that updates CHANGELOG.md
|
||||
EOF
|
||||
exit "$failed"
|
76
test/distrotest
Executable file
76
test/distrotest
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
# This script runs 'buildtest' on each of several distros
|
||||
# via Docker.
|
||||
set -o pipefail
|
||||
|
||||
exec 3>&1 4>&2
|
||||
die() { echo "$*" >&4; exit 1; }
|
||||
|
||||
[ -e "ShellCheck.cabal" ] || die "ShellCheck.cabal not in this dir"
|
||||
|
||||
[ "$1" = "--run" ] || {
|
||||
cat << EOF
|
||||
This script pulls multiple distros via Docker and compiles
|
||||
ShellCheck and dependencies for each one. It takes hours,
|
||||
and is still highly experimental.
|
||||
|
||||
Make sure you're plugged in and have screen/tmux in place,
|
||||
then re-run with $0 --run to continue.
|
||||
|
||||
Also note that dist* will be deleted.
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
echo "Deleting 'dist' and 'dist-newstyle'..."
|
||||
rm -rf dist dist-newstyle
|
||||
|
||||
log=$(mktemp) || die "Can't create temp file"
|
||||
date >> "$log" || die "Can't write to log"
|
||||
|
||||
echo "Logging to $log" >&3
|
||||
exec >> "$log" 2>&1
|
||||
|
||||
final=0
|
||||
while read -r distro setup
|
||||
do
|
||||
[[ "$distro" = "#"* || -z "$distro" ]] && continue
|
||||
printf '%s ' "$distro" >&3
|
||||
docker pull "$distro" || die "Can't pull $distro"
|
||||
printf 'pulled. ' >&3
|
||||
|
||||
tmp=$(mktemp -d) || die "Can't make temp dir"
|
||||
cp -r . "$tmp/" || die "Can't populate test dir"
|
||||
printf 'Result: ' >&3
|
||||
< /dev/null docker run -v "$tmp:/mnt" "$distro" sh -c "
|
||||
$setup
|
||||
cd /mnt || exit 1
|
||||
test/buildtest
|
||||
"
|
||||
ret=$?
|
||||
if [ "$ret" = 0 ]
|
||||
then
|
||||
echo "OK" >&3
|
||||
else
|
||||
echo "FAIL with $ret. See $log" >&3
|
||||
final=1
|
||||
fi
|
||||
rm -rf "$tmp"
|
||||
done << EOF
|
||||
# Docker tag Setup command
|
||||
debian:stable apt-get update && apt-get install -y cabal-install
|
||||
debian:testing apt-get update && apt-get install -y cabal-install
|
||||
ubuntu:latest apt-get update && apt-get install -y cabal-install
|
||||
haskell:latest true
|
||||
opensuse/leap:latest zypper install -y cabal-install ghc
|
||||
fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils
|
||||
archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
|
||||
|
||||
# Ubuntu LTS
|
||||
ubuntu:20.04 apt-get update && apt-get install -y cabal-install
|
||||
|
||||
# Stack on Ubuntu LTS
|
||||
ubuntu:20.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
|
||||
|
||||
exit "$final"
|
@@ -1,65 +0,0 @@
|
||||
#!/usr/bin/env runhaskell
|
||||
-- #!/usr/bin/env runhugs
|
||||
-- $Id: quickcheck,v 1.4 2003/01/08 15:09:22 shae Exp $
|
||||
-- This file defines a command
|
||||
-- quickCheck <options> <files>
|
||||
-- which invokes quickCheck on all properties defined in the files given as
|
||||
-- arguments, by generating an input script for hugs and then invoking it.
|
||||
-- quickCheck recognises the options
|
||||
-- +names print the name of each property before checking it
|
||||
-- -names do not print property names (the default)
|
||||
-- +verbose displays each test case before running
|
||||
-- -verbose do not displays each test case before running (the default)
|
||||
-- Other options (beginning with + or -) are passed unchanged to hugs.
|
||||
--
|
||||
-- Change the first line of this file to the location of runhaskell or runhugs
|
||||
-- on your system.
|
||||
-- Make the file executable.
|
||||
--
|
||||
-- TODO:
|
||||
-- someone on #haskell asked about supporting QC tests inside LaTeX, ex. \{begin} \{end}, how?
|
||||
|
||||
import System.Cmd
|
||||
import System.Directory (findExecutable)
|
||||
import System.Environment
|
||||
import Data.List
|
||||
import Data.Maybe (fromJust)
|
||||
|
||||
main :: IO ()
|
||||
main = do as<-getArgs
|
||||
sequence_ (map (process (filter isOption as))
|
||||
(filter (not.isOption) as))
|
||||
|
||||
-- ugly hack for .lhs files, is there a better way?
|
||||
unlit [] = []
|
||||
unlit x = if (head x) == '>' then (tail x) else x
|
||||
|
||||
process opts file =
|
||||
let (namesOpt,opts') = getOption "names" "-names" opts
|
||||
(verboseOpt,opts'') = getOption "verbose" "-verbose" opts' in
|
||||
do xs<-readFile file
|
||||
let names = nub$ filter (\x -> (("> prop_" `isPrefixOf` x) || ("prop_" `isPrefixOf` x)))
|
||||
(map (fst.head.lex.unlit) (lines xs))
|
||||
if null names then
|
||||
putStr (file++": no properties to check\n")
|
||||
else do writeFile "hugsin"$
|
||||
unlines ((":load "++file):
|
||||
":m +Test.QuickCheck":
|
||||
"let quackCheck p = quickCheckWith (stdArgs { maxSuccess = 1 }) p ":
|
||||
[(if namesOpt=="+names" then
|
||||
"putStr \""++p++": \" >> "
|
||||
else "") ++
|
||||
("quackCheck ")
|
||||
++ p | p<-names])
|
||||
-- To use ghci
|
||||
ghci <- findExecutable "ghci"
|
||||
system (fromJust ghci ++options opts''++" <hugsin")
|
||||
return ()
|
||||
|
||||
isOption xs = head xs `elem` "-+"
|
||||
|
||||
options opts = unwords ["\""++opt++"\"" | opt<-opts]
|
||||
|
||||
getOption name def opts =
|
||||
let opt = head [opt | opt<-opts++[def], isPrefixOf name (drop 1 opt)] in
|
||||
(opt, filter (/=opt) opts)
|
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Todo: Find a way to make this not suck.
|
||||
|
||||
ulimit -t 60 # Sometimes GHC ends in a spin loop, and this is easier than debugging
|
||||
|
||||
[[ -e test/quackCheck.hs ]] || { echo "Are you running me from the wrong directory?"; exit 1; }
|
||||
[[ $1 == -v ]] && pattern="" || pattern="FAIL"
|
||||
|
||||
find . -name '*.hs' -exec bash -c '
|
||||
grep -v "^module " "$1" > quack.tmp.hs
|
||||
./test/quackCheck.hs +names quack.tmp.hs
|
||||
' -- {} \; 2>&1 | grep -i "$pattern"
|
||||
result=$?
|
||||
rm -f quack.tmp.hs hugsin
|
||||
|
||||
if [[ $result == 0 ]]
|
||||
then
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
|
32
test/shellcheck.hs
Normal file
32
test/shellcheck.hs
Normal file
@@ -0,0 +1,32 @@
|
||||
module Main where
|
||||
|
||||
import Control.Monad
|
||||
import System.Exit
|
||||
import qualified ShellCheck.Analytics
|
||||
import qualified ShellCheck.AnalyzerLib
|
||||
import qualified ShellCheck.ASTLib
|
||||
import qualified ShellCheck.Checker
|
||||
import qualified ShellCheck.Checks.Commands
|
||||
import qualified ShellCheck.Checks.Custom
|
||||
import qualified ShellCheck.Checks.ShellSupport
|
||||
import qualified ShellCheck.Fixer
|
||||
import qualified ShellCheck.Formatter.Diff
|
||||
import qualified ShellCheck.Parser
|
||||
|
||||
main = do
|
||||
putStrLn "Running ShellCheck tests..."
|
||||
results <- sequence [
|
||||
ShellCheck.Analytics.runTests
|
||||
,ShellCheck.AnalyzerLib.runTests
|
||||
,ShellCheck.ASTLib.runTests
|
||||
,ShellCheck.Checker.runTests
|
||||
,ShellCheck.Checks.Commands.runTests
|
||||
,ShellCheck.Checks.Custom.runTests
|
||||
,ShellCheck.Checks.ShellSupport.runTests
|
||||
,ShellCheck.Fixer.runTests
|
||||
,ShellCheck.Formatter.Diff.runTests
|
||||
,ShellCheck.Parser.runTests
|
||||
]
|
||||
if and results
|
||||
then exitSuccess
|
||||
else exitFailure
|
28
test/stacktest
Executable file
28
test/stacktest
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# This script builds ShellCheck through `stack` using
|
||||
# various resolvers. It's run via distrotest.
|
||||
|
||||
resolvers=(
|
||||
nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
|
||||
)
|
||||
|
||||
die() { echo "$*" >&2; exit 1; }
|
||||
|
||||
[ -e "ShellCheck.cabal" ] ||
|
||||
die "ShellCheck.cabal not in current dir"
|
||||
[ -e "stack.yaml" ] ||
|
||||
die "stack.yaml not in current dir"
|
||||
command -v stack ||
|
||||
die "stack is missing"
|
||||
|
||||
stack setup || die "Failed to setup with default resolver"
|
||||
stack build --test || die "Failed to build/test with default resolver"
|
||||
|
||||
# Nice to haves, but not necessary
|
||||
for resolver in "${resolvers[@]}"
|
||||
do
|
||||
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! This probably doesn't matter."
|
||||
done
|
||||
|
||||
echo "Success"
|
Reference in New Issue
Block a user