mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 00:39:19 +08:00
Compare commits
310 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
ad1a0da954 | ||
|
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 |
20
.gitignore
vendored
20
.gitignore
vendored
@@ -1,7 +1,15 @@
|
|||||||
*.hi
|
# Created by http://www.gitignore.io
|
||||||
*.o
|
|
||||||
.tests
|
### Haskell ###
|
||||||
jsoncheck
|
|
||||||
shellcheck
|
|
||||||
shellcheck.1
|
|
||||||
dist
|
dist
|
||||||
|
cabal-dev
|
||||||
|
*.o
|
||||||
|
*.hi
|
||||||
|
*.chi
|
||||||
|
*.chs.h
|
||||||
|
.virtualenv
|
||||||
|
.hsenv
|
||||||
|
.cabal-sandbox/
|
||||||
|
cabal.sandbox.config
|
||||||
|
cabal.config
|
||||||
|
.stack-work
|
||||||
|
141
LICENSE
141
LICENSE
@@ -1,5 +1,5 @@
|
|||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 19 November 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
@@ -7,15 +7,17 @@
|
|||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU Affero General Public License is a free, copyleft license for
|
The GNU General Public License is a free, copyleft license for
|
||||||
software and other kinds of works, specifically designed to ensure
|
software and other kinds of works.
|
||||||
cooperation with the community in the case of network server software.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
our General Public Licenses are intended to guarantee your freedom to
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
share and change all versions of a program--to make sure it remains free
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users.
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
@@ -24,34 +26,44 @@ them if you wish), that you receive source code or can get it if you
|
|||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
To protect your rights, we need to prevent others from denying you
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
you this License which gives you legal permission to copy, distribute
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
and/or modify the software.
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
For example, if you distribute copies of such a program, whether
|
||||||
improvements made in alternate versions of the program, if they
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
receive widespread use, become available for other developers to
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
incorporate. Many developers of free software are heartened and
|
or can get the source code. And you must show them these terms so they
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
know their rights.
|
||||||
software used on network servers, this result may fail to come about.
|
|
||||||
The GNU General Public License permits making a modified version and
|
|
||||||
letting the public access it on a server without ever releasing its
|
|
||||||
source code to the public.
|
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
ensure that, in such cases, the modified source code becomes available
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
to the community. It requires the operator of a network server to
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
provide the source code of the modified version running there to the
|
|
||||||
users of that server. Therefore, public use of a modified version, on
|
|
||||||
a publicly accessible server, gives the public access to the source
|
|
||||||
code of the modified version.
|
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
that there is no warranty for this free software. For both users' and
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
changed, so that their problems will not be attributed erroneously to
|
||||||
this license.
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
@@ -60,7 +72,7 @@ modification follow.
|
|||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
@@ -537,45 +549,35 @@ to collect a royalty for further conveying from those to whom you convey
|
|||||||
the Program, the only way you could satisfy both those terms and this
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU General Public License into a single
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
License will continue to apply to the part which is the covered work,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the work with which it is combined will remain governed by version
|
but the special requirements of the GNU Affero General Public License,
|
||||||
3 of the GNU General Public License.
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU Affero General Public License from time to time. Such new versions
|
the GNU General Public License from time to time. Such new versions will
|
||||||
will be similar in spirit to the present version, but may differ in detail to
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU Affero General
|
Program specifies that a certain numbered version of the GNU General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU Affero General Public License, you may choose any version ever published
|
GNU General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU Affero General Public License can be used, that proxy's
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
@@ -633,29 +635,40 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU Affero General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
If the program does terminal interaction, make it output a short
|
||||||
network, you should also make sure that it provides a way for users to
|
notice like this when it starts in an interactive mode:
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
<program> Copyright (C) <year> <name of author>
|
||||||
of the code. There are many ways you could offer source, and different
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
solutions will be better for different programs; see section 13 for the
|
This is free software, and you are welcome to redistribute it
|
||||||
specific requirements.
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
<http://www.gnu.org/licenses/>.
|
<http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||||
|
24
Makefile
24
Makefile
@@ -1,24 +0,0 @@
|
|||||||
# TODO: Phase out Makefile in favor of Cabal
|
|
||||||
|
|
||||||
GHCFLAGS=-O9
|
|
||||||
|
|
||||||
all: shellcheck .tests shellcheck.1
|
|
||||||
: Done
|
|
||||||
|
|
||||||
shellcheck: regardless
|
|
||||||
: Conditionally compiling shellcheck
|
|
||||||
ghc $(GHCFLAGS) --make shellcheck
|
|
||||||
|
|
||||||
.tests: *.hs */*.hs
|
|
||||||
: Running unit tests
|
|
||||||
./test/runQuack && touch .tests
|
|
||||||
|
|
||||||
shellcheck.1: shellcheck.1.md
|
|
||||||
pandoc -s -t man $< -o $@
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -f .tests shellcheck shellcheck.1
|
|
||||||
rm -f *.hi *.o ShellCheck/*.hi ShellCheck/*.o
|
|
||||||
rm -rf dist
|
|
||||||
|
|
||||||
regardless:
|
|
310
README.md
310
README.md
@@ -1,68 +1,314 @@
|
|||||||
# ShellCheck - A shell script static analysis tool
|
# ShellCheck - A shell script static analysis tool
|
||||||
|
|
||||||
http://www.shellcheck.net
|
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts:
|
||||||
|
|
||||||
Copyright 2012-2014, Vidar 'koala_man' Holen
|
.
|
||||||
Licensed under the GNU Affero General Public License, v3
|
|
||||||
|
|
||||||
The goals of ShellCheck are:
|
The goals of ShellCheck are
|
||||||
|
|
||||||
- To point out and clarify typical beginner's syntax issues,
|
- To point out and clarify typical beginner's syntax issues
|
||||||
that causes a shell to give cryptic error messages.
|
that cause a shell to give cryptic error messages.
|
||||||
|
|
||||||
- To point out and clarify typical intermediate level semantic problems,
|
- To point out and clarify typical intermediate level semantic problems
|
||||||
that causes a shell to behave strangely and counter-intuitively.
|
that cause a shell to behave strangely and counter-intuitively.
|
||||||
|
|
||||||
- To point out subtle caveats, corner cases and pitfalls, that may cause an
|
- To point out subtle caveats, corner cases and pitfalls that may cause an
|
||||||
advanced user's otherwise working script to fail under future circumstances.
|
advanced user's otherwise working script to fail under future circumstances.
|
||||||
|
|
||||||
ShellCheck requires at least 1 GB of RAM to compile. Executables can be built with cabal. Tests currently still rely on a Makefile.
|
See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for examples of what ShellCheck can help you identify!
|
||||||
|
|
||||||
|
|
||||||
## Building with Cabal
|
## How to use
|
||||||
|
There are a variety of ways to use ShellCheck!
|
||||||
|
|
||||||
Make sure cabal is installed. On Debian based distros:
|
|
||||||
|
|
||||||
apt-get install cabal-install
|
#### On the web
|
||||||
|
Paste a shell script on http://www.shellcheck.net for instant feedback.
|
||||||
|
|
||||||
On Fedora:
|
[ShellCheck.net](http://www.shellcheck.net) is always synchronized to the latest git commit, and is the simplest way to give ShellCheck a go. Tell your friends!
|
||||||
|
|
||||||
yum install cabal-install
|
|
||||||
|
|
||||||
On Mac OS X with homebrew (http://brew.sh/):
|
#### From your terminal
|
||||||
|
Run `shellcheck yourscript` in your terminal for instant output, as seen above.
|
||||||
|
|
||||||
brew install cabal-install
|
|
||||||
|
|
||||||
On Mac OS X with MacPorts (http://www.macports.org/):
|
#### In your editor
|
||||||
|
|
||||||
port install hs-cabal-install
|
You can see ShellCheck suggestions directly in a variety of editors.
|
||||||
|
|
||||||
With cabal installed, cd to the shellcheck source directory and:
|
* Vim, through [Syntastic](https://github.com/scrooloose/syntastic):
|
||||||
|
|
||||||
|
.
|
||||||
|
|
||||||
|
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck):
|
||||||
|
|
||||||
|
.
|
||||||
|
|
||||||
|
* Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck).
|
||||||
|
|
||||||
|
* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
|
||||||
|
|
||||||
|
* Most other editors, through [GCC error compatibility](shellcheck.1.md#user-content-formats).
|
||||||
|
|
||||||
|
|
||||||
|
#### In your build or test suites
|
||||||
|
While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites.
|
||||||
|
|
||||||
|
Use ShellCheck's exit code, or its [CheckStyle compatible XML output](shellcheck.1.md#user-content-formats). There's also a simple JSON output format for easy integration.
|
||||||
|
|
||||||
|
|
||||||
|
## Installing
|
||||||
|
|
||||||
|
The easiest way to install ShellCheck locally is through your package manager.
|
||||||
|
|
||||||
|
On systems with Cabal (installs to `~/.cabal/bin`):
|
||||||
|
|
||||||
|
cabal update
|
||||||
|
cabal install shellcheck
|
||||||
|
|
||||||
|
On Debian based distros:
|
||||||
|
|
||||||
|
apt-get install shellcheck
|
||||||
|
|
||||||
|
On Fedora based distros:
|
||||||
|
|
||||||
|
dnf install ShellCheck
|
||||||
|
|
||||||
|
On OS X with homebrew:
|
||||||
|
|
||||||
|
brew install shellcheck
|
||||||
|
|
||||||
|
On OS X with MacPorts:
|
||||||
|
|
||||||
|
port install shellcheck
|
||||||
|
|
||||||
|
On openSUSE:Tumbleweed:
|
||||||
|
|
||||||
|
zypper in ShellCheck
|
||||||
|
|
||||||
|
On other openSUSE distributions:
|
||||||
|
|
||||||
|
add OBS devel:languages:haskell repository from https://build.opensuse.org/project/repositories/devel:languages:haskell
|
||||||
|
|
||||||
|
zypper ar http://download.opensuse.org/repositories/devel:/languages:/haskell/openSUSE_$(version)/devel:languages:haskell.repo
|
||||||
|
zypper in ShellCheck
|
||||||
|
|
||||||
|
or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
|
||||||
|
|
||||||
|
|
||||||
|
## 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`, `yum`, `zypper` or `brew`).
|
||||||
|
|
||||||
|
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
|
$ cabal install
|
||||||
...
|
|
||||||
|
This will compile ShellCheck and install it to your `~/.cabal/bin` directory.
|
||||||
|
|
||||||
|
Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
|
||||||
|
|
||||||
|
export PATH="$HOME/.cabal/bin:$PATH"
|
||||||
|
|
||||||
|
Log out and in again, and verify that your PATH is set up correctly:
|
||||||
|
|
||||||
$ which shellcheck
|
$ which shellcheck
|
||||||
~/.cabal/bin/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`:
|
||||||
|
|
||||||
## Building with Make
|
> chcp 65001
|
||||||
|
Active code page: 65001
|
||||||
|
|
||||||
ShellCheck is written in Haskell, and requires GHC, Parsec3, JSON and
|
In Powershell ISE, you may need to additionally update the output encoding:
|
||||||
Text.Regex. To run the unit tests, it also requires QuickCheck2.
|
|
||||||
|
|
||||||
On Fedora, these can be installed with:
|
> [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
yum install ghc ghc-parsec-devel ghc-QuickCheck-devel \
|
#### Running tests
|
||||||
ghc-json-devel ghc-regex-compat-devel
|
|
||||||
|
|
||||||
On Ubuntu and similar, use:
|
To run the unit test suite:
|
||||||
|
|
||||||
apt-get install ghc libghc-parsec3-dev libghc-json-dev \
|
$ cabal test
|
||||||
libghc-regex-compat-dev libghc-quickcheck2-dev
|
|
||||||
|
|
||||||
To build and run the tests, cd to the shellcheck source directory and:
|
|
||||||
|
|
||||||
$ make
|
## Gallery of bad code
|
||||||
|
So what kind of things does ShellCheck look for? Here is an incomplete list of detected issues.
|
||||||
|
|
||||||
|
#### Quoting
|
||||||
|
|
||||||
|
ShellCheck can recognize several types of incorrect quoting:
|
||||||
|
|
||||||
|
echo $1 # Unquoted variables
|
||||||
|
find . -name *.ogg # Unquoted find/grep patterns
|
||||||
|
rm "~/my file.txt" # Quoted tilde expansion
|
||||||
|
v='--verbose="true"'; cmd $v # Literal quotes in variables
|
||||||
|
for f in "*.ogg" # Incorrectly quoted 'for' loops
|
||||||
|
touch $@ # Unquoted $@
|
||||||
|
echo 'Don't forget to restart!' # Singlequote closed by apostrophe
|
||||||
|
echo 'Don\'t try this at home' # Attempting to escape ' in ''
|
||||||
|
echo 'Path is $PATH' # Variables in single quotes
|
||||||
|
trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap
|
||||||
|
|
||||||
|
|
||||||
|
#### Conditionals
|
||||||
|
|
||||||
|
ShellCheck can recognize many types of incorrect test statements.
|
||||||
|
|
||||||
|
[[ n != 0 ]] # Constant test expressions
|
||||||
|
[[ -e *.mpg ]] # Existence checks of globs
|
||||||
|
[[ $foo==0 ]] # Always true due to missing spaces
|
||||||
|
[[ -n "$foo " ]] # Always true due to literals
|
||||||
|
[[ $foo =~ "fo+" ]] # Quoted regex in =~
|
||||||
|
[ foo =~ re ] # Unsupported [ ] operators
|
||||||
|
[ $1 -eq "shellcheck" ] # Numerical comparison of strings
|
||||||
|
[ $n && $m ] # && in [ .. ]
|
||||||
|
[ grep -q foo file ] # Command without $(..)
|
||||||
|
|
||||||
|
|
||||||
|
#### Frequently misused commands
|
||||||
|
|
||||||
|
ShellCheck can recognize instances where commands are used incorrectly:
|
||||||
|
|
||||||
|
grep '*foo*' file # Globs in regex contexts
|
||||||
|
find . -exec foo {} && bar {} \; # Prematurely terminated find -exec
|
||||||
|
sudo echo 'Var=42' > /etc/profile # Redirecting sudo
|
||||||
|
time --format=%s sleep 10 # Passing time(1) flags to time builtin
|
||||||
|
while read h; do ssh "$h" uptime # Commands eating while loop input
|
||||||
|
alias archive='mv $1 /backup' # Defining aliases with arguments
|
||||||
|
tr -cd '[a-zA-Z0-9]' # [] around ranges in tr
|
||||||
|
exec foo; echo "Done!" # Misused 'exec'
|
||||||
|
find -name \*.bak -o -name \*~ -delete # Implicit precedence in find
|
||||||
|
f() { whoami; }; sudo f # External use of internal functions
|
||||||
|
|
||||||
|
|
||||||
|
#### Common beginner's mistakes
|
||||||
|
|
||||||
|
ShellCheck recognizes many common beginner's syntax errors:
|
||||||
|
|
||||||
|
var = 42 # Spaces around = in assignments
|
||||||
|
$foo=42 # $ in assignments
|
||||||
|
for $var in *; do ... # $ in for loop variables
|
||||||
|
var$n="Hello" # Wrong indirect assignment
|
||||||
|
echo ${var$n} # Wrong indirect reference
|
||||||
|
var=(1, 2, 3) # Comma separated arrays
|
||||||
|
echo "Argument 10 is $10" # Positional parameter misreference
|
||||||
|
if $(myfunction); then ..; fi # Wrapping commands in $()
|
||||||
|
else if othercondition; then .. # Using 'else if'
|
||||||
|
|
||||||
|
|
||||||
|
#### Style
|
||||||
|
|
||||||
|
ShellCheck can make suggestions to improve style:
|
||||||
|
|
||||||
|
[[ -z $(find /tmp | grep mpg) ]] # Use grep -q instead
|
||||||
|
a >> log; b >> log; c >> log # Use a redirection block instead
|
||||||
|
echo "The time is `date`" # Use $() instead
|
||||||
|
cd dir; process *; cd ..; # Use subshells instead
|
||||||
|
echo $[1+2] # Use standard $((..)) instead of old $[]
|
||||||
|
echo $(($RANDOM % 6)) # Don't use $ on variables in $((..))
|
||||||
|
echo "$(date)" # Useless use of echo
|
||||||
|
cat file | grep foo # Useless use of cat
|
||||||
|
|
||||||
|
|
||||||
|
#### Data and typing errors
|
||||||
|
|
||||||
|
ShellCheck can recognize issues related to data and typing:
|
||||||
|
|
||||||
|
args="$@" # Assigning arrays to strings
|
||||||
|
files=(foo bar); echo "$files" # Referencing arrays as strings
|
||||||
|
printf "%s\n" "Arguments: $@." # Concatenating strings and arrays.
|
||||||
|
[[ $# > 2 ]] # Comparing numbers as strings
|
||||||
|
var=World; echo "Hello " var # Unused lowercase variables
|
||||||
|
echo "Hello $name" # Unassigned lowercase variables
|
||||||
|
cmd | read bar; echo $bar # Assignments in subshells
|
||||||
|
|
||||||
|
|
||||||
|
#### Robustness
|
||||||
|
|
||||||
|
ShellCheck can make suggestions for improving the robustness of a script:
|
||||||
|
|
||||||
|
rm -rf "$STEAMROOT/"* # Catastrophic rm
|
||||||
|
touch ./-l; ls * # Globs that could become options
|
||||||
|
find . -exec sh -c 'a && b {}' \; # Find -exec shell injection
|
||||||
|
printf "Hello $name" # Variables in printf format
|
||||||
|
for f in $(ls *.txt); do # Iterating over ls output
|
||||||
|
export MYVAR=$(cmd) # Masked exit codes
|
||||||
|
|
||||||
|
|
||||||
|
#### Portability
|
||||||
|
|
||||||
|
ShellCheck will warn when using features not supported by the shebang. For example, if you set the shebang to `#!/bin/sh`, ShellCheck will warn about portability issues similar to `checkbashisms`:
|
||||||
|
|
||||||
|
|
||||||
|
echo {1..$n} # Works in ksh, but not bash/dash/sh
|
||||||
|
echo {1..10} # Works in ksh and bash, but not dash/sh
|
||||||
|
echo -n 42 # Works in ksh, bash and dash, undefined in sh
|
||||||
|
trap 'exit 42' sigint # Unportable signal spec
|
||||||
|
cmd &> file # Unportable redirection operator
|
||||||
|
read foo < /dev/tcp/host/22 # Unportable intercepted files
|
||||||
|
foo-bar() { ..; } # Undefined/unsupported function name
|
||||||
|
[ $UID = 0 ] # Variable undefined in dash/sh
|
||||||
|
local var=value # local is undefined in sh
|
||||||
|
|
||||||
|
|
||||||
|
#### Miscellaneous
|
||||||
|
|
||||||
|
ShellCheck recognizes a menagerie of other issues:
|
||||||
|
|
||||||
|
PS1='\e[0;32m\$\e[0m ' # PS1 colors not in \[..\]
|
||||||
|
PATH="$PATH:~/bin" # Literal tilde in $PATH
|
||||||
|
rm “file” # Unicode quotes
|
||||||
|
echo "Hello world" # Carriage return / DOS line endings
|
||||||
|
var=42 echo $var # Expansion of inlined environment
|
||||||
|
#!/bin/bash -x -e # Common shebang errors
|
||||||
|
echo $((n/180*100)) # Unnecessary loss of precision
|
||||||
|
ls *[:digit:].txt # Bad character class globs
|
||||||
|
sed 's/foo/bar/' file > file # Redirecting to input
|
||||||
|
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
||||||
|
|
||||||
|
## Reporting bugs
|
||||||
|
|
||||||
|
Please use the GitHub issue tracker for any bugs or feature suggestions:
|
||||||
|
|
||||||
|
https://github.com/koalaman/shellcheck/issues
|
||||||
|
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Please submit patches to code or documentation as GitHub pull requests!
|
||||||
|
|
||||||
|
Contributions must be licensed under the GNU GPLv3.
|
||||||
|
The contributor retains the copyright.
|
||||||
|
|
||||||
|
|
||||||
|
## Copyright
|
||||||
|
|
||||||
|
ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE).
|
||||||
|
|
||||||
|
Copyright 2012-2015, Vidar 'koala_man' Holen and contributors.
|
||||||
|
|
||||||
Happy ShellChecking!
|
Happy ShellChecking!
|
||||||
|
38
Setup.hs
38
Setup.hs
@@ -1,2 +1,36 @@
|
|||||||
import Distribution.Simple
|
import Distribution.PackageDescription (
|
||||||
main = defaultMain
|
HookedBuildInfo,
|
||||||
|
emptyHookedBuildInfo )
|
||||||
|
import Distribution.Simple (
|
||||||
|
Args,
|
||||||
|
UserHooks ( preSDist ),
|
||||||
|
defaultMainWithHooks,
|
||||||
|
simpleUserHooks )
|
||||||
|
import Distribution.Simple.Setup ( SDistFlags )
|
||||||
|
|
||||||
|
import System.Process ( system )
|
||||||
|
|
||||||
|
|
||||||
|
main = defaultMainWithHooks myHooks
|
||||||
|
where
|
||||||
|
myHooks = simpleUserHooks { preSDist = myPreSDist }
|
||||||
|
|
||||||
|
-- | This hook will be executed before e.g. @cabal sdist@. It runs
|
||||||
|
-- pandoc to create the man page from shellcheck.1.md. If the pandoc
|
||||||
|
-- command is not found, this will fail with an error message:
|
||||||
|
--
|
||||||
|
-- /bin/sh: pandoc: command not found
|
||||||
|
--
|
||||||
|
-- Since the man page is listed in the Extra-Source-Files section of
|
||||||
|
-- our cabal file, a failure here should result in a failure to
|
||||||
|
-- create the distribution tarball (that's a good thing).
|
||||||
|
--
|
||||||
|
myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo
|
||||||
|
myPreSDist _ _ = do
|
||||||
|
putStrLn "Building the man page (shellcheck.1) with pandoc..."
|
||||||
|
putStrLn pandoc_cmd
|
||||||
|
result <- system pandoc_cmd
|
||||||
|
putStrLn $ "pandoc exited with " ++ show result
|
||||||
|
return emptyHookedBuildInfo
|
||||||
|
where
|
||||||
|
pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"
|
||||||
|
@@ -1,14 +1,13 @@
|
|||||||
Name: ShellCheck
|
Name: ShellCheck
|
||||||
-- Must also be updated in ShellCheck/Data.hs :
|
Version: 0.4.4
|
||||||
Version: 0.3.2
|
|
||||||
Synopsis: Shell script analysis tool
|
Synopsis: Shell script analysis tool
|
||||||
License: OtherLicense
|
License: GPL-3
|
||||||
License-file: LICENSE
|
License-file: LICENSE
|
||||||
Category: Static Analysis
|
Category: Static Analysis
|
||||||
Author: Vidar Holen
|
Author: Vidar Holen
|
||||||
Maintainer: vidar@vidarholen.net
|
Maintainer: vidar@vidarholen.net
|
||||||
Homepage: http://www.shellcheck.net/
|
Homepage: http://www.shellcheck.net/
|
||||||
Build-Type: Simple
|
Build-Type: Custom
|
||||||
Cabal-Version: >= 1.8
|
Cabal-Version: >= 1.8
|
||||||
Bug-reports: https://github.com/koalaman/shellcheck/issues
|
Bug-reports: https://github.com/koalaman/shellcheck/issues
|
||||||
Description:
|
Description:
|
||||||
@@ -23,6 +22,15 @@ Description:
|
|||||||
* To point out subtle caveats, corner cases and pitfalls, that may cause an
|
* To point out subtle caveats, corner cases and pitfalls, that may cause an
|
||||||
advanced user's otherwise working script to fail under future circumstances.
|
advanced user's otherwise working script to fail under future circumstances.
|
||||||
|
|
||||||
|
Extra-Source-Files:
|
||||||
|
-- documentation
|
||||||
|
README.md
|
||||||
|
shellcheck.1.md
|
||||||
|
-- built with a cabal sdist hook
|
||||||
|
shellcheck.1
|
||||||
|
-- tests
|
||||||
|
test/shellcheck.hs
|
||||||
|
|
||||||
source-repository head
|
source-repository head
|
||||||
type: git
|
type: git
|
||||||
location: git://github.com/koalaman/shellcheck.git
|
location: git://github.com/koalaman/shellcheck.git
|
||||||
@@ -33,15 +41,31 @@ library
|
|||||||
containers,
|
containers,
|
||||||
directory,
|
directory,
|
||||||
json,
|
json,
|
||||||
mtl,
|
mtl >= 2.2.1,
|
||||||
parsec,
|
parsec,
|
||||||
regex-compat
|
regex-tdfa,
|
||||||
|
QuickCheck >= 2.7.4,
|
||||||
|
-- When cabal supports it, move this to setup-depends:
|
||||||
|
process
|
||||||
exposed-modules:
|
exposed-modules:
|
||||||
ShellCheck.Analytics
|
|
||||||
ShellCheck.AST
|
ShellCheck.AST
|
||||||
|
ShellCheck.ASTLib
|
||||||
|
ShellCheck.Analytics
|
||||||
|
ShellCheck.Analyzer
|
||||||
|
ShellCheck.AnalyzerLib
|
||||||
|
ShellCheck.Checker
|
||||||
|
ShellCheck.Checks.Commands
|
||||||
ShellCheck.Data
|
ShellCheck.Data
|
||||||
|
ShellCheck.Formatter.Format
|
||||||
|
ShellCheck.Formatter.CheckStyle
|
||||||
|
ShellCheck.Formatter.GCC
|
||||||
|
ShellCheck.Formatter.JSON
|
||||||
|
ShellCheck.Formatter.TTY
|
||||||
|
ShellCheck.Interface
|
||||||
ShellCheck.Parser
|
ShellCheck.Parser
|
||||||
ShellCheck.Simple
|
ShellCheck.Regex
|
||||||
|
other-modules:
|
||||||
|
Paths_ShellCheck
|
||||||
|
|
||||||
executable shellcheck
|
executable shellcheck
|
||||||
build-depends:
|
build-depends:
|
||||||
@@ -50,7 +74,23 @@ executable shellcheck
|
|||||||
containers,
|
containers,
|
||||||
directory,
|
directory,
|
||||||
json,
|
json,
|
||||||
mtl,
|
mtl >= 2.2.1,
|
||||||
parsec,
|
parsec,
|
||||||
regex-compat
|
regex-tdfa,
|
||||||
|
QuickCheck >= 2.7.4
|
||||||
main-is: shellcheck.hs
|
main-is: shellcheck.hs
|
||||||
|
|
||||||
|
test-suite test-shellcheck
|
||||||
|
type: exitcode-stdio-1.0
|
||||||
|
build-depends:
|
||||||
|
ShellCheck,
|
||||||
|
base >= 4 && < 5,
|
||||||
|
containers,
|
||||||
|
directory,
|
||||||
|
json,
|
||||||
|
mtl >= 2.2.1,
|
||||||
|
parsec,
|
||||||
|
regex-tdfa,
|
||||||
|
QuickCheck >= 2.7.4
|
||||||
|
main-is: test/shellcheck.hs
|
||||||
|
|
||||||
|
@@ -1,25 +1,27 @@
|
|||||||
{-
|
{-
|
||||||
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
http://www.vidarholen.net/contents/shellcheck
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
ShellCheck is distributed in the hope that it will be useful,
|
ShellCheck is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU Affero General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.AST where
|
module ShellCheck.AST where
|
||||||
|
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Control.Monad.Identity
|
import Control.Monad.Identity
|
||||||
import qualified Text.Regex as Re
|
import qualified ShellCheck.Regex as Re
|
||||||
|
|
||||||
data Id = Id Int deriving (Show, Eq, Ord)
|
data Id = Id Int deriving (Show, Eq, Ord)
|
||||||
|
|
||||||
@@ -28,16 +30,16 @@ data Dashed = Dashed | Undashed deriving (Show, Eq)
|
|||||||
data AssignmentMode = Assign | Append deriving (Show, Eq)
|
data AssignmentMode = Assign | Append deriving (Show, Eq)
|
||||||
data FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
|
data FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
|
||||||
data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
|
data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
|
||||||
|
data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
|
||||||
|
|
||||||
data Token =
|
data Token =
|
||||||
TA_Base Id String Token
|
TA_Binary Id String Token Token
|
||||||
| TA_Binary Id String Token Token
|
| TA_Assignment Id String Token Token
|
||||||
| TA_Expansion Id Token
|
| TA_Expansion Id [Token]
|
||||||
| TA_Literal Id String
|
| TA_Index Id Token
|
||||||
| TA_Sequence Id [Token]
|
| TA_Sequence Id [Token]
|
||||||
| TA_Trinary Id Token Token Token
|
| TA_Trinary Id Token Token Token
|
||||||
| TA_Unary Id String Token
|
| TA_Unary Id String Token
|
||||||
| TA_Variable Id String
|
|
||||||
| TC_And Id ConditionType String Token Token
|
| TC_And Id ConditionType String Token Token
|
||||||
| TC_Binary Id ConditionType String Token Token
|
| TC_Binary Id ConditionType String Token Token
|
||||||
| TC_Group Id ConditionType Token
|
| TC_Group Id ConditionType Token
|
||||||
@@ -48,16 +50,17 @@ data Token =
|
|||||||
| T_AndIf Id (Token) (Token)
|
| T_AndIf Id (Token) (Token)
|
||||||
| T_Arithmetic Id Token
|
| T_Arithmetic Id Token
|
||||||
| T_Array Id [Token]
|
| T_Array Id [Token]
|
||||||
|
| T_IndexedElement Id Token Token
|
||||||
| T_Assignment Id AssignmentMode String (Maybe Token) Token
|
| T_Assignment Id AssignmentMode String (Maybe Token) Token
|
||||||
| T_Backgrounded Id Token
|
| T_Backgrounded Id Token
|
||||||
| T_Backticked Id [Token]
|
| T_Backticked Id [Token]
|
||||||
| T_Bang Id
|
| T_Bang Id
|
||||||
| T_Banged Id Token
|
| T_Banged Id Token
|
||||||
| T_BraceExpansion Id String
|
| T_BraceExpansion Id [Token]
|
||||||
| T_BraceGroup Id [Token]
|
| T_BraceGroup Id [Token]
|
||||||
| T_CLOBBER Id
|
| T_CLOBBER Id
|
||||||
| T_Case Id
|
| T_Case Id
|
||||||
| T_CaseExpression Id Token [([Token],[Token])]
|
| T_CaseExpression Id Token [(CaseType, [Token], [Token])]
|
||||||
| T_Condition Id ConditionType Token
|
| T_Condition Id ConditionType Token
|
||||||
| T_DGREAT Id
|
| T_DGREAT Id
|
||||||
| T_DLESS Id
|
| T_DLESS Id
|
||||||
@@ -70,6 +73,7 @@ data Token =
|
|||||||
| T_DollarDoubleQuoted Id [Token]
|
| T_DollarDoubleQuoted Id [Token]
|
||||||
| T_DollarExpansion Id [Token]
|
| T_DollarExpansion Id [Token]
|
||||||
| T_DollarSingleQuoted Id String
|
| T_DollarSingleQuoted Id String
|
||||||
|
| T_DollarBraceCommandExpansion Id [Token]
|
||||||
| T_Done Id
|
| T_Done Id
|
||||||
| T_DoubleQuoted Id [Token]
|
| T_DoubleQuoted Id [Token]
|
||||||
| T_EOF Id
|
| T_EOF Id
|
||||||
@@ -121,16 +125,26 @@ data Token =
|
|||||||
| T_WhileExpression Id [Token] [Token]
|
| T_WhileExpression Id [Token] [Token]
|
||||||
| T_Annotation Id [Annotation] Token
|
| T_Annotation Id [Annotation] Token
|
||||||
| T_Pipe Id String
|
| T_Pipe Id String
|
||||||
|
| T_CoProc Id (Maybe String) Token
|
||||||
|
| T_CoProcBody Id Token
|
||||||
|
| T_Include Id Token Token -- . & source: SimpleCommand T_Script
|
||||||
|
| T_BatsTest Id Token Token
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
data Annotation = DisableComment Integer deriving (Show, Eq)
|
data Annotation =
|
||||||
|
DisableComment Integer
|
||||||
|
| SourceOverride String
|
||||||
|
| ShellOverride String
|
||||||
|
deriving (Show, Eq)
|
||||||
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
|
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
|
||||||
|
|
||||||
-- I apologize for nothing!
|
-- This is an abomination.
|
||||||
lolHax s = Re.subRegex (Re.mkRegex "(Id [0-9]+)") (show s) "(Id 0)"
|
tokenEquals :: Token -> Token -> Bool
|
||||||
instance Eq Token where
|
tokenEquals a b = kludge a == kludge b
|
||||||
(==) a b = lolHax a == lolHax b
|
where kludge s = Re.subRegex (Re.mkRegex "\\(Id [0-9]+\\)") (show s) "(Id 0)"
|
||||||
|
|
||||||
|
instance Eq Token where
|
||||||
|
(==) = tokenEquals
|
||||||
|
|
||||||
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> Token) -> Token -> m Token
|
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> Token) -> Token -> m Token
|
||||||
analyze f g i =
|
analyze f g i =
|
||||||
@@ -154,7 +168,7 @@ analyze f g i =
|
|||||||
dll l m v = do
|
dll l m v = do
|
||||||
x <- roundAll l
|
x <- roundAll l
|
||||||
y <- roundAll m
|
y <- roundAll m
|
||||||
return $ v x m
|
return $ v x y
|
||||||
d1 t v = do
|
d1 t v = do
|
||||||
x <- round t
|
x <- round t
|
||||||
return $ v x
|
return $ v x
|
||||||
@@ -167,6 +181,8 @@ analyze f g i =
|
|||||||
delve (T_DoubleQuoted id list) = dl list $ T_DoubleQuoted id
|
delve (T_DoubleQuoted id list) = dl list $ T_DoubleQuoted id
|
||||||
delve (T_DollarDoubleQuoted id list) = dl list $ T_DollarDoubleQuoted id
|
delve (T_DollarDoubleQuoted id list) = dl list $ T_DollarDoubleQuoted id
|
||||||
delve (T_DollarExpansion id list) = dl list $ T_DollarExpansion id
|
delve (T_DollarExpansion id list) = dl list $ T_DollarExpansion id
|
||||||
|
delve (T_DollarBraceCommandExpansion id list) = dl list $ T_DollarBraceCommandExpansion id
|
||||||
|
delve (T_BraceExpansion id list) = dl list $ T_BraceExpansion id
|
||||||
delve (T_Backticked id list) = dl list $ T_Backticked id
|
delve (T_Backticked id list) = dl list $ T_Backticked id
|
||||||
delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id
|
delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id
|
||||||
delve (T_DollarBracket id c) = d1 c $ T_DollarBracket id
|
delve (T_DollarBracket id c) = d1 c $ T_DollarBracket id
|
||||||
@@ -178,6 +194,7 @@ analyze f g i =
|
|||||||
b <- round value
|
b <- round value
|
||||||
return $ T_Assignment id mode var a b
|
return $ T_Assignment id mode var a b
|
||||||
delve (T_Array id t) = dl t $ T_Array id
|
delve (T_Array id t) = dl t $ T_Array id
|
||||||
|
delve (T_IndexedElement id t1 t2) = d2 t1 t2 $ T_IndexedElement id
|
||||||
delve (T_Redirecting id redirs cmd) = do
|
delve (T_Redirecting id redirs cmd) = do
|
||||||
a <- roundAll redirs
|
a <- roundAll redirs
|
||||||
b <- round cmd
|
b <- round cmd
|
||||||
@@ -206,10 +223,10 @@ analyze f g i =
|
|||||||
delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v
|
delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v
|
||||||
delve (T_CaseExpression id word cases) = do
|
delve (T_CaseExpression id word cases) = do
|
||||||
newWord <- round word
|
newWord <- round word
|
||||||
newCases <- mapM (\(c, t) -> do
|
newCases <- mapM (\(o, c, t) -> do
|
||||||
x <- mapM round c
|
x <- mapM round c
|
||||||
y <- mapM round t
|
y <- mapM round t
|
||||||
return (x,y)
|
return (o, x,y)
|
||||||
) cases
|
) cases
|
||||||
return $ T_CaseExpression id newWord newCases
|
return $ T_CaseExpression id newWord newCases
|
||||||
|
|
||||||
@@ -235,6 +252,7 @@ analyze f g i =
|
|||||||
delve (TC_Noary id typ token) = d1 token $ TC_Noary id typ
|
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_Binary id op t1 t2) = d2 t1 t2 $ TA_Binary id op
|
||||||
|
delve (TA_Assignment id op t1 t2) = d2 t1 t2 $ TA_Assignment id op
|
||||||
delve (TA_Unary id op t1) = d1 t1 $ TA_Unary id op
|
delve (TA_Unary id op t1) = d1 t1 $ TA_Unary id op
|
||||||
delve (TA_Sequence id l) = dl l $ TA_Sequence id
|
delve (TA_Sequence id l) = dl l $ TA_Sequence id
|
||||||
delve (TA_Trinary id t1 t2 t3) = do
|
delve (TA_Trinary id t1 t2 t3) = do
|
||||||
@@ -242,9 +260,13 @@ analyze f g i =
|
|||||||
b <- round t2
|
b <- round t2
|
||||||
c <- round t3
|
c <- round t3
|
||||||
return $ TA_Trinary id a b c
|
return $ TA_Trinary id a b c
|
||||||
delve (TA_Expansion id t) = d1 t $ TA_Expansion id
|
delve (TA_Expansion id t) = dl t $ TA_Expansion id
|
||||||
delve (TA_Base id b t) = d1 t $ TA_Base id b
|
delve (TA_Index id t) = d1 t $ TA_Index id
|
||||||
delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns
|
delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns
|
||||||
|
delve (T_CoProc id var body) = d1 body $ T_CoProc id var
|
||||||
|
delve (T_CoProcBody id t) = d1 t $ T_CoProcBody id
|
||||||
|
delve (T_Include id includer script) = d2 includer script $ T_Include id
|
||||||
|
delve (T_BatsTest id name t) = d2 name t $ T_BatsTest id
|
||||||
delve t = return t
|
delve t = return t
|
||||||
|
|
||||||
getId t = case t of
|
getId t = case t of
|
||||||
@@ -290,12 +312,14 @@ getId t = case t of
|
|||||||
T_DollarBraced id _ -> id
|
T_DollarBraced id _ -> id
|
||||||
T_DollarArithmetic id _ -> id
|
T_DollarArithmetic id _ -> id
|
||||||
T_BraceExpansion id _ -> id
|
T_BraceExpansion id _ -> id
|
||||||
|
T_DollarBraceCommandExpansion id _ -> id
|
||||||
T_IoFile id _ _ -> id
|
T_IoFile id _ _ -> id
|
||||||
T_HereDoc id _ _ _ _ -> id
|
T_HereDoc id _ _ _ _ -> id
|
||||||
T_HereString id _ -> id
|
T_HereString id _ -> id
|
||||||
T_FdRedirect id _ _ -> id
|
T_FdRedirect id _ _ -> id
|
||||||
T_Assignment id _ _ _ _ -> id
|
T_Assignment id _ _ _ _ -> id
|
||||||
T_Array id _ -> id
|
T_Array id _ -> id
|
||||||
|
T_IndexedElement id _ _ -> id
|
||||||
T_Redirecting id _ _ -> id
|
T_Redirecting id _ _ -> id
|
||||||
T_SimpleCommand id _ _ -> id
|
T_SimpleCommand id _ _ -> id
|
||||||
T_Pipeline id _ _ -> id
|
T_Pipeline id _ _ -> id
|
||||||
@@ -324,13 +348,12 @@ getId t = case t of
|
|||||||
TC_Unary id _ _ _ -> id
|
TC_Unary id _ _ _ -> id
|
||||||
TC_Noary id _ _ -> id
|
TC_Noary id _ _ -> id
|
||||||
TA_Binary id _ _ _ -> id
|
TA_Binary id _ _ _ -> id
|
||||||
|
TA_Assignment id _ _ _ -> id
|
||||||
TA_Unary id _ _ -> id
|
TA_Unary id _ _ -> id
|
||||||
TA_Sequence id _ -> id
|
TA_Sequence id _ -> id
|
||||||
TA_Variable id _ -> id
|
|
||||||
TA_Trinary id _ _ _ -> id
|
TA_Trinary id _ _ _ -> id
|
||||||
TA_Expansion id _ -> id
|
TA_Expansion id _ -> id
|
||||||
TA_Literal id _ -> id
|
TA_Index id _ -> id
|
||||||
TA_Base id _ _ -> id
|
|
||||||
T_ProcSub id _ _ -> id
|
T_ProcSub id _ _ -> id
|
||||||
T_Glob id _ -> id
|
T_Glob id _ -> id
|
||||||
T_ForArithmetic id _ _ _ _ -> id
|
T_ForArithmetic id _ _ _ _ -> id
|
||||||
@@ -339,6 +362,10 @@ getId t = case t of
|
|||||||
T_DollarBracket id _ -> id
|
T_DollarBracket id _ -> id
|
||||||
T_Annotation id _ _ -> id
|
T_Annotation id _ _ -> id
|
||||||
T_Pipe id _ -> id
|
T_Pipe id _ -> id
|
||||||
|
T_CoProc id _ _ -> id
|
||||||
|
T_CoProcBody id _ -> id
|
||||||
|
T_Include id _ _ -> id
|
||||||
|
T_BatsTest id _ _ -> id
|
||||||
|
|
||||||
blank :: Monad m => Token -> m ()
|
blank :: Monad m => Token -> m ()
|
||||||
blank = const $ return ()
|
blank = const $ return ()
|
||||||
@@ -346,10 +373,3 @@ doAnalysis f = analyze f blank id
|
|||||||
doStackAnalysis startToken endToken = analyze startToken endToken id
|
doStackAnalysis startToken endToken = analyze startToken endToken id
|
||||||
doTransform i = runIdentity . analyze blank blank i
|
doTransform i = runIdentity . analyze blank blank i
|
||||||
|
|
||||||
isLoop t = case t of
|
|
||||||
T_WhileExpression {} -> True
|
|
||||||
T_UntilExpression {} -> True
|
|
||||||
T_ForIn {} -> True
|
|
||||||
T_ForArithmetic {} -> True
|
|
||||||
T_SelectIn {} -> True
|
|
||||||
_ -> False
|
|
||||||
|
253
ShellCheck/ASTLib.hs
Normal file
253
ShellCheck/ASTLib.hs
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
{-
|
||||||
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
|
This file is part of ShellCheck.
|
||||||
|
http://www.vidarholen.net/contents/shellcheck
|
||||||
|
|
||||||
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
ShellCheck is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-}
|
||||||
|
module ShellCheck.ASTLib where
|
||||||
|
|
||||||
|
import ShellCheck.AST
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import Data.List
|
||||||
|
import Data.Maybe
|
||||||
|
|
||||||
|
-- Is this a type of loop?
|
||||||
|
isLoop t = case t of
|
||||||
|
T_WhileExpression {} -> True
|
||||||
|
T_UntilExpression {} -> True
|
||||||
|
T_ForIn {} -> True
|
||||||
|
T_ForArithmetic {} -> True
|
||||||
|
T_SelectIn {} -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
-- Will this split into multiple words when used as an argument?
|
||||||
|
willSplit x =
|
||||||
|
case x of
|
||||||
|
T_DollarBraced {} -> True
|
||||||
|
T_DollarExpansion {} -> True
|
||||||
|
T_Backticked {} -> True
|
||||||
|
T_BraceExpansion {} -> True
|
||||||
|
T_Glob {} -> True
|
||||||
|
T_Extglob {} -> True
|
||||||
|
T_NormalWord _ l -> any willSplit l
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
isGlob (T_Extglob {}) = True
|
||||||
|
isGlob (T_Glob {}) = True
|
||||||
|
isGlob (T_NormalWord _ l) = any isGlob l
|
||||||
|
isGlob _ = False
|
||||||
|
|
||||||
|
-- Is this shell word a constant?
|
||||||
|
isConstant token =
|
||||||
|
case token of
|
||||||
|
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_SimpleCommand _ vars words) -> concatMap oversimplify words
|
||||||
|
(T_Redirecting _ _ foo) -> oversimplify foo
|
||||||
|
(T_DollarSingleQuoted _ s) -> [s]
|
||||||
|
(T_Annotation _ _ s) -> oversimplify s
|
||||||
|
-- Workaround for let "foo = bar" parsing
|
||||||
|
(TA_Sequence _ [TA_Expansion _ v]) -> concatMap oversimplify v
|
||||||
|
otherwise -> []
|
||||||
|
|
||||||
|
|
||||||
|
-- Turn a SimpleCommand foo -avz --bar=baz into args "a", "v", "z", "bar",
|
||||||
|
-- each in a tuple of (token, stringFlag). Non-flag arguments are added with
|
||||||
|
-- stringFlag == "".
|
||||||
|
getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
|
||||||
|
let tokenAndText = map (\x -> (x, concat $ oversimplify x)) args
|
||||||
|
(flagArgs, rest) = break (stopCondition . snd) tokenAndText
|
||||||
|
in
|
||||||
|
concatMap flag flagArgs ++ map (\(t, _) -> (t, "")) rest
|
||||||
|
where
|
||||||
|
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
|
||||||
|
flag (x, '-':args) = map (\v -> (x, [v])) args
|
||||||
|
flag (x, _) = [ (x, "") ]
|
||||||
|
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
|
||||||
|
|
||||||
|
-- Get all flags in a GNU way, up until --
|
||||||
|
getAllFlags = getFlagsUntil (== "--")
|
||||||
|
-- Get all flags in a BSD way, up until first non-flag argument
|
||||||
|
getLeadingFlags = getFlagsUntil (not . ("-" `isPrefixOf`))
|
||||||
|
|
||||||
|
|
||||||
|
-- Given a T_DollarBraced, return a simplified version of the string contents.
|
||||||
|
bracedString (T_DollarBraced _ l) = concat $ oversimplify l
|
||||||
|
bracedString _ = error "Internal shellcheck error, please report! (bracedString on non-variable)"
|
||||||
|
|
||||||
|
-- Is this an expansion of multiple items of an array?
|
||||||
|
isArrayExpansion t@(T_DollarBraced _ _) =
|
||||||
|
let string = bracedString t in
|
||||||
|
"@" `isPrefixOf` string ||
|
||||||
|
not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string
|
||||||
|
isArrayExpansion _ = False
|
||||||
|
|
||||||
|
-- Is it possible that this arg becomes multiple args?
|
||||||
|
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
|
||||||
|
where
|
||||||
|
f t@(T_DollarBraced _ _) =
|
||||||
|
let string = bracedString t in
|
||||||
|
"!" `isPrefixOf` string
|
||||||
|
f (T_DoubleQuoted _ parts) = any f parts
|
||||||
|
f (T_NormalWord _ parts) = any f parts
|
||||||
|
f _ = False
|
||||||
|
|
||||||
|
-- Is it certain that this word will becomes multiple words?
|
||||||
|
willBecomeMultipleArgs t = willConcatInAssignment t || f t
|
||||||
|
where
|
||||||
|
f (T_Extglob {}) = True
|
||||||
|
f (T_Glob {}) = True
|
||||||
|
f (T_BraceExpansion {}) = True
|
||||||
|
f (T_DoubleQuoted _ parts) = any f parts
|
||||||
|
f (T_NormalWord _ parts) = any f parts
|
||||||
|
f _ = False
|
||||||
|
|
||||||
|
-- This does token cause implicit concatenation in assignments?
|
||||||
|
willConcatInAssignment token =
|
||||||
|
case token of
|
||||||
|
t@(T_DollarBraced {}) -> isArrayExpansion t
|
||||||
|
(T_DoubleQuoted _ parts) -> any willConcatInAssignment parts
|
||||||
|
(T_NormalWord _ parts) -> any willConcatInAssignment parts
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
-- Maybe get the literal string corresponding to this token
|
||||||
|
getLiteralString :: Token -> Maybe String
|
||||||
|
getLiteralString = getLiteralStringExt (const Nothing)
|
||||||
|
|
||||||
|
-- Definitely get a literal string, skipping over all non-literals
|
||||||
|
onlyLiteralString :: Token -> String
|
||||||
|
onlyLiteralString = fromJust . getLiteralStringExt (const $ return "")
|
||||||
|
|
||||||
|
-- Maybe get a literal string, but only if it's an unquoted argument.
|
||||||
|
getUnquotedLiteral (T_NormalWord _ list) =
|
||||||
|
liftM concat $ mapM str list
|
||||||
|
where
|
||||||
|
str (T_Literal _ s) = return s
|
||||||
|
str _ = Nothing
|
||||||
|
getUnquotedLiteral _ = Nothing
|
||||||
|
|
||||||
|
-- Maybe get the literal string of this token and any globs in it.
|
||||||
|
getGlobOrLiteralString = getLiteralStringExt f
|
||||||
|
where
|
||||||
|
f (T_Glob _ str) = return str
|
||||||
|
f _ = Nothing
|
||||||
|
|
||||||
|
-- Maybe get the literal value of a token, using a custom function
|
||||||
|
-- to map unrecognized Tokens into strings.
|
||||||
|
getLiteralStringExt :: (Token -> Maybe String) -> Token -> Maybe String
|
||||||
|
getLiteralStringExt more = g
|
||||||
|
where
|
||||||
|
allInList = liftM concat . mapM g
|
||||||
|
g (T_DoubleQuoted _ l) = allInList l
|
||||||
|
g (T_DollarDoubleQuoted _ l) = allInList l
|
||||||
|
g (T_NormalWord _ l) = allInList l
|
||||||
|
g (TA_Expansion _ l) = allInList l
|
||||||
|
g (T_SingleQuoted _ s) = return s
|
||||||
|
g (T_Literal _ s) = return s
|
||||||
|
g x = more x
|
||||||
|
|
||||||
|
-- Is this token a string literal?
|
||||||
|
isLiteral t = isJust $ getLiteralString t
|
||||||
|
|
||||||
|
|
||||||
|
-- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz]
|
||||||
|
getWordParts (T_NormalWord _ l) = concatMap getWordParts l
|
||||||
|
getWordParts (T_DoubleQuoted _ l) = l
|
||||||
|
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 the command name of a token representing a command
|
||||||
|
getCommandName t =
|
||||||
|
case t of
|
||||||
|
T_Redirecting _ _ w -> getCommandName w
|
||||||
|
T_SimpleCommand _ _ (w:_) -> getLiteralString w
|
||||||
|
T_Annotation _ _ t -> getCommandName t
|
||||||
|
otherwise -> Nothing
|
||||||
|
|
||||||
|
-- Get the basename of a token representing a command
|
||||||
|
getCommandBasename = liftM basename . getCommandName
|
||||||
|
where
|
||||||
|
basename = reverse . takeWhile (/= '/') . reverse
|
||||||
|
|
||||||
|
isAssignment t =
|
||||||
|
case t of
|
||||||
|
T_Redirecting _ _ w -> isAssignment w
|
||||||
|
T_SimpleCommand _ (w:_) [] -> True
|
||||||
|
T_Assignment {} -> True
|
||||||
|
T_Annotation _ _ w -> isAssignment w
|
||||||
|
otherwise -> False
|
||||||
|
|
||||||
|
isOnlyRedirection t =
|
||||||
|
case t of
|
||||||
|
T_Pipeline _ _ [x] -> isOnlyRedirection x
|
||||||
|
T_Annotation _ _ w -> isOnlyRedirection w
|
||||||
|
T_Redirecting _ (_:_) c -> isOnlyRedirection c
|
||||||
|
T_SimpleCommand _ [] [] -> True
|
||||||
|
otherwise -> False
|
||||||
|
|
||||||
|
isFunction t = case t of T_Function {} -> True; _ -> False
|
||||||
|
|
||||||
|
-- Get the list of commands from tokens that contain them, such as
|
||||||
|
-- the body of while loops and if statements.
|
||||||
|
getCommandSequences t =
|
||||||
|
case t of
|
||||||
|
T_Script _ _ cmds -> [cmds]
|
||||||
|
T_BraceGroup _ cmds -> [cmds]
|
||||||
|
T_Subshell _ cmds -> [cmds]
|
||||||
|
T_WhileExpression _ _ cmds -> [cmds]
|
||||||
|
T_UntilExpression _ _ cmds -> [cmds]
|
||||||
|
T_ForIn _ _ _ cmds -> [cmds]
|
||||||
|
T_ForArithmetic _ _ _ _ cmds -> [cmds]
|
||||||
|
T_IfExpression _ thens elses -> map snd thens ++ [elses]
|
||||||
|
otherwise -> []
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
36
ShellCheck/Analyzer.hs
Normal file
36
ShellCheck/Analyzer.hs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{-
|
||||||
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
|
This file is part of ShellCheck.
|
||||||
|
http://www.vidarholen.net/contents/shellcheck
|
||||||
|
|
||||||
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
ShellCheck is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-}
|
||||||
|
module ShellCheck.Analyzer (analyzeScript) where
|
||||||
|
|
||||||
|
import ShellCheck.Analytics
|
||||||
|
import ShellCheck.AnalyzerLib
|
||||||
|
import ShellCheck.Interface
|
||||||
|
import Data.List
|
||||||
|
import qualified ShellCheck.Checks.Commands
|
||||||
|
|
||||||
|
|
||||||
|
-- TODO: Clean up the cruft this is layered on
|
||||||
|
analyzeScript :: AnalysisSpec -> AnalysisResult
|
||||||
|
analyzeScript spec = AnalysisResult {
|
||||||
|
arComments =
|
||||||
|
filterByAnnotation (asScript spec) . nub $
|
||||||
|
runAnalytics spec
|
||||||
|
++ ShellCheck.Checks.Commands.runChecks spec
|
||||||
|
}
|
646
ShellCheck/AnalyzerLib.hs
Normal file
646
ShellCheck/AnalyzerLib.hs
Normal file
@@ -0,0 +1,646 @@
|
|||||||
|
{-
|
||||||
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
|
This file is part of ShellCheck.
|
||||||
|
http://www.vidarholen.net/contents/shellcheck
|
||||||
|
|
||||||
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
ShellCheck is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-}
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
module ShellCheck.AnalyzerLib where
|
||||||
|
import ShellCheck.AST
|
||||||
|
import ShellCheck.ASTLib
|
||||||
|
import ShellCheck.Data
|
||||||
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Parser
|
||||||
|
import ShellCheck.Regex
|
||||||
|
|
||||||
|
import Control.Arrow (first)
|
||||||
|
import Control.Monad.Identity
|
||||||
|
import Control.Monad.Reader
|
||||||
|
import Control.Monad.State
|
||||||
|
import Control.Monad.Writer
|
||||||
|
import Data.Char
|
||||||
|
import Data.List
|
||||||
|
import Data.Maybe
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
|
import Test.QuickCheck.All (forAllProperties)
|
||||||
|
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||||
|
|
||||||
|
type Analysis = ReaderT Parameters (Writer [TokenComment]) ()
|
||||||
|
|
||||||
|
|
||||||
|
data Parameters = Parameters {
|
||||||
|
variableFlow :: [StackData],
|
||||||
|
parentMap :: Map.Map Id Token,
|
||||||
|
shellType :: Shell,
|
||||||
|
shellTypeSpecified :: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
data Scope = SubshellScope String | NoneScope deriving (Show, Eq)
|
||||||
|
data StackData =
|
||||||
|
StackScope Scope
|
||||||
|
| StackScopeEnd
|
||||||
|
-- (Base expression, specific position, var name, assigned values)
|
||||||
|
| Assignment (Token, Token, String, DataType)
|
||||||
|
| Reference (Token, Token, String)
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
data DataType = DataString DataSource | DataArray DataSource
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
data DataSource = SourceFrom [Token] | SourceExternal | SourceDeclaration | SourceInteger
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
data VariableState = Dead Token String | Alive deriving (Show)
|
||||||
|
|
||||||
|
defaultSpec root = AnalysisSpec {
|
||||||
|
asScript = root,
|
||||||
|
asShellType = Nothing,
|
||||||
|
asExecutionMode = Executed
|
||||||
|
}
|
||||||
|
|
||||||
|
pScript s =
|
||||||
|
let
|
||||||
|
pSpec = ParseSpec {
|
||||||
|
psFilename = "script",
|
||||||
|
psScript = s
|
||||||
|
}
|
||||||
|
in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec
|
||||||
|
|
||||||
|
makeComment :: Severity -> Id -> Code -> String -> TokenComment
|
||||||
|
makeComment severity id code note =
|
||||||
|
TokenComment id $ Comment severity code note
|
||||||
|
|
||||||
|
addComment note = tell [note]
|
||||||
|
|
||||||
|
warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m ()
|
||||||
|
warn id code str = addComment $ makeComment WarningC id code str
|
||||||
|
err id code str = addComment $ makeComment ErrorC id code str
|
||||||
|
info id code str = addComment $ makeComment InfoC id code str
|
||||||
|
style id code str = addComment $ makeComment StyleC id code str
|
||||||
|
|
||||||
|
makeParameters spec =
|
||||||
|
let params = Parameters {
|
||||||
|
shellType = fromMaybe (determineShell root) $ asShellType spec,
|
||||||
|
shellTypeSpecified = isJust $ asShellType spec,
|
||||||
|
parentMap = getParentTree root,
|
||||||
|
variableFlow =
|
||||||
|
getVariableFlow (shellType params) (parentMap params) root
|
||||||
|
} in params
|
||||||
|
where root = asScript spec
|
||||||
|
|
||||||
|
prop_determineShell0 = determineShell (fromJust $ pScript "#!/bin/sh") == Sh
|
||||||
|
prop_determineShell1 = determineShell (fromJust $ pScript "#!/usr/bin/env ksh") == Ksh
|
||||||
|
prop_determineShell2 = determineShell (fromJust $ pScript "") == Bash
|
||||||
|
prop_determineShell3 = determineShell (fromJust $ pScript "#!/bin/sh -e") == Sh
|
||||||
|
prop_determineShell4 = determineShell (fromJust $ pScript
|
||||||
|
"#!/bin/ksh\n#shellcheck shell=sh\nfoo") == Sh
|
||||||
|
prop_determineShell5 = determineShell (fromJust $ pScript
|
||||||
|
"#shellcheck shell=sh\nfoo") == Sh
|
||||||
|
prop_determineShell6 = determineShell (fromJust $ pScript "#! /bin/sh") == Sh
|
||||||
|
determineShell t = fromMaybe Bash $ do
|
||||||
|
shellString <- foldl mplus Nothing $ getCandidates t
|
||||||
|
shellForExecutable shellString
|
||||||
|
where
|
||||||
|
forAnnotation t =
|
||||||
|
case t of
|
||||||
|
(ShellOverride s) -> return s
|
||||||
|
_ -> fail ""
|
||||||
|
getCandidates :: Token -> [Maybe String]
|
||||||
|
getCandidates t@(T_Script {}) = [Just $ fromShebang t]
|
||||||
|
getCandidates (T_Annotation _ annotations s) =
|
||||||
|
map forAnnotation annotations ++
|
||||||
|
[Just $ fromShebang s]
|
||||||
|
fromShebang (T_Script _ s t) = shellFor s
|
||||||
|
|
||||||
|
shellFor s | "/env " `isInfixOf` s = head (drop 1 (words s)++[""])
|
||||||
|
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
|
||||||
|
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
|
||||||
|
|
||||||
|
|
||||||
|
--- Context seeking
|
||||||
|
|
||||||
|
getParentTree t =
|
||||||
|
snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
|
||||||
|
where
|
||||||
|
pre t = modify (first ((:) t))
|
||||||
|
post t = do
|
||||||
|
(_:rest, map) <- get
|
||||||
|
case rest of [] -> put (rest, map)
|
||||||
|
(x:_) -> put (rest, Map.insert (getId t) x map)
|
||||||
|
|
||||||
|
getTokenMap t =
|
||||||
|
execState (doAnalysis f t) Map.empty
|
||||||
|
where
|
||||||
|
f t = modify (Map.insert (getId t) t)
|
||||||
|
|
||||||
|
|
||||||
|
-- Is this node self quoting for a regular element?
|
||||||
|
isQuoteFree = isQuoteFreeNode False
|
||||||
|
|
||||||
|
-- Is this node striclty self quoting, for array expansions
|
||||||
|
isStrictlyQuoteFree = isQuoteFreeNode True
|
||||||
|
|
||||||
|
|
||||||
|
isQuoteFreeNode strict tree t =
|
||||||
|
(isQuoteFreeElement t == Just True) ||
|
||||||
|
head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False])
|
||||||
|
where
|
||||||
|
-- Is this node self-quoting in itself?
|
||||||
|
isQuoteFreeElement t =
|
||||||
|
case t of
|
||||||
|
T_Assignment {} -> return True
|
||||||
|
T_FdRedirect {} -> return True
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
-- Are any subnodes inherently self-quoting?
|
||||||
|
isQuoteFreeContext t =
|
||||||
|
case t of
|
||||||
|
TC_Noary _ DoubleBracket _ -> return True
|
||||||
|
TC_Unary _ DoubleBracket _ _ -> return True
|
||||||
|
TC_Binary _ DoubleBracket _ _ _ -> return True
|
||||||
|
TA_Sequence {} -> return True
|
||||||
|
T_Arithmetic {} -> return True
|
||||||
|
T_Assignment {} -> return True
|
||||||
|
T_Redirecting {} -> return $
|
||||||
|
if strict then False else
|
||||||
|
-- Not true, just a hack to prevent warning about non-expansion refs
|
||||||
|
any (isCommand t) ["local", "declare", "typeset", "export", "trap", "readonly"]
|
||||||
|
T_DoubleQuoted _ _ -> return True
|
||||||
|
T_DollarDoubleQuoted _ _ -> return True
|
||||||
|
T_CaseExpression {} -> return True
|
||||||
|
T_HereDoc {} -> return True
|
||||||
|
T_DollarBraced {} -> return True
|
||||||
|
-- When non-strict, pragmatically assume it's desirable to split here
|
||||||
|
T_ForIn {} -> return (not strict)
|
||||||
|
T_SelectIn {} -> return (not strict)
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
isParamTo tree cmd =
|
||||||
|
go
|
||||||
|
where
|
||||||
|
go x = case Map.lookup (getId x) tree of
|
||||||
|
Nothing -> False
|
||||||
|
Just parent -> check parent
|
||||||
|
check t =
|
||||||
|
case t of
|
||||||
|
T_SingleQuoted _ _ -> go t
|
||||||
|
T_DoubleQuoted _ _ -> go t
|
||||||
|
T_NormalWord _ _ -> go t
|
||||||
|
T_SimpleCommand {} -> isCommand t cmd
|
||||||
|
T_Redirecting {} -> isCommand t cmd
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
getClosestCommand tree t =
|
||||||
|
msum . map getCommand $ getPath tree t
|
||||||
|
where
|
||||||
|
getCommand t@(T_Redirecting {}) = return t
|
||||||
|
getCommand _ = Nothing
|
||||||
|
|
||||||
|
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
||||||
|
where
|
||||||
|
go currentId (T_NormalWord id [word]:rest)
|
||||||
|
| currentId == getId word = go id rest
|
||||||
|
go currentId (T_DoubleQuoted id [word]:rest)
|
||||||
|
| currentId == getId word = go id rest
|
||||||
|
go currentId (T_SimpleCommand _ _ (word:_):_)
|
||||||
|
| currentId == getId word = True
|
||||||
|
go _ _ = False
|
||||||
|
|
||||||
|
-- A list of the element and all its parents
|
||||||
|
getPath tree t = t :
|
||||||
|
case Map.lookup (getId t) tree of
|
||||||
|
Nothing -> []
|
||||||
|
Just parent -> getPath tree parent
|
||||||
|
|
||||||
|
isParentOf tree parent child =
|
||||||
|
elem (getId parent) . map getId $ getPath tree child
|
||||||
|
|
||||||
|
parents params = getPath (parentMap params)
|
||||||
|
|
||||||
|
pathTo t = do
|
||||||
|
parents <- reader parentMap
|
||||||
|
return $ getPath parents t
|
||||||
|
|
||||||
|
-- Check whether a word is entirely output from a single command
|
||||||
|
tokenIsJustCommandOutput t = case t of
|
||||||
|
T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds
|
||||||
|
T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ cmds]] -> check cmds
|
||||||
|
T_NormalWord id [T_Backticked _ cmds] -> check cmds
|
||||||
|
T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ cmds]] -> check cmds
|
||||||
|
_ -> False
|
||||||
|
where
|
||||||
|
check [x] = not $ isOnlyRedirection x
|
||||||
|
check _ = False
|
||||||
|
|
||||||
|
-- TODO: Replace this with a proper Control Flow Graph
|
||||||
|
getVariableFlow shell parents t =
|
||||||
|
let (_, stack) = runState (doStackAnalysis startScope endScope t) []
|
||||||
|
in reverse stack
|
||||||
|
where
|
||||||
|
startScope t =
|
||||||
|
let scopeType = leadType shell parents t
|
||||||
|
in do
|
||||||
|
when (scopeType /= NoneScope) $ modify (StackScope scopeType:)
|
||||||
|
when (assignFirst t) $ setWritten t
|
||||||
|
|
||||||
|
endScope t =
|
||||||
|
let scopeType = leadType shell parents t
|
||||||
|
in do
|
||||||
|
setRead t
|
||||||
|
unless (assignFirst t) $ setWritten t
|
||||||
|
when (scopeType /= NoneScope) $ modify (StackScopeEnd:)
|
||||||
|
|
||||||
|
assignFirst (T_ForIn {}) = True
|
||||||
|
assignFirst (T_SelectIn {}) = True
|
||||||
|
assignFirst (T_BatsTest {}) = True
|
||||||
|
assignFirst _ = False
|
||||||
|
|
||||||
|
setRead t =
|
||||||
|
let read = getReferencedVariables parents t
|
||||||
|
in mapM_ (\v -> modify (Reference v:)) read
|
||||||
|
|
||||||
|
setWritten t =
|
||||||
|
let written = getModifiedVariables t
|
||||||
|
in mapM_ (\v -> modify (Assignment v:)) written
|
||||||
|
|
||||||
|
|
||||||
|
leadType shell parents t =
|
||||||
|
case t of
|
||||||
|
T_DollarExpansion _ _ -> SubshellScope "$(..) expansion"
|
||||||
|
T_Backticked _ _ -> SubshellScope "`..` expansion"
|
||||||
|
T_Backgrounded _ _ -> SubshellScope "backgrounding &"
|
||||||
|
T_Subshell _ _ -> SubshellScope "(..) group"
|
||||||
|
T_BatsTest {} -> SubshellScope "@bats test"
|
||||||
|
T_CoProcBody _ _ -> SubshellScope "coproc"
|
||||||
|
T_Redirecting {} ->
|
||||||
|
if fromMaybe False causesSubshell
|
||||||
|
then SubshellScope "pipeline"
|
||||||
|
else NoneScope
|
||||||
|
_ -> NoneScope
|
||||||
|
where
|
||||||
|
parentPipeline = do
|
||||||
|
parent <- Map.lookup (getId t) parents
|
||||||
|
case parent of
|
||||||
|
T_Pipeline {} -> return parent
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
causesSubshell = do
|
||||||
|
(T_Pipeline _ _ list) <- parentPipeline
|
||||||
|
if length list <= 1
|
||||||
|
then return False
|
||||||
|
else if lastCreatesSubshell
|
||||||
|
then return True
|
||||||
|
else return . not $ (getId . head $ reverse list) == getId t
|
||||||
|
|
||||||
|
lastCreatesSubshell =
|
||||||
|
case shell of
|
||||||
|
Bash -> True
|
||||||
|
Dash -> True
|
||||||
|
Sh -> True
|
||||||
|
Ksh -> False
|
||||||
|
|
||||||
|
getModifiedVariables t =
|
||||||
|
case t of
|
||||||
|
T_SimpleCommand _ vars [] ->
|
||||||
|
concatMap (\x -> case x of
|
||||||
|
T_Assignment id _ name _ w ->
|
||||||
|
[(x, x, name, dataTypeFrom DataString w)]
|
||||||
|
_ -> []
|
||||||
|
) vars
|
||||||
|
c@(T_SimpleCommand {}) ->
|
||||||
|
getModifiedVariableCommand c
|
||||||
|
|
||||||
|
TA_Unary _ "++|" var -> maybeToList $ do
|
||||||
|
name <- getLiteralString var
|
||||||
|
return (t, t, name, DataString $ SourceFrom [t])
|
||||||
|
TA_Unary _ "|++" var -> maybeToList $ do
|
||||||
|
name <- getLiteralString var
|
||||||
|
return (t, t, name, DataString $ SourceFrom [t])
|
||||||
|
TA_Assignment _ op lhs rhs -> maybeToList $ do
|
||||||
|
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
||||||
|
name <- getLiteralString lhs
|
||||||
|
return (t, t, name, DataString $ SourceFrom [rhs])
|
||||||
|
|
||||||
|
T_BatsTest {} -> [
|
||||||
|
(t, t, "lines", DataArray SourceExternal),
|
||||||
|
(t, t, "status", DataString SourceInteger),
|
||||||
|
(t, t, "output", DataString SourceExternal)
|
||||||
|
]
|
||||||
|
|
||||||
|
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo
|
||||||
|
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
|
||||||
|
|
||||||
|
t@(T_CoProc _ name _) ->
|
||||||
|
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
|
||||||
|
|
||||||
|
--Points to 'for' rather than variable
|
||||||
|
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
|
||||||
|
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
|
||||||
|
_ -> []
|
||||||
|
|
||||||
|
isClosingFileOp op =
|
||||||
|
case op of
|
||||||
|
T_IoFile _ (T_GREATAND _) (T_NormalWord _ [T_Literal _ "-"]) -> True
|
||||||
|
T_IoFile _ (T_LESSAND _) (T_NormalWord _ [T_Literal _ "-"]) -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
|
||||||
|
-- Consider 'export/declare -x' a reference, since it makes the var available
|
||||||
|
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
||||||
|
case x of
|
||||||
|
"export" -> if "f" `elem` flags
|
||||||
|
then []
|
||||||
|
else concatMap getReference rest
|
||||||
|
"declare" -> if any (`elem` flags) ["x", "p"]
|
||||||
|
then concatMap getReference rest
|
||||||
|
else []
|
||||||
|
"readonly" -> concatMap getReference rest
|
||||||
|
"trap" ->
|
||||||
|
case rest of
|
||||||
|
head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head
|
||||||
|
_ -> []
|
||||||
|
_ -> []
|
||||||
|
where
|
||||||
|
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
|
||||||
|
getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
|
||||||
|
getReference _ = []
|
||||||
|
flags = map snd $ getAllFlags base
|
||||||
|
|
||||||
|
getReferencedVariableCommand _ = []
|
||||||
|
|
||||||
|
getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
||||||
|
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
|
||||||
|
case x of
|
||||||
|
"read" ->
|
||||||
|
let params = map getLiteral rest in
|
||||||
|
catMaybes . takeWhile isJust . reverse $ params
|
||||||
|
"getopts" ->
|
||||||
|
case rest of
|
||||||
|
opts:var:_ -> maybeToList $ getLiteral var
|
||||||
|
_ -> []
|
||||||
|
|
||||||
|
"let" -> concatMap letParamToLiteral rest
|
||||||
|
|
||||||
|
"export" ->
|
||||||
|
if "f" `elem` flags then [] else concatMap getModifierParamString rest
|
||||||
|
|
||||||
|
"declare" -> if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
|
||||||
|
"typeset" -> declaredVars
|
||||||
|
|
||||||
|
"local" -> concatMap getModifierParamString rest
|
||||||
|
"readonly" -> concatMap getModifierParamString rest
|
||||||
|
"set" -> maybeToList $ do
|
||||||
|
params <- getSetParams rest
|
||||||
|
return (base, base, "@", DataString $ SourceFrom params)
|
||||||
|
|
||||||
|
"printf" -> maybeToList $ getPrintfVariable rest
|
||||||
|
|
||||||
|
"mapfile" -> maybeToList $ getMapfileArray base rest
|
||||||
|
"readarray" -> maybeToList $ getMapfileArray base rest
|
||||||
|
|
||||||
|
_ -> []
|
||||||
|
where
|
||||||
|
flags = map snd $ getAllFlags base
|
||||||
|
stripEquals s = let rest = dropWhile (/= '=') s in
|
||||||
|
if rest == "" then "" else tail rest
|
||||||
|
stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) =
|
||||||
|
T_NormalWord id1 (T_Literal id2 (stripEquals s):rs)
|
||||||
|
stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) =
|
||||||
|
T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
|
||||||
|
stripEqualsFrom t = t
|
||||||
|
|
||||||
|
declaredVars = concatMap (getModifierParam defaultType) rest
|
||||||
|
where
|
||||||
|
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
|
||||||
|
|
||||||
|
getLiteral t = do
|
||||||
|
s <- getLiteralString t
|
||||||
|
when ("-" `isPrefixOf` s) $ fail "argument"
|
||||||
|
return (base, t, s, DataString SourceExternal)
|
||||||
|
|
||||||
|
getModifierParamString = getModifierParam DataString
|
||||||
|
|
||||||
|
getModifierParam def t@(T_Assignment _ _ name _ value) =
|
||||||
|
[(base, t, name, dataTypeFrom def value)]
|
||||||
|
getModifierParam def t@(T_NormalWord {}) = maybeToList $ do
|
||||||
|
name <- getLiteralString t
|
||||||
|
guard $ isVariableName name
|
||||||
|
return (base, t, name, def SourceDeclaration)
|
||||||
|
getModifierParam _ _ = []
|
||||||
|
|
||||||
|
letParamToLiteral token =
|
||||||
|
if var == ""
|
||||||
|
then []
|
||||||
|
else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])]
|
||||||
|
where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token
|
||||||
|
|
||||||
|
getSetParams (t:_:rest) | getLiteralString t == Just "-o" = getSetParams rest
|
||||||
|
getSetParams (t:rest) =
|
||||||
|
let s = getLiteralString t in
|
||||||
|
case s of
|
||||||
|
Just "--" -> return rest
|
||||||
|
Just ('-':_) -> getSetParams rest
|
||||||
|
_ -> return (t:fromMaybe [] (getSetParams rest))
|
||||||
|
getSetParams [] = Nothing
|
||||||
|
|
||||||
|
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
|
||||||
|
where
|
||||||
|
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, var, DataString $ SourceFrom list)
|
||||||
|
f (_:rest) = f rest
|
||||||
|
f [] = fail "not found"
|
||||||
|
|
||||||
|
-- mapfile has some curious syntax allowing flags plus 0..n variable names
|
||||||
|
-- where only the first non-option one is used if any. Here we cheat and
|
||||||
|
-- just get the last one, if it's a variable name.
|
||||||
|
getMapfileArray base arguments = do
|
||||||
|
lastArg <- listToMaybe (reverse arguments)
|
||||||
|
name <- getLiteralString lastArg
|
||||||
|
guard $ isVariableName name
|
||||||
|
return (base, lastArg, name, DataArray SourceExternal)
|
||||||
|
|
||||||
|
getModifiedVariableCommand _ = []
|
||||||
|
|
||||||
|
getIndexReferences s = fromMaybe [] $ do
|
||||||
|
match <- matchRegex re s
|
||||||
|
index <- match !!! 0
|
||||||
|
return $ matchAllStrings variableNameRegex index
|
||||||
|
where
|
||||||
|
re = mkRegex "(\\[.*\\])"
|
||||||
|
|
||||||
|
getReferencedVariables parents t =
|
||||||
|
case t of
|
||||||
|
T_DollarBraced id l -> let str = bracedString t in
|
||||||
|
(t, t, getBracedReference str) :
|
||||||
|
map (\x -> (l, l, x)) (getIndexReferences str)
|
||||||
|
TA_Expansion id _ ->
|
||||||
|
if isArithmeticAssignment t
|
||||||
|
then []
|
||||||
|
else getIfReference t t
|
||||||
|
T_Assignment id mode str _ word ->
|
||||||
|
[(t, t, str) | mode == Append] ++ specialReferences str t word
|
||||||
|
|
||||||
|
TC_Unary id _ "-v" token -> getIfReference t token
|
||||||
|
TC_Unary id _ "-R" token -> getIfReference t token
|
||||||
|
TC_Binary id DoubleBracket op lhs rhs ->
|
||||||
|
if isDereferencing op
|
||||||
|
then concatMap (getIfReference t) [lhs, rhs]
|
||||||
|
else []
|
||||||
|
|
||||||
|
T_BatsTest {} -> [ -- pretend @test references vars to avoid warnings
|
||||||
|
(t, t, "lines"),
|
||||||
|
(t, t, "status"),
|
||||||
|
(t, t, "output")
|
||||||
|
]
|
||||||
|
|
||||||
|
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
|
||||||
|
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
|
||||||
|
x -> getReferencedVariableCommand x
|
||||||
|
where
|
||||||
|
-- Try to reduce false positives for unused vars only referenced from evaluated vars
|
||||||
|
specialReferences name base word =
|
||||||
|
if name `elem` [
|
||||||
|
"PS1", "PS2", "PS3", "PS4",
|
||||||
|
"PROMPT_COMMAND"
|
||||||
|
]
|
||||||
|
then
|
||||||
|
map (\x -> (base, base, x)) $
|
||||||
|
getVariablesFromLiteralToken word
|
||||||
|
else []
|
||||||
|
|
||||||
|
literalizer (TA_Index {}) = return "" -- x[0] becomes a reference of x
|
||||||
|
literalizer _ = Nothing
|
||||||
|
|
||||||
|
getIfReference context token = maybeToList $ do
|
||||||
|
str <- getLiteralStringExt literalizer token
|
||||||
|
guard . not $ null str
|
||||||
|
when (isDigit $ head str) $ fail "is a number"
|
||||||
|
return (context, token, getBracedReference str)
|
||||||
|
|
||||||
|
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
|
||||||
|
|
||||||
|
isArithmeticAssignment t = case getPath parents t of
|
||||||
|
this: TA_Assignment _ "=" _ _ :_ -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
|
||||||
|
|
||||||
|
|
||||||
|
--- Command specific checks
|
||||||
|
|
||||||
|
isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `isSuffixOf` cmd)
|
||||||
|
isUnqualifiedCommand token str = isCommandMatch token (== str)
|
||||||
|
|
||||||
|
isCommandMatch token matcher = fromMaybe False $ do
|
||||||
|
cmd <- getCommandName token
|
||||||
|
return $ matcher cmd
|
||||||
|
|
||||||
|
isConfusedGlobRegex ('*':_) = True
|
||||||
|
isConfusedGlobRegex [x,'*'] | x /= '\\' = True
|
||||||
|
isConfusedGlobRegex _ = False
|
||||||
|
|
||||||
|
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
|
||||||
|
isVariableChar x = isVariableStartChar x || isDigit x
|
||||||
|
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
|
||||||
|
|
||||||
|
prop_isVariableName1 = isVariableName "_fo123"
|
||||||
|
prop_isVariableName2 = not $ isVariableName "4"
|
||||||
|
prop_isVariableName3 = not $ isVariableName "test: "
|
||||||
|
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
|
||||||
|
isVariableName _ = False
|
||||||
|
|
||||||
|
getVariablesFromLiteralToken token =
|
||||||
|
getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token)
|
||||||
|
|
||||||
|
-- Try to get referenced variables from a literal string like "$foo"
|
||||||
|
-- Ignores tons of cases like arithmetic evaluation and array indices.
|
||||||
|
prop_getVariablesFromLiteral1 =
|
||||||
|
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
|
||||||
|
getVariablesFromLiteral string =
|
||||||
|
map (!! 0) $ matchAllSubgroups variableRegex string
|
||||||
|
where
|
||||||
|
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
|
||||||
|
|
||||||
|
prop_getBracedReference1 = getBracedReference "foo" == "foo"
|
||||||
|
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
|
||||||
|
prop_getBracedReference3 = getBracedReference "#" == "#"
|
||||||
|
prop_getBracedReference4 = getBracedReference "##" == "#"
|
||||||
|
prop_getBracedReference5 = getBracedReference "#!" == "!"
|
||||||
|
prop_getBracedReference6 = getBracedReference "!#" == "#"
|
||||||
|
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
|
||||||
|
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
|
||||||
|
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
|
||||||
|
prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
|
||||||
|
prop_getBracedReference11= getBracedReference "!os*" == ""
|
||||||
|
prop_getBracedReference12= getBracedReference "!os?bar**" == ""
|
||||||
|
getBracedReference s = fromMaybe s $
|
||||||
|
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
|
||||||
|
where
|
||||||
|
noPrefix = dropPrefix s
|
||||||
|
dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest
|
||||||
|
dropPrefix "" = ""
|
||||||
|
takeName s = do
|
||||||
|
let name = takeWhile isVariableChar s
|
||||||
|
guard . not $ null name
|
||||||
|
return name
|
||||||
|
getSpecial (c:_) =
|
||||||
|
if c `elem` "*@#?-$!" then return [c] else fail "not special"
|
||||||
|
getSpecial _ = fail "empty"
|
||||||
|
|
||||||
|
nameExpansion ('!':rest) = do -- e.g. ${!foo*bar*}
|
||||||
|
let suffix = dropWhile isVariableChar rest
|
||||||
|
guard $ suffix /= rest -- e.g. ${!@}
|
||||||
|
first <- suffix !!! 0
|
||||||
|
guard $ first `elem` "*?"
|
||||||
|
return ""
|
||||||
|
nameExpansion _ = Nothing
|
||||||
|
|
||||||
|
|
||||||
|
-- Useful generic functions
|
||||||
|
potentially :: Monad m => Maybe (m ()) -> m ()
|
||||||
|
potentially = fromMaybe (return ())
|
||||||
|
|
||||||
|
headOrDefault _ (a:_) = a
|
||||||
|
headOrDefault def _ = def
|
||||||
|
|
||||||
|
(!!!) list i =
|
||||||
|
case drop i list of
|
||||||
|
[] -> Nothing
|
||||||
|
(r:_) -> Just r
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
filterByAnnotation token =
|
||||||
|
filter (not . shouldIgnore)
|
||||||
|
where
|
||||||
|
idFor (TokenComment id _) = id
|
||||||
|
shouldIgnore note =
|
||||||
|
any (shouldIgnoreFor (getCode note)) $
|
||||||
|
getPath parents (T_Bang $ idFor note)
|
||||||
|
shouldIgnoreFor num (T_Annotation _ anns _) =
|
||||||
|
any hasNum anns
|
||||||
|
where
|
||||||
|
hasNum (DisableComment ts) = num == ts
|
||||||
|
hasNum _ = False
|
||||||
|
shouldIgnoreFor _ (T_Include {}) = True -- Ignore included files
|
||||||
|
shouldIgnoreFor _ _ = False
|
||||||
|
parents = getParentTree token
|
||||||
|
getCode (TokenComment _ (Comment _ c _)) = c
|
||||||
|
|
||||||
|
|
||||||
|
return []
|
||||||
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
162
ShellCheck/Checker.hs
Normal file
162
ShellCheck/Checker.hs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
{-
|
||||||
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
|
This file is part of ShellCheck.
|
||||||
|
http://www.vidarholen.net/contents/shellcheck
|
||||||
|
|
||||||
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
ShellCheck is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-}
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
|
||||||
|
|
||||||
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Parser
|
||||||
|
import ShellCheck.Analyzer
|
||||||
|
|
||||||
|
import Data.Either
|
||||||
|
import Data.Functor
|
||||||
|
import Data.List
|
||||||
|
import Data.Maybe
|
||||||
|
import Data.Ord
|
||||||
|
import Control.Monad.Identity
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import qualified System.IO
|
||||||
|
import Prelude hiding (readFile)
|
||||||
|
import Control.Monad
|
||||||
|
|
||||||
|
import Test.QuickCheck.All
|
||||||
|
|
||||||
|
tokenToPosition map (TokenComment id c) = fromMaybe fail $ do
|
||||||
|
position <- Map.lookup id map
|
||||||
|
return $ PositionedComment position c
|
||||||
|
where
|
||||||
|
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
|
||||||
|
|
||||||
|
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
|
||||||
|
checkScript sys spec = do
|
||||||
|
results <- checkScript (csScript spec)
|
||||||
|
return CheckResult {
|
||||||
|
crFilename = csFilename spec,
|
||||||
|
crComments = results
|
||||||
|
}
|
||||||
|
where
|
||||||
|
checkScript contents = do
|
||||||
|
result <- parseScript sys ParseSpec {
|
||||||
|
psFilename = csFilename spec,
|
||||||
|
psScript = contents
|
||||||
|
}
|
||||||
|
let parseMessages = prComments result
|
||||||
|
let analysisMessages =
|
||||||
|
fromMaybe [] $
|
||||||
|
(arComments . analyzeScript . analysisSpec)
|
||||||
|
<$> prRoot result
|
||||||
|
let translator = tokenToPosition (prTokenPositions result)
|
||||||
|
return . nub . sortMessages . filter shouldInclude $
|
||||||
|
(parseMessages ++ map translator analysisMessages)
|
||||||
|
|
||||||
|
shouldInclude (PositionedComment _ (Comment _ code _)) =
|
||||||
|
code `notElem` csExcludedWarnings spec
|
||||||
|
|
||||||
|
sortMessages = sortBy (comparing order)
|
||||||
|
order (PositionedComment pos (Comment severity code message)) =
|
||||||
|
(posFile pos, posLine pos, posColumn pos, severity, code, message)
|
||||||
|
getPosition (PositionedComment pos _) = pos
|
||||||
|
|
||||||
|
analysisSpec root =
|
||||||
|
AnalysisSpec {
|
||||||
|
asScript = root,
|
||||||
|
asShellType = csShellTypeOverride spec,
|
||||||
|
asExecutionMode = Executed
|
||||||
|
}
|
||||||
|
|
||||||
|
getErrors sys spec =
|
||||||
|
sort . map getCode . crComments $
|
||||||
|
runIdentity (checkScript sys spec)
|
||||||
|
where
|
||||||
|
getCode (PositionedComment _ (Comment _ code _)) = code
|
||||||
|
|
||||||
|
check = checkWithIncludes []
|
||||||
|
|
||||||
|
checkWithIncludes includes src =
|
||||||
|
getErrors
|
||||||
|
(mockedSystemInterface includes)
|
||||||
|
emptyCheckSpec {
|
||||||
|
csScript = src,
|
||||||
|
csExcludedWarnings = [2148]
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_findsParseIssue = check "echo \"$12\"" == [1037]
|
||||||
|
|
||||||
|
prop_commentDisablesParseIssue1 =
|
||||||
|
null $ check "#shellcheck disable=SC1037\necho \"$12\""
|
||||||
|
prop_commentDisablesParseIssue2 =
|
||||||
|
null $ check "#shellcheck disable=SC1037\n#lol\necho \"$12\""
|
||||||
|
|
||||||
|
prop_findsAnalysisIssue =
|
||||||
|
check "echo $1" == [2086]
|
||||||
|
prop_commentDisablesAnalysisIssue1 =
|
||||||
|
null $ check "#shellcheck disable=SC2086\necho $1"
|
||||||
|
prop_commentDisablesAnalysisIssue2 =
|
||||||
|
null $ check "#shellcheck disable=SC2086\n#lol\necho $1"
|
||||||
|
|
||||||
|
prop_optionDisablesIssue1 =
|
||||||
|
null $ getErrors
|
||||||
|
(mockedSystemInterface [])
|
||||||
|
emptyCheckSpec {
|
||||||
|
csScript = "echo $1",
|
||||||
|
csExcludedWarnings = [2148, 2086]
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_optionDisablesIssue2 =
|
||||||
|
null $ getErrors
|
||||||
|
(mockedSystemInterface [])
|
||||||
|
emptyCheckSpec {
|
||||||
|
csScript = "echo \"$10\"",
|
||||||
|
csExcludedWarnings = [2148, 1037]
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_canParseDevNull =
|
||||||
|
[] == check "source /dev/null"
|
||||||
|
|
||||||
|
prop_failsWhenNotSourcing =
|
||||||
|
[1091, 2154] == check "source lol; echo \"$bar\""
|
||||||
|
|
||||||
|
prop_worksWhenSourcing =
|
||||||
|
null $ checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\""
|
||||||
|
|
||||||
|
prop_worksWhenDotting =
|
||||||
|
null $ checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\""
|
||||||
|
|
||||||
|
prop_noInfiniteSourcing =
|
||||||
|
[] == checkWithIncludes [("lib", "source lib")] "source lib"
|
||||||
|
|
||||||
|
prop_canSourceBadSyntax =
|
||||||
|
[1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
|
||||||
|
|
||||||
|
prop_cantSourceDynamic =
|
||||||
|
[1090] == checkWithIncludes [("lib", "")] ". \"$1\""
|
||||||
|
|
||||||
|
prop_cantSourceDynamic2 =
|
||||||
|
[1090] == checkWithIncludes [("lib", "")] "source ~/foo"
|
||||||
|
|
||||||
|
prop_canSourceDynamicWhenRedirected =
|
||||||
|
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
|
||||||
|
|
||||||
|
prop_sourceDirectiveDoesntFollowFile =
|
||||||
|
null $ checkWithIncludes
|
||||||
|
[("foo", "source bar"), ("bar", "baz=3")]
|
||||||
|
"#shellcheck source=foo\n. \"$1\"; echo \"$baz\""
|
||||||
|
|
||||||
|
return []
|
||||||
|
runTests = $quickCheckAll
|
560
ShellCheck/Checks/Commands.hs
Normal file
560
ShellCheck/Checks/Commands.hs
Normal file
@@ -0,0 +1,560 @@
|
|||||||
|
{-
|
||||||
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
|
This file is part of ShellCheck.
|
||||||
|
http://www.vidarholen.net/contents/shellcheck
|
||||||
|
|
||||||
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
ShellCheck is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-}
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
|
||||||
|
-- This module contains checks that examine specific commands by name.
|
||||||
|
module ShellCheck.Checks.Commands (runChecks
|
||||||
|
, ShellCheck.Checks.Commands.runTests
|
||||||
|
) where
|
||||||
|
|
||||||
|
import ShellCheck.AST
|
||||||
|
import ShellCheck.ASTLib
|
||||||
|
import ShellCheck.AnalyzerLib
|
||||||
|
import ShellCheck.Data
|
||||||
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Parser
|
||||||
|
import ShellCheck.Regex
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import Control.Monad.Reader
|
||||||
|
import Control.Monad.Writer
|
||||||
|
import Data.Char
|
||||||
|
import Data.List
|
||||||
|
import Data.Maybe
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import Test.QuickCheck.All (forAllProperties)
|
||||||
|
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||||
|
|
||||||
|
data CommandName = Exactly String | Basename String
|
||||||
|
deriving (Eq, Ord)
|
||||||
|
|
||||||
|
data CommandCheck =
|
||||||
|
CommandCheck CommandName (Token -> Analysis)
|
||||||
|
|
||||||
|
nullCheck :: Token -> Analysis
|
||||||
|
nullCheck _ = return ()
|
||||||
|
|
||||||
|
|
||||||
|
verify :: CommandCheck -> String -> Bool
|
||||||
|
verify f s = producesComments f s == Just True
|
||||||
|
verifyNot f s = producesComments f s == Just False
|
||||||
|
|
||||||
|
producesComments :: CommandCheck -> String -> Maybe Bool
|
||||||
|
producesComments f s = do
|
||||||
|
root <- pScript s
|
||||||
|
return . not . null $ runList (defaultSpec root) [f]
|
||||||
|
|
||||||
|
composeChecks f g t = do
|
||||||
|
f t
|
||||||
|
g t
|
||||||
|
|
||||||
|
arguments (T_SimpleCommand _ _ (cmd:args)) = args
|
||||||
|
|
||||||
|
commandChecks :: [CommandCheck]
|
||||||
|
commandChecks = [
|
||||||
|
checkTr
|
||||||
|
,checkFindNameGlob
|
||||||
|
,checkNeedlessExpr
|
||||||
|
,checkGrepRe
|
||||||
|
,checkTrapQuotes
|
||||||
|
,checkReturn
|
||||||
|
,checkFindExecWithSingleArgument
|
||||||
|
,checkUnusedEchoEscapes
|
||||||
|
,checkInjectableFindSh
|
||||||
|
,checkFindActionPrecedence
|
||||||
|
,checkMkdirDashPM
|
||||||
|
,checkNonportableSignals
|
||||||
|
,checkInteractiveSu
|
||||||
|
,checkSshCommandString
|
||||||
|
,checkPrintfVar
|
||||||
|
,checkUuoeCmd
|
||||||
|
,checkSetAssignment
|
||||||
|
,checkExportedExpansions
|
||||||
|
,checkAliasesUsesArgs
|
||||||
|
,checkAliasesExpandEarly
|
||||||
|
]
|
||||||
|
|
||||||
|
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
||||||
|
buildCommandMap = foldl' addCheck Map.empty
|
||||||
|
where
|
||||||
|
addCheck map (CommandCheck name function) =
|
||||||
|
Map.insertWith' composeChecks name function map
|
||||||
|
|
||||||
|
|
||||||
|
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
|
||||||
|
checkCommand map t@(T_SimpleCommand id _ (cmd:rest)) = fromMaybe (return ()) $ do
|
||||||
|
name <- getLiteralString cmd
|
||||||
|
return $
|
||||||
|
if '/' `elem` name
|
||||||
|
then
|
||||||
|
Map.findWithDefault nullCheck (Basename $ basename name) map t
|
||||||
|
else do
|
||||||
|
Map.findWithDefault nullCheck (Exactly name) map t
|
||||||
|
Map.findWithDefault nullCheck (Basename name) map t
|
||||||
|
|
||||||
|
where
|
||||||
|
basename = reverse . takeWhile (/= '/') . reverse
|
||||||
|
checkCommand _ _ = return ()
|
||||||
|
|
||||||
|
runList spec list = notes
|
||||||
|
where
|
||||||
|
root = asScript spec
|
||||||
|
params = makeParameters spec
|
||||||
|
notes = execWriter $ runReaderT (doAnalysis (checkCommand map) root) params
|
||||||
|
map = buildCommandMap list
|
||||||
|
|
||||||
|
runChecks spec = runList spec commandChecks
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
|
||||||
|
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
|
||||||
|
prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'"
|
||||||
|
prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'"
|
||||||
|
prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'"
|
||||||
|
prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'"
|
||||||
|
prop_checkTr4 = verifyNot checkTr "ls [a-z]"
|
||||||
|
prop_checkTr5 = verify checkTr "tr foo bar"
|
||||||
|
prop_checkTr6 = verify checkTr "tr 'hello' 'world'"
|
||||||
|
prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
|
||||||
|
prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
|
||||||
|
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
|
||||||
|
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
|
||||||
|
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
|
||||||
|
where
|
||||||
|
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
|
||||||
|
warn (getId w) 2060 "Quote parameters to tr to prevent glob expansion."
|
||||||
|
f word =
|
||||||
|
case getLiteralString word of
|
||||||
|
Just "a-z" -> info (getId word) 2018 "Use '[:lower:]' to support accents and foreign alphabets."
|
||||||
|
Just "A-Z" -> info (getId word) 2019 "Use '[:upper:]' to support accents and foreign alphabets."
|
||||||
|
Just s -> do -- Eliminate false positives by only looking for dupes in SET2?
|
||||||
|
when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $
|
||||||
|
info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)."
|
||||||
|
unless ("[:" `isPrefixOf` s) $
|
||||||
|
when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $
|
||||||
|
info (getId word) 2021 "Don't use [] around ranges in tr, it replaces literal square brackets."
|
||||||
|
Nothing -> return ()
|
||||||
|
|
||||||
|
duplicated s =
|
||||||
|
let relevant = filter isAlpha s
|
||||||
|
in relevant /= nub relevant
|
||||||
|
|
||||||
|
prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php"
|
||||||
|
prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)"
|
||||||
|
prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"
|
||||||
|
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
|
||||||
|
acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
|
||||||
|
acceptsGlob _ = False
|
||||||
|
f [] = return ()
|
||||||
|
f [x] = return ()
|
||||||
|
f (a:b:r) = do
|
||||||
|
when (acceptsGlob (getLiteralString a) && isGlob b) $ do
|
||||||
|
let (Just s) = getLiteralString a
|
||||||
|
warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."
|
||||||
|
f (b:r)
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
|
||||||
|
prop_checkNeedlessExpr2 = verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``"
|
||||||
|
prop_checkNeedlessExpr3 = verifyNot checkNeedlessExpr "foo=$(expr foo : regex)"
|
||||||
|
prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)"
|
||||||
|
checkNeedlessExpr = CommandCheck (Basename "expr") f where
|
||||||
|
f t =
|
||||||
|
when (all (`notElem` exceptions) (words $ arguments t)) $
|
||||||
|
style (getId t) 2003
|
||||||
|
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
|
||||||
|
-- These operators are hard to replicate in POSIX
|
||||||
|
exceptions = [ ":", "<", ">", "<=", ">=" ]
|
||||||
|
words = mapMaybe getLiteralString
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3"
|
||||||
|
prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3"
|
||||||
|
prop_checkGrepRe3 = verify checkGrepRe "grep --regex=*.mp3 file"
|
||||||
|
prop_checkGrepRe4 = verifyNot checkGrepRe "grep foo *.mp3"
|
||||||
|
prop_checkGrepRe5 = verifyNot checkGrepRe "grep-v --regex=moo *"
|
||||||
|
prop_checkGrepRe6 = verifyNot checkGrepRe "grep foo \\*.mp3"
|
||||||
|
prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file"
|
||||||
|
prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg"
|
||||||
|
prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"
|
||||||
|
prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file"
|
||||||
|
prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo"
|
||||||
|
|
||||||
|
checkGrepRe = CommandCheck (Basename "grep") (f . arguments) where
|
||||||
|
-- --regex=*(extglob) doesn't work. Fixme?
|
||||||
|
skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s
|
||||||
|
skippable _ = False
|
||||||
|
f [] = return ()
|
||||||
|
f (x:r) | skippable (getLiteralStringExt (const $ return "_") x) = f r
|
||||||
|
f (re:_) = do
|
||||||
|
when (isGlob re) $
|
||||||
|
warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it."
|
||||||
|
let string = concat $ oversimplify re
|
||||||
|
if isConfusedGlobRegex string then
|
||||||
|
warn (getId re) 2063 "Grep uses regex, but this looks like a glob."
|
||||||
|
else potentially $ do
|
||||||
|
char <- getSuspiciousRegexWildcard string
|
||||||
|
return $ info (getId re) 2022 $
|
||||||
|
"Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'."
|
||||||
|
|
||||||
|
wordStartingWith c =
|
||||||
|
head . filter ([c] `isPrefixOf`) $ candidates
|
||||||
|
where
|
||||||
|
candidates =
|
||||||
|
sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords ++ [c:"test"]
|
||||||
|
|
||||||
|
getSuspiciousRegexWildcard str =
|
||||||
|
if not $ str `matches` contra
|
||||||
|
then do
|
||||||
|
match <- matchRegex suspicious str
|
||||||
|
str <- match !!! 0
|
||||||
|
str !!! 0
|
||||||
|
else
|
||||||
|
fail "looks good"
|
||||||
|
where
|
||||||
|
suspicious = mkRegex "([A-Za-z1-9])\\*"
|
||||||
|
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
|
||||||
|
prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT"
|
||||||
|
prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT"
|
||||||
|
prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
|
||||||
|
checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
|
||||||
|
f (x:_) = checkTrap x
|
||||||
|
f _ = return ()
|
||||||
|
checkTrap (T_NormalWord _ [T_DoubleQuoted _ rs]) = mapM_ checkExpansions rs
|
||||||
|
checkTrap _ = return ()
|
||||||
|
warning id = warn id 2064 "Use single quotes, otherwise this expands now rather than when signalled."
|
||||||
|
checkExpansions (T_DollarExpansion id _) = warning id
|
||||||
|
checkExpansions (T_Backticked id _) = warning id
|
||||||
|
checkExpansions (T_DollarBraced id _) = warning id
|
||||||
|
checkExpansions (T_DollarArithmetic id _) = warning id
|
||||||
|
checkExpansions _ = return ()
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkReturn1 = verifyNot checkReturn "return"
|
||||||
|
prop_checkReturn2 = verifyNot checkReturn "return 1"
|
||||||
|
prop_checkReturn3 = verifyNot checkReturn "return $var"
|
||||||
|
prop_checkReturn4 = verifyNot checkReturn "return $((a|b))"
|
||||||
|
prop_checkReturn5 = verify checkReturn "return -1"
|
||||||
|
prop_checkReturn6 = verify checkReturn "return 1000"
|
||||||
|
prop_checkReturn7 = verify checkReturn "return 'hello world'"
|
||||||
|
checkReturn = CommandCheck (Exactly "return") (f . arguments)
|
||||||
|
where
|
||||||
|
f (first:second:_) =
|
||||||
|
err (getId second) 2151
|
||||||
|
"Only one integer 0-255 can be returned. Use stdout for other data."
|
||||||
|
f [value] =
|
||||||
|
when (isInvalid $ literal value) $
|
||||||
|
err (getId value) 2152
|
||||||
|
"Can only return 0-255. Other data should be written to stdout."
|
||||||
|
f _ = return ()
|
||||||
|
|
||||||
|
isInvalid s = s == "" || any (not . isDigit) s || length s > 5
|
||||||
|
|| let value = (read s :: Integer) in value > 255
|
||||||
|
|
||||||
|
literal token = fromJust $ getLiteralStringExt lit token
|
||||||
|
lit (T_DollarBraced {}) = return "0"
|
||||||
|
lit (T_DollarArithmetic {}) = return "0"
|
||||||
|
lit (T_DollarExpansion {}) = return "0"
|
||||||
|
lit (T_Backticked {}) = return "0"
|
||||||
|
lit _ = return "WTF"
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkFindExecWithSingleArgument1 = verify checkFindExecWithSingleArgument "find . -exec 'cat {} | wc -l' \\;"
|
||||||
|
prop_checkFindExecWithSingleArgument2 = verify checkFindExecWithSingleArgument "find . -execdir 'cat {} | wc -l' +"
|
||||||
|
prop_checkFindExecWithSingleArgument3 = verifyNot checkFindExecWithSingleArgument "find . -exec wc -l {} \\;"
|
||||||
|
checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments)
|
||||||
|
where
|
||||||
|
f = void . sequence . mapMaybe check . tails
|
||||||
|
check (exec:arg:term:_) = do
|
||||||
|
execS <- getLiteralString exec
|
||||||
|
termS <- getLiteralString term
|
||||||
|
cmdS <- getLiteralStringExt (const $ return " ") arg
|
||||||
|
|
||||||
|
guard $ execS `elem` ["-exec", "-execdir"] && termS `elem` [";", "+"]
|
||||||
|
guard $ cmdS `matches` commandRegex
|
||||||
|
return $ warn (getId exec) 2150 "-exec does not invoke a shell. Rewrite or use -exec sh -c .. ."
|
||||||
|
check _ = Nothing
|
||||||
|
commandRegex = mkRegex "[ |;]"
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkUnusedEchoEscapes1 = verify checkUnusedEchoEscapes "echo 'foo\\nbar\\n'"
|
||||||
|
prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'"
|
||||||
|
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
|
||||||
|
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
|
||||||
|
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
|
||||||
|
checkUnusedEchoEscapes = CommandCheck (Basename "echo") (f . arguments)
|
||||||
|
where
|
||||||
|
isDashE = mkRegex "^-.*e"
|
||||||
|
hasEscapes = mkRegex "\\\\[rnt]"
|
||||||
|
f args | concat (concatMap oversimplify allButLast) `matches` isDashE =
|
||||||
|
return ()
|
||||||
|
where allButLast = reverse . drop 1 . reverse $ args
|
||||||
|
f args = mapM_ checkEscapes args
|
||||||
|
|
||||||
|
checkEscapes (T_NormalWord _ args) =
|
||||||
|
mapM_ checkEscapes args
|
||||||
|
checkEscapes (T_DoubleQuoted id args) =
|
||||||
|
mapM_ checkEscapes args
|
||||||
|
checkEscapes (T_Literal id str) = examine id str
|
||||||
|
checkEscapes (T_SingleQuoted id str) = examine id str
|
||||||
|
checkEscapes _ = return ()
|
||||||
|
|
||||||
|
examine id str =
|
||||||
|
when (str `matches` hasEscapes) $
|
||||||
|
info id 2028 "echo won't expand escape sequences. Consider printf."
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
|
||||||
|
prop_checkInjectableFindSh2 = verify checkInjectableFindSh "find . -execdir bash -c 'rm \"{}\"' ';'"
|
||||||
|
prop_checkInjectableFindSh3 = verifyNot checkInjectableFindSh "find . -exec sh -c 'rm \"$@\"' _ {} \\;"
|
||||||
|
checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments)
|
||||||
|
where
|
||||||
|
check args = do
|
||||||
|
let idStrings = map (\x -> (getId x, onlyLiteralString x)) args
|
||||||
|
match pattern idStrings
|
||||||
|
|
||||||
|
match _ [] = return ()
|
||||||
|
match [] (next:_) = action next
|
||||||
|
match (p:tests) ((id, arg):args) = do
|
||||||
|
when (p arg) $ match tests args
|
||||||
|
match (p:tests) args
|
||||||
|
|
||||||
|
pattern = [
|
||||||
|
(`elem` ["-exec", "-execdir"]),
|
||||||
|
(`elem` ["sh", "bash", "ksh"]),
|
||||||
|
(== "-c")
|
||||||
|
]
|
||||||
|
action (id, arg) =
|
||||||
|
when ("{}" `isInfixOf` arg) $
|
||||||
|
warn id 2156 "Injecting filenames is fragile and insecure. Use parameters."
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkFindActionPrecedence1 = verify checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au' -exec rm {} +"
|
||||||
|
prop_checkFindActionPrecedence2 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o \\( -name '*.au' -exec rm {} + \\)"
|
||||||
|
prop_checkFindActionPrecedence3 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au'"
|
||||||
|
checkFindActionPrecedence = CommandCheck (Basename "find") (f . arguments)
|
||||||
|
where
|
||||||
|
pattern = [isMatch, const True, isParam ["-o", "-or"], isMatch, const True, isAction]
|
||||||
|
f list | length list < length pattern = return ()
|
||||||
|
f list@(_:rest) =
|
||||||
|
if and (zipWith ($) pattern list)
|
||||||
|
then warnFor (list !! (length pattern - 1))
|
||||||
|
else f rest
|
||||||
|
isMatch = isParam [ "-name", "-regex", "-iname", "-iregex", "-wholename", "-iwholename" ]
|
||||||
|
isAction = isParam [ "-exec", "-execdir", "-delete", "-print", "-print0" ]
|
||||||
|
isParam strs t = fromMaybe False $ do
|
||||||
|
param <- getLiteralString t
|
||||||
|
return $ param `elem` strs
|
||||||
|
warnFor t = warn (getId t) 2146 "This action ignores everything before the -o. Use \\( \\) to group."
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkMkdirDashPM0 = verify checkMkdirDashPM "mkdir -p -m 0755 a/b"
|
||||||
|
prop_checkMkdirDashPM1 = verify checkMkdirDashPM "mkdir -pm 0755 $dir"
|
||||||
|
prop_checkMkdirDashPM2 = verify checkMkdirDashPM "mkdir -vpm 0755 a/b"
|
||||||
|
prop_checkMkdirDashPM3 = verify checkMkdirDashPM "mkdir -pm 0755 -v a/b"
|
||||||
|
prop_checkMkdirDashPM4 = verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b"
|
||||||
|
prop_checkMkdirDashPM5 = verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b"
|
||||||
|
prop_checkMkdirDashPM6 = verify checkMkdirDashPM "mkdir -p --mode=0755 a/b"
|
||||||
|
prop_checkMkdirDashPM7 = verify checkMkdirDashPM "mkdir --parents -m 0755 a/b"
|
||||||
|
prop_checkMkdirDashPM8 = verifyNot checkMkdirDashPM "mkdir -p a/b"
|
||||||
|
prop_checkMkdirDashPM9 = verifyNot checkMkdirDashPM "mkdir -m 0755 a/b"
|
||||||
|
prop_checkMkdirDashPM10 = verifyNot checkMkdirDashPM "mkdir a/b"
|
||||||
|
prop_checkMkdirDashPM11 = verifyNot checkMkdirDashPM "mkdir --parents a/b"
|
||||||
|
prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b"
|
||||||
|
prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b"
|
||||||
|
prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"
|
||||||
|
checkMkdirDashPM = CommandCheck (Basename "mkdir") check
|
||||||
|
where
|
||||||
|
check t = potentially $ do
|
||||||
|
let flags = getAllFlags t
|
||||||
|
dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
|
||||||
|
dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
|
||||||
|
guard $ any couldHaveSubdirs (drop 1 $ arguments t) -- mkdir -pm 0700 dir is fine, but dir/subdir is not.
|
||||||
|
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
|
||||||
|
couldHaveSubdirs t = fromMaybe True $ do
|
||||||
|
name <- getLiteralString t
|
||||||
|
return $ '/' `elem` name
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8"
|
||||||
|
prop_checkNonportableSignals2 = verifyNot checkNonportableSignals "trap f 0"
|
||||||
|
prop_checkNonportableSignals3 = verifyNot checkNonportableSignals "trap f 14"
|
||||||
|
prop_checkNonportableSignals4 = verify checkNonportableSignals "trap f SIGKILL"
|
||||||
|
prop_checkNonportableSignals5 = verify checkNonportableSignals "trap f 9"
|
||||||
|
prop_checkNonportableSignals6 = verify checkNonportableSignals "trap f stop"
|
||||||
|
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
|
||||||
|
where
|
||||||
|
f = mapM_ check
|
||||||
|
check param = potentially $ do
|
||||||
|
str <- getLiteralString param
|
||||||
|
let id = getId param
|
||||||
|
return $ sequence_ $ mapMaybe (\f -> f id str) [
|
||||||
|
checkNumeric,
|
||||||
|
checkUntrappable
|
||||||
|
]
|
||||||
|
|
||||||
|
checkNumeric id str = do
|
||||||
|
guard $ not (null str)
|
||||||
|
guard $ all isDigit str
|
||||||
|
guard $ str /= "0" -- POSIX exit trap
|
||||||
|
guard $ str `notElem` ["1", "2", "3", "6", "9", "14", "15" ] -- XSI
|
||||||
|
return $ warn id 2172
|
||||||
|
"Trapping signals by number is not well defined. Prefer signal names."
|
||||||
|
|
||||||
|
checkUntrappable id str = do
|
||||||
|
guard $ map toLower str `elem` ["kill", "9", "sigkill", "stop", "sigstop"]
|
||||||
|
return $ err id 2173
|
||||||
|
"SIGKILL/SIGSTOP can not be trapped."
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkInteractiveSu1 = verify checkInteractiveSu "su; rm file; su $USER"
|
||||||
|
prop_checkInteractiveSu2 = verify checkInteractiveSu "su foo; something; exit"
|
||||||
|
prop_checkInteractiveSu3 = verifyNot checkInteractiveSu "echo rm | su foo"
|
||||||
|
prop_checkInteractiveSu4 = verifyNot checkInteractiveSu "su root < script"
|
||||||
|
checkInteractiveSu = CommandCheck (Basename "su") f
|
||||||
|
where
|
||||||
|
f cmd = when (length (arguments cmd) <= 1) $ do
|
||||||
|
path <- pathTo cmd
|
||||||
|
when (all undirected path) $
|
||||||
|
info (getId cmd) 2117
|
||||||
|
"To run commands as another user, use su -c or sudo."
|
||||||
|
|
||||||
|
undirected (T_Pipeline _ _ l) = length l <= 1
|
||||||
|
-- This should really just be modifications to stdin, but meh
|
||||||
|
undirected (T_Redirecting _ list _) = null list
|
||||||
|
undirected _ = True
|
||||||
|
|
||||||
|
|
||||||
|
-- This is hard to get right without properly parsing ssh args
|
||||||
|
prop_checkSshCmdStr1 = verify checkSshCommandString "ssh host \"echo $PS1\""
|
||||||
|
prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\""
|
||||||
|
prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""
|
||||||
|
checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
|
||||||
|
where
|
||||||
|
nonOptions =
|
||||||
|
filter (\x -> not $ "-" `isPrefixOf` concat (oversimplify x))
|
||||||
|
f args =
|
||||||
|
case nonOptions args of
|
||||||
|
(hostport:r@(_:_)) -> checkArg $ last r
|
||||||
|
_ -> return ()
|
||||||
|
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
|
||||||
|
case filter (not . isConstant) parts of
|
||||||
|
[] -> return ()
|
||||||
|
(x:_) -> info (getId x) 2029
|
||||||
|
"Note that, unescaped, this expands on the client side."
|
||||||
|
checkArg _ = return ()
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkPrintfVar1 = verify checkPrintfVar "printf \"Lol: $s\""
|
||||||
|
prop_checkPrintfVar2 = verifyNot checkPrintfVar "printf 'Lol: $s'"
|
||||||
|
prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)"
|
||||||
|
prop_checkPrintfVar4 = verifyNot checkPrintfVar "printf \"%${count}s\" var"
|
||||||
|
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||||
|
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
|
||||||
|
f (format:params) = check format
|
||||||
|
f _ = return ()
|
||||||
|
check format =
|
||||||
|
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
|
||||||
|
warn (getId format) 2059
|
||||||
|
"Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"."
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)"
|
||||||
|
prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`"
|
||||||
|
prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\""
|
||||||
|
prop_checkUuoeCmd4 = verify checkUuoeCmd "echo \"`date`\""
|
||||||
|
prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\""
|
||||||
|
prop_checkUuoeCmd6 = verifyNot checkUuoeCmd "echo \"$(<file)\""
|
||||||
|
checkUuoeCmd = CommandCheck (Exactly "echo") (f . arguments) where
|
||||||
|
msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'."
|
||||||
|
f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token)
|
||||||
|
f _ = return ()
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkSetAssignment1 = verify checkSetAssignment "set foo 42"
|
||||||
|
prop_checkSetAssignment2 = verify checkSetAssignment "set foo = 42"
|
||||||
|
prop_checkSetAssignment3 = verify checkSetAssignment "set foo=42"
|
||||||
|
prop_checkSetAssignment4 = verifyNot checkSetAssignment "set -- if=/dev/null"
|
||||||
|
prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'"
|
||||||
|
prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"
|
||||||
|
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
|
||||||
|
where
|
||||||
|
f (var:value:rest) =
|
||||||
|
let str = literal var in
|
||||||
|
when (isVariableName str || isAssignment str) $
|
||||||
|
msg (getId var)
|
||||||
|
f (var:_) =
|
||||||
|
when (isAssignment $ literal var) $
|
||||||
|
msg (getId var)
|
||||||
|
f _ = return ()
|
||||||
|
|
||||||
|
msg id = warn id 2121 "To assign a variable, use just 'var=value', no 'set ..'."
|
||||||
|
|
||||||
|
isAssignment str = '=' `elem` str
|
||||||
|
literal (T_NormalWord _ l) = concatMap literal l
|
||||||
|
literal (T_Literal _ str) = str
|
||||||
|
literal _ = "*"
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkExportedExpansions1 = verify checkExportedExpansions "export $foo"
|
||||||
|
prop_checkExportedExpansions2 = verify checkExportedExpansions "export \"$foo\""
|
||||||
|
prop_checkExportedExpansions3 = verifyNot checkExportedExpansions "export foo"
|
||||||
|
checkExportedExpansions = CommandCheck (Exactly "export") (check . arguments)
|
||||||
|
where
|
||||||
|
check = mapM_ checkForVariables
|
||||||
|
checkForVariables f =
|
||||||
|
case getWordParts f of
|
||||||
|
[t@(T_DollarBraced {})] ->
|
||||||
|
warn (getId t) 2163 "Exporting an expansion rather than a variable."
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
|
||||||
|
prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'"
|
||||||
|
prop_checkAliasesUsesArgs3 = verify checkAliasesUsesArgs "alias a=\"echo \\${@}\""
|
||||||
|
checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments)
|
||||||
|
where
|
||||||
|
re = mkRegex "\\$\\{?[0-9*@]"
|
||||||
|
f = mapM_ checkArg
|
||||||
|
checkArg arg =
|
||||||
|
let string = fromJust $ getLiteralStringExt (const $ return "_") arg in
|
||||||
|
when ('=' `elem` string && string `matches` re) $
|
||||||
|
err (getId arg) 2142
|
||||||
|
"Aliases can't use positional parameters. Use a function."
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkAliasesExpandEarly1 = verify checkAliasesExpandEarly "alias foo=\"echo $PWD\""
|
||||||
|
prop_checkAliasesExpandEarly2 = verifyNot checkAliasesExpandEarly "alias -p"
|
||||||
|
prop_checkAliasesExpandEarly3 = verifyNot checkAliasesExpandEarly "alias foo='echo {1..10}'"
|
||||||
|
checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
|
||||||
|
where
|
||||||
|
f = mapM_ checkArg
|
||||||
|
checkArg arg | '=' `elem` concat (oversimplify arg) =
|
||||||
|
forM_ (take 1 $ filter (not . isLiteral) $ getWordParts arg) $
|
||||||
|
\x -> warn (getId x) 2139 "This expands when defined, not when used. Consider escaping."
|
||||||
|
checkArg _ = return ()
|
||||||
|
|
||||||
|
|
||||||
|
return []
|
||||||
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -1,6 +1,10 @@
|
|||||||
module ShellCheck.Data where
|
module ShellCheck.Data where
|
||||||
|
|
||||||
shellcheckVersion = "0.3.2" -- Must also be updated in ShellCheck.cabal
|
import ShellCheck.Interface
|
||||||
|
import Data.Version (showVersion)
|
||||||
|
import Paths_ShellCheck (version)
|
||||||
|
|
||||||
|
shellcheckVersion = showVersion version
|
||||||
|
|
||||||
internalVariables = [
|
internalVariables = [
|
||||||
-- Generic
|
-- Generic
|
||||||
@@ -21,25 +25,14 @@ internalVariables = [
|
|||||||
"FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",
|
"FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",
|
||||||
"HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",
|
"HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",
|
||||||
"IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
|
"IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
|
||||||
"LC_MESSAGES", "LC_NUMERIC", "LINES", "MAIL", "MAILCHECK", "MAILPATH",
|
"LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LINES", "MAIL",
|
||||||
"OPTERR", "PATH", "POSIXLY_CORRECT", "PROMPT_COMMAND",
|
"MAILCHECK", "MAILPATH", "OPTERR", "PATH", "POSIXLY_CORRECT",
|
||||||
"PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT",
|
"PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL",
|
||||||
"TMOUT", "TMPDIR", "auto_resume", "histchars",
|
"TIMEFORMAT", "TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC",
|
||||||
|
|
||||||
-- Zsh
|
-- Other
|
||||||
"ARGV0", "BAUD", "cdpath", "COLUMNS", "CORRECT_IGNORE",
|
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
|
||||||
"DIRSTACKSIZE", "ENV", "FCEDIT", "fignore", "fpath", "histchars",
|
"HOSTNAME", "KRB5CCNAME", "XAUTHORITY"
|
||||||
"HISTCHARS", "HISTFILE", "HISTSIZE", "HOME", "IFS", "KEYBOARD_HACK",
|
|
||||||
"KEYTIMEOUT", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
|
|
||||||
"LC_MESSAGES", "LC_NUMERIC", "LC_TIME", "LINES", "LISTMAX",
|
|
||||||
"LOGCHECK", "MAIL", "MAILCHECK", "mailpath", "manpath", "module_path",
|
|
||||||
"NULLCMD", "path", "POSTEDIT", "PROMPT", "PROMPT2", "PROMPT3",
|
|
||||||
"PROMPT4", "prompt", "PROMPT_EOL_MARK", "PS1", "PS2", "PS3", "PS4",
|
|
||||||
"psvar", "READNULLCMD", "REPORTTIME", "REPLY", "reply", "RPROMPT",
|
|
||||||
"RPS1", "RPROMPT2", "RPS2", "SAVEHIST", "SPROMPT", "STTY", "TERM",
|
|
||||||
"TERMINFO", "TIMEFMT", "TMOUT", "TMPPREFIX", "watch", "WATCHFMT",
|
|
||||||
"WORDCHARS", "ZBEEP", "ZDOTDIR", "ZLE_LINE_ABORTED",
|
|
||||||
"ZLE_REMOVE_SUFFIX_CHARS", "ZLE_SPACE_SUFFIX_CHARS"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
variablesWithoutSpaces = [
|
variablesWithoutSpaces = [
|
||||||
@@ -49,6 +42,12 @@ variablesWithoutSpaces = [
|
|||||||
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
|
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
arrayVariables = [
|
||||||
|
"BASH_ALIASES", "BASH_ARGC", "BASH_ARGV", "BASH_CMDS", "BASH_LINENO",
|
||||||
|
"BASH_REMATCH", "BASH_SOURCE", "BASH_VERSINFO", "COMP_WORDS", "COPROC",
|
||||||
|
"DIRSTACK", "FUNCNAME", "GROUPS", "MAPFILE", "PIPESTATUS", "COMPREPLY"
|
||||||
|
]
|
||||||
|
|
||||||
commonCommands = [
|
commonCommands = [
|
||||||
"admin", "alias", "ar", "asa", "at", "awk", "basename", "batch",
|
"admin", "alias", "ar", "asa", "at", "awk", "basename", "batch",
|
||||||
"bc", "bg", "break", "c99", "cal", "cat", "cd", "cflow", "chgrp",
|
"bc", "bg", "break", "c99", "cal", "cat", "cd", "cflow", "chgrp",
|
||||||
@@ -74,3 +73,23 @@ commonCommands = [
|
|||||||
"val", "vi", "wait", "wc", "what", "who", "write", "xargs", "yacc",
|
"val", "vi", "wait", "wc", "what", "who", "write", "xargs", "yacc",
|
||||||
"zcat"
|
"zcat"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
sampleWords = [
|
||||||
|
"alpha", "bravo", "charlie", "delta", "echo", "foxtrot",
|
||||||
|
"golf", "hotel", "india", "juliett", "kilo", "lima", "mike",
|
||||||
|
"november", "oscar", "papa", "quebec", "romeo", "sierra",
|
||||||
|
"tango", "uniform", "victor", "whiskey", "xray", "yankee",
|
||||||
|
"zulu"
|
||||||
|
]
|
||||||
|
|
||||||
|
shellForExecutable :: String -> Maybe Shell
|
||||||
|
shellForExecutable name =
|
||||||
|
case name of
|
||||||
|
"sh" -> return Sh
|
||||||
|
"bash" -> return Bash
|
||||||
|
"bats" -> return Bash
|
||||||
|
"dash" -> return Dash
|
||||||
|
"ksh" -> return Ksh
|
||||||
|
"ksh88" -> return Ksh
|
||||||
|
"ksh93" -> return Ksh
|
||||||
|
otherwise -> Nothing
|
||||||
|
82
ShellCheck/Formatter/CheckStyle.hs
Normal file
82
ShellCheck/Formatter/CheckStyle.hs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
{-
|
||||||
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
|
This file is part of ShellCheck.
|
||||||
|
http://www.vidarholen.net/contents/shellcheck
|
||||||
|
|
||||||
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
ShellCheck is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-}
|
||||||
|
module ShellCheck.Formatter.CheckStyle (format) where
|
||||||
|
|
||||||
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Formatter.Format
|
||||||
|
|
||||||
|
import Data.Char
|
||||||
|
import Data.List
|
||||||
|
import GHC.Exts
|
||||||
|
import System.IO
|
||||||
|
|
||||||
|
format :: IO Formatter
|
||||||
|
format = return Formatter {
|
||||||
|
header = do
|
||||||
|
putStrLn "<?xml version='1.0' encoding='UTF-8'?>"
|
||||||
|
putStrLn "<checkstyle version='4.3'>",
|
||||||
|
|
||||||
|
onFailure = outputError,
|
||||||
|
onResult = outputResult,
|
||||||
|
|
||||||
|
footer = putStrLn "</checkstyle>"
|
||||||
|
}
|
||||||
|
|
||||||
|
outputResult result contents = do
|
||||||
|
let comments = makeNonVirtual (crComments result) contents
|
||||||
|
putStrLn . formatFile (crFilename result) $ comments
|
||||||
|
|
||||||
|
formatFile name comments = concat [
|
||||||
|
"<file ", attr "name" name, ">\n",
|
||||||
|
concatMap formatComment comments,
|
||||||
|
"</file>"
|
||||||
|
]
|
||||||
|
|
||||||
|
formatComment c = concat [
|
||||||
|
"<error ",
|
||||||
|
attr "line" $ show . lineNo $ c,
|
||||||
|
attr "column" $ show . colNo $ c,
|
||||||
|
attr "severity" . severity $ severityText c,
|
||||||
|
attr "message" $ messageText c,
|
||||||
|
attr "source" $ "ShellCheck.SC" ++ show (codeNo c),
|
||||||
|
"/>\n"
|
||||||
|
]
|
||||||
|
|
||||||
|
outputError file error = putStrLn $ concat [
|
||||||
|
"<file ", attr "name" file, ">\n",
|
||||||
|
"<error ",
|
||||||
|
attr "line" "1",
|
||||||
|
attr "column" "1",
|
||||||
|
attr "severity" "error",
|
||||||
|
attr "message" error,
|
||||||
|
attr "source" "ShellCheck",
|
||||||
|
"/>\n",
|
||||||
|
"</file>"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
attr s v = concat [ s, "='", escape v, "' " ]
|
||||||
|
escape = concatMap escape'
|
||||||
|
escape' c = if isOk c then [c] else "&#" ++ show (ord c) ++ ";"
|
||||||
|
isOk x = any ($x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")]
|
||||||
|
|
||||||
|
severity "error" = "error"
|
||||||
|
severity "warning" = "warning"
|
||||||
|
severity _ = "info"
|
61
ShellCheck/Formatter/Format.hs
Normal file
61
ShellCheck/Formatter/Format.hs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{-
|
||||||
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
|
This file is part of ShellCheck.
|
||||||
|
http://www.vidarholen.net/contents/shellcheck
|
||||||
|
|
||||||
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
ShellCheck is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-}
|
||||||
|
module ShellCheck.Formatter.Format where
|
||||||
|
|
||||||
|
import ShellCheck.Data
|
||||||
|
import ShellCheck.Interface
|
||||||
|
|
||||||
|
-- A formatter that carries along an arbitrary piece of data
|
||||||
|
data Formatter = Formatter {
|
||||||
|
header :: IO (),
|
||||||
|
onResult :: CheckResult -> String -> IO (),
|
||||||
|
onFailure :: FilePath -> ErrorMessage -> IO (),
|
||||||
|
footer :: IO ()
|
||||||
|
}
|
||||||
|
|
||||||
|
lineNo (PositionedComment pos _) = posLine pos
|
||||||
|
colNo (PositionedComment pos _) = posColumn pos
|
||||||
|
codeNo (PositionedComment _ (Comment _ code _)) = code
|
||||||
|
messageText (PositionedComment _ (Comment _ _ t)) = t
|
||||||
|
|
||||||
|
severityText :: PositionedComment -> String
|
||||||
|
severityText (PositionedComment _ (Comment c _ _)) =
|
||||||
|
case c of
|
||||||
|
ErrorC -> "error"
|
||||||
|
WarningC -> "warning"
|
||||||
|
InfoC -> "info"
|
||||||
|
StyleC -> "style"
|
||||||
|
|
||||||
|
-- Realign comments from a tabstop of 8 to 1
|
||||||
|
makeNonVirtual comments contents =
|
||||||
|
map fix comments
|
||||||
|
where
|
||||||
|
ls = lines contents
|
||||||
|
fix c@(PositionedComment pos comment) = PositionedComment pos {
|
||||||
|
posColumn =
|
||||||
|
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
|
||||||
|
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
|
||||||
|
else colNo c
|
||||||
|
} comment
|
||||||
|
real _ r v target | target <= v = r
|
||||||
|
real [] r v _ = r -- should never happen
|
||||||
|
real ('\t':rest) r v target =
|
||||||
|
real rest (r+1) (v + 8 - (v `mod` 8)) target
|
||||||
|
real (_:rest) r v target = real rest (r+1) (v+1) target
|
54
ShellCheck/Formatter/GCC.hs
Normal file
54
ShellCheck/Formatter/GCC.hs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{-
|
||||||
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
|
This file is part of ShellCheck.
|
||||||
|
http://www.vidarholen.net/contents/shellcheck
|
||||||
|
|
||||||
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
ShellCheck is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-}
|
||||||
|
module ShellCheck.Formatter.GCC (format) where
|
||||||
|
|
||||||
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Formatter.Format
|
||||||
|
|
||||||
|
import Data.List
|
||||||
|
import GHC.Exts
|
||||||
|
import System.IO
|
||||||
|
|
||||||
|
format :: IO Formatter
|
||||||
|
format = return Formatter {
|
||||||
|
header = return (),
|
||||||
|
footer = return (),
|
||||||
|
onFailure = outputError,
|
||||||
|
onResult = outputResult
|
||||||
|
}
|
||||||
|
|
||||||
|
outputError file error = hPutStrLn stderr $ file ++ ": " ++ error
|
||||||
|
|
||||||
|
outputResult result contents = do
|
||||||
|
let comments = makeNonVirtual (crComments result) contents
|
||||||
|
mapM_ (putStrLn . formatComment (crFilename result)) comments
|
||||||
|
|
||||||
|
formatComment filename c = concat [
|
||||||
|
filename, ":",
|
||||||
|
show $ lineNo c, ":",
|
||||||
|
show $ colNo c, ": ",
|
||||||
|
case severityText c of
|
||||||
|
"error" -> "error"
|
||||||
|
"warning" -> "warning"
|
||||||
|
_ -> "note",
|
||||||
|
": ",
|
||||||
|
concat . lines $ messageText c,
|
||||||
|
" [SC", show $ codeNo c, "]"
|
||||||
|
]
|
58
ShellCheck/Formatter/JSON.hs
Normal file
58
ShellCheck/Formatter/JSON.hs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{-
|
||||||
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
|
This file is part of ShellCheck.
|
||||||
|
http://www.vidarholen.net/contents/shellcheck
|
||||||
|
|
||||||
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
ShellCheck is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-}
|
||||||
|
module ShellCheck.Formatter.JSON (format) where
|
||||||
|
|
||||||
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Formatter.Format
|
||||||
|
|
||||||
|
import Data.IORef
|
||||||
|
import GHC.Exts
|
||||||
|
import System.IO
|
||||||
|
import Text.JSON
|
||||||
|
|
||||||
|
format = do
|
||||||
|
ref <- newIORef []
|
||||||
|
return Formatter {
|
||||||
|
header = return (),
|
||||||
|
onResult = collectResult ref,
|
||||||
|
onFailure = outputError,
|
||||||
|
footer = finish ref
|
||||||
|
}
|
||||||
|
|
||||||
|
instance JSON (PositionedComment) where
|
||||||
|
showJSON comment@(PositionedComment pos (Comment level code string)) = makeObj [
|
||||||
|
("file", showJSON $ posFile pos),
|
||||||
|
("line", showJSON $ posLine pos),
|
||||||
|
("column", showJSON $ posColumn pos),
|
||||||
|
("level", showJSON $ severityText comment),
|
||||||
|
("code", showJSON code),
|
||||||
|
("message", showJSON string)
|
||||||
|
]
|
||||||
|
|
||||||
|
readJSON = undefined
|
||||||
|
|
||||||
|
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
|
||||||
|
collectResult ref result _ =
|
||||||
|
modifyIORef ref (\x -> crComments result ++ x)
|
||||||
|
|
||||||
|
finish ref = do
|
||||||
|
list <- readIORef ref
|
||||||
|
putStrLn $ encodeStrict list
|
||||||
|
|
91
ShellCheck/Formatter/TTY.hs
Normal file
91
ShellCheck/Formatter/TTY.hs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
{-
|
||||||
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
|
This file is part of ShellCheck.
|
||||||
|
http://www.vidarholen.net/contents/shellcheck
|
||||||
|
|
||||||
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
ShellCheck is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-}
|
||||||
|
module ShellCheck.Formatter.TTY (format) where
|
||||||
|
|
||||||
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Formatter.Format
|
||||||
|
|
||||||
|
import Data.List
|
||||||
|
import GHC.Exts
|
||||||
|
import System.Info
|
||||||
|
import System.IO
|
||||||
|
|
||||||
|
format :: FormatterOptions -> IO Formatter
|
||||||
|
format options = return Formatter {
|
||||||
|
header = return (),
|
||||||
|
footer = return (),
|
||||||
|
onFailure = outputError options,
|
||||||
|
onResult = outputResult options
|
||||||
|
}
|
||||||
|
|
||||||
|
colorForLevel level =
|
||||||
|
case level of
|
||||||
|
"error" -> 31 -- red
|
||||||
|
"warning" -> 33 -- yellow
|
||||||
|
"info" -> 32 -- green
|
||||||
|
"style" -> 32 -- green
|
||||||
|
"message" -> 1 -- bold
|
||||||
|
"source" -> 0 -- none
|
||||||
|
otherwise -> 0 -- none
|
||||||
|
|
||||||
|
outputError options file error = do
|
||||||
|
color <- getColorFunc $ foColorOption options
|
||||||
|
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
|
||||||
|
|
||||||
|
outputResult options result contents = do
|
||||||
|
color <- getColorFunc $ foColorOption options
|
||||||
|
let comments = crComments result
|
||||||
|
let fileLines = lines contents
|
||||||
|
let lineCount = fromIntegral $ length fileLines
|
||||||
|
let groups = groupWith lineNo comments
|
||||||
|
mapM_ (\x -> do
|
||||||
|
let lineNum = lineNo (head x)
|
||||||
|
let line = if lineNum < 1 || lineNum > lineCount
|
||||||
|
then ""
|
||||||
|
else fileLines !! fromIntegral (lineNum - 1)
|
||||||
|
putStrLn ""
|
||||||
|
putStrLn $ color "message" $
|
||||||
|
"In " ++ crFilename result ++" line " ++ show lineNum ++ ":"
|
||||||
|
putStrLn (color "source" line)
|
||||||
|
mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) x
|
||||||
|
putStrLn ""
|
||||||
|
) groups
|
||||||
|
|
||||||
|
cuteIndent :: PositionedComment -> String
|
||||||
|
cuteIndent comment =
|
||||||
|
replicate (fromIntegral $ colNo comment - 1) ' ' ++
|
||||||
|
"^-- " ++ code (codeNo comment) ++ ": " ++ messageText comment
|
||||||
|
|
||||||
|
code code = "SC" ++ show code
|
||||||
|
|
||||||
|
getColorFunc colorOption = do
|
||||||
|
term <- hIsTerminalDevice stdout
|
||||||
|
let windows = "mingw" `isPrefixOf` os
|
||||||
|
let isUsableTty = term && not windows
|
||||||
|
let useColor = case colorOption of
|
||||||
|
ColorAlways -> True
|
||||||
|
ColorNever -> False
|
||||||
|
ColorAuto -> isUsableTty
|
||||||
|
return $ if useColor then colorComment else const id
|
||||||
|
where
|
||||||
|
colorComment level comment =
|
||||||
|
ansi (colorForLevel level) ++ comment ++ clear
|
||||||
|
clear = ansi 0
|
||||||
|
ansi n = "\x1B[" ++ show n ++ "m"
|
116
ShellCheck/Interface.hs
Normal file
116
ShellCheck/Interface.hs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
{-
|
||||||
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
|
This file is part of ShellCheck.
|
||||||
|
http://www.vidarholen.net/contents/shellcheck
|
||||||
|
|
||||||
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
ShellCheck is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-}
|
||||||
|
module ShellCheck.Interface where
|
||||||
|
|
||||||
|
import ShellCheck.AST
|
||||||
|
import Control.Monad.Identity
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
|
|
||||||
|
data SystemInterface m = SystemInterface {
|
||||||
|
-- Read a file by filename, or return an error
|
||||||
|
siReadFile :: String -> m (Either ErrorMessage String)
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ShellCheck input and output
|
||||||
|
data CheckSpec = CheckSpec {
|
||||||
|
csFilename :: String,
|
||||||
|
csScript :: String,
|
||||||
|
csExcludedWarnings :: [Integer],
|
||||||
|
csShellTypeOverride :: Maybe Shell
|
||||||
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
data CheckResult = CheckResult {
|
||||||
|
crFilename :: String,
|
||||||
|
crComments :: [PositionedComment]
|
||||||
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
emptyCheckSpec = CheckSpec {
|
||||||
|
csFilename = "",
|
||||||
|
csScript = "",
|
||||||
|
csExcludedWarnings = [],
|
||||||
|
csShellTypeOverride = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Parser input and output
|
||||||
|
data ParseSpec = ParseSpec {
|
||||||
|
psFilename :: String,
|
||||||
|
psScript :: String
|
||||||
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
data ParseResult = ParseResult {
|
||||||
|
prComments :: [PositionedComment],
|
||||||
|
prTokenPositions :: Map.Map Id Position,
|
||||||
|
prRoot :: Maybe Token
|
||||||
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
-- Analyzer input and output
|
||||||
|
data AnalysisSpec = AnalysisSpec {
|
||||||
|
asScript :: Token,
|
||||||
|
asShellType :: Maybe Shell,
|
||||||
|
asExecutionMode :: ExecutionMode
|
||||||
|
}
|
||||||
|
|
||||||
|
data AnalysisResult = AnalysisResult {
|
||||||
|
arComments :: [TokenComment]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
-- Formatter options
|
||||||
|
data FormatterOptions = FormatterOptions {
|
||||||
|
foColorOption :: ColorOption
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
-- Supporting data types
|
||||||
|
data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
|
||||||
|
data ExecutionMode = Executed | Sourced deriving (Show, Eq)
|
||||||
|
|
||||||
|
type ErrorMessage = String
|
||||||
|
type Code = Integer
|
||||||
|
|
||||||
|
data Severity = ErrorC | WarningC | InfoC | StyleC deriving (Show, Eq, Ord)
|
||||||
|
data Position = Position {
|
||||||
|
posFile :: String, -- Filename
|
||||||
|
posLine :: Integer, -- 1 based source line
|
||||||
|
posColumn :: Integer -- 1 based source column, where tabs are 8
|
||||||
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
data Comment = Comment Severity Code String deriving (Show, Eq)
|
||||||
|
data PositionedComment = PositionedComment Position Comment deriving (Show, Eq)
|
||||||
|
data TokenComment = TokenComment Id Comment deriving (Show, Eq)
|
||||||
|
|
||||||
|
data ColorOption =
|
||||||
|
ColorAuto
|
||||||
|
| ColorAlways
|
||||||
|
| ColorNever
|
||||||
|
deriving (Ord, Eq, Show)
|
||||||
|
|
||||||
|
-- For testing
|
||||||
|
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
|
||||||
|
mockedSystemInterface files = SystemInterface {
|
||||||
|
siReadFile = rf
|
||||||
|
}
|
||||||
|
where
|
||||||
|
rf file =
|
||||||
|
case filter ((== file) . fst) files of
|
||||||
|
[] -> return $ Left "File not included in mock."
|
||||||
|
[(_, contents)] -> return $ Right contents
|
||||||
|
|
1571
ShellCheck/Parser.hs
1571
ShellCheck/Parser.hs
File diff suppressed because it is too large
Load Diff
80
ShellCheck/Regex.hs
Normal file
80
ShellCheck/Regex.hs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
{-
|
||||||
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
|
This file is part of ShellCheck.
|
||||||
|
http://www.vidarholen.net/contents/shellcheck
|
||||||
|
|
||||||
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
ShellCheck is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
|
||||||
|
-- Basically Text.Regex based on regex-tdfa instead of the buggy regex-posix.
|
||||||
|
module ShellCheck.Regex where
|
||||||
|
|
||||||
|
import Data.List
|
||||||
|
import Data.Maybe
|
||||||
|
import Control.Monad
|
||||||
|
import Text.Regex.TDFA
|
||||||
|
|
||||||
|
-- Precompile the regex
|
||||||
|
mkRegex :: String -> Regex
|
||||||
|
mkRegex str =
|
||||||
|
let make :: RegexMaker Regex CompOption ExecOption String => String -> Regex
|
||||||
|
make = makeRegex
|
||||||
|
in
|
||||||
|
make str
|
||||||
|
|
||||||
|
-- Does the regex match?
|
||||||
|
matches :: String -> Regex -> Bool
|
||||||
|
matches = flip match
|
||||||
|
|
||||||
|
-- Get all subgroups of the first match
|
||||||
|
matchRegex :: Regex -> String -> Maybe [String]
|
||||||
|
matchRegex re str = do
|
||||||
|
(_, _, _, groups) <- matchM re str :: Maybe (String,String,String,[String])
|
||||||
|
return groups
|
||||||
|
|
||||||
|
-- Get all full matches
|
||||||
|
matchAllStrings :: Regex -> String -> [String]
|
||||||
|
matchAllStrings re = unfoldr f
|
||||||
|
where
|
||||||
|
f :: String -> Maybe (String, String)
|
||||||
|
f str = do
|
||||||
|
(_, match, rest, _) <- matchM re str :: Maybe (String, String, String, [String])
|
||||||
|
return (match, rest)
|
||||||
|
|
||||||
|
-- Get all subgroups from all matches
|
||||||
|
matchAllSubgroups :: Regex -> String -> [[String]]
|
||||||
|
matchAllSubgroups re = unfoldr f
|
||||||
|
where
|
||||||
|
f :: String -> Maybe ([String], String)
|
||||||
|
f str = do
|
||||||
|
(_, _, rest, groups) <- matchM re str :: Maybe (String, String, String, [String])
|
||||||
|
return (groups, rest)
|
||||||
|
|
||||||
|
-- Replace regex in input with string
|
||||||
|
subRegex :: Regex -> String -> String -> String
|
||||||
|
subRegex re input replacement = f input
|
||||||
|
where
|
||||||
|
f str = fromMaybe str $ do
|
||||||
|
(before, match, after) <- matchM re str :: Maybe (String, String, String)
|
||||||
|
when (null match) $ error ("Internal error: substituted empty in " ++ str)
|
||||||
|
return $ before ++ replacement ++ f after
|
||||||
|
|
||||||
|
-- Split a string based on a regex.
|
||||||
|
splitOn :: String -> Regex -> [String]
|
||||||
|
splitOn input re =
|
||||||
|
case matchM re input :: Maybe (String, String, String) of
|
||||||
|
Just (before, match, after) -> before : after `splitOn` re
|
||||||
|
Nothing -> [input]
|
@@ -1,67 +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, scCode, scMessage) where
|
|
||||||
|
|
||||||
import ShellCheck.Parser
|
|
||||||
import ShellCheck.Analytics
|
|
||||||
import Data.Maybe
|
|
||||||
import Text.Parsec.Pos
|
|
||||||
import Data.List
|
|
||||||
|
|
||||||
|
|
||||||
prop_findsParseIssue =
|
|
||||||
let comments = shellCheck "echo \"$12\"" [] in
|
|
||||||
(length comments) == 1 && (scCode $ head comments) == 1037
|
|
||||||
prop_commentDisablesParseIssue1 =
|
|
||||||
null $ shellCheck "#shellcheck disable=SC1037\necho \"$12\"" []
|
|
||||||
prop_commentDisablesParseIssue2 =
|
|
||||||
null $ shellCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\"" []
|
|
||||||
|
|
||||||
prop_findsAnalysisIssue =
|
|
||||||
let comments = shellCheck "echo $1" [] in
|
|
||||||
(length comments) == 1 && (scCode $ head comments) == 2086
|
|
||||||
prop_commentDisablesAnalysisIssue1 =
|
|
||||||
null $ shellCheck "#shellcheck disable=SC2086\necho $1" []
|
|
||||||
prop_commentDisablesAnalysisIssue2 =
|
|
||||||
null $ shellCheck "#shellcheck disable=SC2086\n#lol\necho $1" []
|
|
||||||
|
|
||||||
shellCheck :: String -> [AnalysisOption] -> [ShellCheckComment]
|
|
||||||
shellCheck script options =
|
|
||||||
let (ParseResult result notes) = parseShell "-" script in
|
|
||||||
let allNotes = notes ++ (concat $ maybeToList $ do
|
|
||||||
(tree, posMap) <- result
|
|
||||||
let list = runAnalytics options tree
|
|
||||||
return $ map (noteToParseNote posMap) $ filterByAnnotation tree list
|
|
||||||
)
|
|
||||||
in
|
|
||||||
map formatNote $ nub $ sortNotes allNotes
|
|
||||||
|
|
||||||
data ShellCheckComment = ShellCheckComment { scLine :: Int, scColumn :: Int, scSeverity :: String, scCode :: Int, scMessage :: String }
|
|
||||||
|
|
||||||
instance Show ShellCheckComment where
|
|
||||||
show c = concat ["(", show $ scLine c, ",", show $ scColumn c, ") ", scSeverity c, ": ", show (scCode c), " ", scMessage c]
|
|
||||||
|
|
||||||
severityToString s =
|
|
||||||
case s of
|
|
||||||
ErrorC -> "error"
|
|
||||||
WarningC -> "warning"
|
|
||||||
InfoC -> "info"
|
|
||||||
StyleC -> "style"
|
|
||||||
|
|
||||||
formatNote (ParseNote pos severity code text) =
|
|
||||||
ShellCheckComment (sourceLine pos) (sourceColumn pos) (severityToString severity) (fromIntegral code) text
|
|
BIN
doc/emacs-flycheck.png
Normal file
BIN
doc/emacs-flycheck.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
doc/terminal.png
Normal file
BIN
doc/terminal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
BIN
doc/vim-syntastic.png
Normal file
BIN
doc/vim-syntastic.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
5
quickrun
Executable file
5
quickrun
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# quickrun runs ShellCheck in an interpreted mode.
|
||||||
|
# This allows testing changes without recompiling.
|
||||||
|
|
||||||
|
runghc -idist/build/autogen shellcheck.hs "$@"
|
21
quicktest
Executable file
21
quicktest
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# quicktest runs the ShellCheck unit tests in an interpreted mode.
|
||||||
|
# This allows running tests without compiling, which can be faster.
|
||||||
|
# 'cabal test' remains the source of truth.
|
||||||
|
|
||||||
|
(
|
||||||
|
var=$(echo 'liftM and $ sequence [
|
||||||
|
ShellCheck.Analytics.runTests
|
||||||
|
,ShellCheck.Parser.runTests
|
||||||
|
,ShellCheck.Checker.runTests
|
||||||
|
,ShellCheck.Checks.Commands.runTests
|
||||||
|
,ShellCheck.AnalyzerLib.runTests
|
||||||
|
]' | tr -d '\n' | cabal repl 2>&1 | tee /dev/stderr)
|
||||||
|
if [[ $var == *$'\nTrue'* ]]
|
||||||
|
then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
grep -C 3 -e "Fail" -e "Tracing" <<< "$var"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
) 2>&1
|
@@ -16,13 +16,27 @@ 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
|
strange behavior, but it also reports on a few more advanced issues where
|
||||||
corner cases can cause delayed failures.
|
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
|
# OPTIONS
|
||||||
|
|
||||||
**-f** *FORMAT*, **--format=***FORMAT*
|
**-C**[*WHEN*],\ **--color**[=*WHEN*]
|
||||||
|
|
||||||
: Specify the output format of shellcheck, which prints its results in the
|
: For TTY output, enable colors *always*, *never* or *auto*. The default
|
||||||
standard output. Subsequent **-f** options are ignored, see **FORMATS**
|
is *auto*. **--color** without an argument is equivalent to
|
||||||
below for more information.
|
**--color=always**.
|
||||||
|
|
||||||
**-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...]
|
**-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...]
|
||||||
|
|
||||||
@@ -30,11 +44,28 @@ corner cases can cause delayed failures.
|
|||||||
options are cumulative, but all the codes can be specified at once,
|
options are cumulative, but all the codes can be specified at once,
|
||||||
comma-separated as a single argument.
|
comma-separated as a single argument.
|
||||||
|
|
||||||
|
**-f** *FORMAT*, **--format=***FORMAT*
|
||||||
|
|
||||||
|
: Specify the output format of shellcheck, which prints its results in the
|
||||||
|
standard output. Subsequent **-f** options are ignored, see **FORMATS**
|
||||||
|
below for more information.
|
||||||
|
|
||||||
**-s**\ *shell*,\ **--shell=***shell*
|
**-s**\ *shell*,\ **--shell=***shell*
|
||||||
|
|
||||||
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *ksh* and
|
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
|
||||||
*zsh*. The default is to use the file's shebang, or *bash* if the target
|
The default is to use the file's shebang, or *bash* if the target shell
|
||||||
shell can't be determined.
|
can't be determined.
|
||||||
|
|
||||||
|
**-V**,\ **--version**
|
||||||
|
|
||||||
|
: Print version information and exit.
|
||||||
|
|
||||||
|
**-x**,\ **--external-sources**
|
||||||
|
|
||||||
|
: Follow 'source' statements even when the file is not specified as input.
|
||||||
|
By default, `shellcheck` will only follow files specified on the command
|
||||||
|
line (plus `/dev/null`). This option allows following any file the script
|
||||||
|
may `source`.
|
||||||
|
|
||||||
# FORMATS
|
# FORMATS
|
||||||
|
|
||||||
@@ -79,11 +110,12 @@ corner cases can cause delayed failures.
|
|||||||
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"line": line,
|
"file": "filename",
|
||||||
"column": column,
|
"line": lineNumber,
|
||||||
"level": level,
|
"column": columnNumber,
|
||||||
"code": ####,
|
"level": "severitylevel",
|
||||||
"message": message
|
"code": errorCode,
|
||||||
|
"message": "warning message"
|
||||||
},
|
},
|
||||||
...
|
...
|
||||||
]
|
]
|
||||||
@@ -100,6 +132,19 @@ For example, to suppress SC2035 about using `./*.jpg`:
|
|||||||
# shellcheck disable=SC2035
|
# shellcheck disable=SC2035
|
||||||
echo "Files: " *.jpg
|
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:
|
Valid keys are:
|
||||||
|
|
||||||
**disable**
|
**disable**
|
||||||
@@ -107,6 +152,28 @@ Valid keys are:
|
|||||||
The command can be a simple command like `echo foo`, or a compound command
|
The command can be a simple command like `echo foo`, or a compound command
|
||||||
like a function definition, subshell block or loop.
|
like a function definition, subshell block or loop.
|
||||||
|
|
||||||
|
**source**
|
||||||
|
: Overrides the filename included by a `source`/`.` statement. This can be
|
||||||
|
used to tell shellcheck where to look for a file whose name is determined
|
||||||
|
at runtime, or to skip a source by telling it to use `/dev/null`.
|
||||||
|
|
||||||
|
# ENVIRONMENT VARIABLES
|
||||||
|
The environment variable `SHELLCHECK_OPTS` can be set with default flags:
|
||||||
|
|
||||||
|
export SHELLCHECK_OPTS='--shell=bash --exclude=SC2016'
|
||||||
|
|
||||||
|
Its value will be split on spaces and prepended to the command line on each
|
||||||
|
invocation.
|
||||||
|
|
||||||
|
# RETURN VALUES
|
||||||
|
|
||||||
|
ShellCheck uses the follow exit codes:
|
||||||
|
|
||||||
|
+ 0: All files successfully scanned with no issues.
|
||||||
|
+ 1: All files successfully scanned with some issues.
|
||||||
|
+ 2: Some files could not be processed (e.g. file not found).
|
||||||
|
+ 3: ShellCheck was invoked with bad syntax (e.g. unknown flag).
|
||||||
|
+ 4: ShellCheck was invoked with bad options (e.g. unknown formatter).
|
||||||
|
|
||||||
# AUTHOR
|
# AUTHOR
|
||||||
ShellCheck is written and maintained by Vidar Holen.
|
ShellCheck is written and maintained by Vidar Holen.
|
||||||
@@ -116,6 +183,12 @@ Bugs and issues can be reported on GitHub:
|
|||||||
|
|
||||||
https://github.com/koalaman/shellcheck/issues
|
https://github.com/koalaman/shellcheck/issues
|
||||||
|
|
||||||
|
# COPYRIGHT
|
||||||
|
Copyright 2012-2015, Vidar Holen.
|
||||||
|
Licensed under the GNU General Public License version 3 or later,
|
||||||
|
see http://gnu.org/licenses/gpl.html
|
||||||
|
|
||||||
|
|
||||||
# SEE ALSO
|
# SEE ALSO
|
||||||
|
|
||||||
sh(1) bash(1)
|
sh(1) bash(1)
|
||||||
|
468
shellcheck.hs
468
shellcheck.hs
@@ -1,233 +1,111 @@
|
|||||||
{-
|
{-
|
||||||
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
http://www.vidarholen.net/contents/shellcheck
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
ShellCheck is distributed in the hope that it will be useful,
|
ShellCheck is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU Affero General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
|
import ShellCheck.Data
|
||||||
|
import ShellCheck.Checker
|
||||||
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Regex
|
||||||
|
|
||||||
|
import ShellCheck.Formatter.Format
|
||||||
|
import qualified ShellCheck.Formatter.CheckStyle
|
||||||
|
import qualified ShellCheck.Formatter.GCC
|
||||||
|
import qualified ShellCheck.Formatter.JSON
|
||||||
|
import qualified ShellCheck.Formatter.TTY
|
||||||
|
|
||||||
import Control.Exception
|
import Control.Exception
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
|
import Control.Monad.Except
|
||||||
import Data.Char
|
import Data.Char
|
||||||
|
import Data.Functor
|
||||||
|
import Data.Either
|
||||||
|
import qualified Data.Map as Map
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import GHC.Exts
|
import Data.Monoid
|
||||||
import GHC.IO.Device
|
|
||||||
import Prelude hiding (catch)
|
import Prelude hiding (catch)
|
||||||
import ShellCheck.Data
|
|
||||||
import ShellCheck.Simple
|
|
||||||
import ShellCheck.Analytics
|
|
||||||
import System.Console.GetOpt
|
import System.Console.GetOpt
|
||||||
import System.Directory
|
import System.Directory
|
||||||
import System.Environment
|
import System.Environment
|
||||||
import System.Exit
|
import System.Exit
|
||||||
import System.IO
|
import System.IO
|
||||||
import Text.JSON
|
|
||||||
import qualified Data.Map as Map
|
|
||||||
|
|
||||||
data Flag = Flag String String
|
data Flag = Flag String String
|
||||||
|
data Status =
|
||||||
|
NoProblems
|
||||||
|
| SomeProblems
|
||||||
|
| SupportFailure
|
||||||
|
| SyntaxFailure
|
||||||
|
| RuntimeException
|
||||||
|
deriving (Ord, Eq, Show)
|
||||||
|
|
||||||
header = "Usage: shellcheck [OPTIONS...] FILES..."
|
instance Monoid Status where
|
||||||
|
mempty = NoProblems
|
||||||
|
mappend = max
|
||||||
|
|
||||||
|
data Options = Options {
|
||||||
|
checkSpec :: CheckSpec,
|
||||||
|
externalSources :: Bool,
|
||||||
|
formatterOptions :: FormatterOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultOptions = Options {
|
||||||
|
checkSpec = emptyCheckSpec,
|
||||||
|
externalSources = False,
|
||||||
|
formatterOptions = FormatterOptions {
|
||||||
|
foColorOption = ColorAuto
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
|
||||||
options = [
|
options = [
|
||||||
Option ['f'] ["format"]
|
Option "e" ["exclude"]
|
||||||
(ReqArg (Flag "format") "FORMAT") "output format",
|
|
||||||
Option ['e'] ["exclude"]
|
|
||||||
(ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings",
|
(ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings",
|
||||||
Option ['s'] ["shell"]
|
Option "f" ["format"]
|
||||||
(ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (bash,sh,ksh,zsh)",
|
(ReqArg (Flag "format") "FORMAT") "output format",
|
||||||
Option ['V'] ["version"]
|
Option "C" ["color"]
|
||||||
|
(OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN")
|
||||||
|
"Use color (auto, always, never)",
|
||||||
|
Option "s" ["shell"]
|
||||||
|
(ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (sh,bash,dash,ksh)",
|
||||||
|
Option "x" ["external-sources"]
|
||||||
|
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES.",
|
||||||
|
Option "V" ["version"]
|
||||||
(NoArg $ Flag "version" "true") "Print version information"
|
(NoArg $ Flag "version" "true") "Print version information"
|
||||||
]
|
]
|
||||||
|
|
||||||
printErr = hPutStrLn stderr
|
printErr = lift . hPutStrLn stderr
|
||||||
|
|
||||||
syntaxFailure = ExitFailure 3
|
|
||||||
supportFailure = ExitFailure 4
|
|
||||||
|
|
||||||
instance JSON ShellCheckComment where
|
|
||||||
showJSON c = makeObj [
|
|
||||||
("line", showJSON $ scLine c),
|
|
||||||
("column", showJSON $ scColumn c),
|
|
||||||
("level", showJSON $ scSeverity c),
|
|
||||||
("code", showJSON $ scCode c),
|
|
||||||
("message", showJSON $ scMessage c)
|
|
||||||
]
|
|
||||||
readJSON = undefined
|
|
||||||
|
|
||||||
|
parseArguments :: [String] -> ExceptT Status IO ([Flag], [FilePath])
|
||||||
parseArguments argv =
|
parseArguments argv =
|
||||||
case getOpt Permute options argv of
|
case getOpt Permute options argv of
|
||||||
(opts, files, []) -> do
|
(opts, files, []) -> return (opts, files)
|
||||||
verifyOptions opts files
|
|
||||||
return $ Just (opts, files)
|
|
||||||
|
|
||||||
(_, _, errors) -> do
|
(_, _, errors) -> do
|
||||||
printErr $ concat errors ++ "\n" ++ usageInfo header options
|
printErr $ concat errors ++ "\n" ++ usageInfo usageHeader options
|
||||||
exitWith syntaxFailure
|
throwError SyntaxFailure
|
||||||
|
|
||||||
formats = Map.fromList [
|
formats :: FormatterOptions -> Map.Map String (IO Formatter)
|
||||||
("json", forJson),
|
formats options = Map.fromList [
|
||||||
("gcc", forGcc),
|
("checkstyle", ShellCheck.Formatter.CheckStyle.format),
|
||||||
("checkstyle", forCheckstyle),
|
("gcc", ShellCheck.Formatter.GCC.format),
|
||||||
("tty", forTty)
|
("json", ShellCheck.Formatter.JSON.format),
|
||||||
|
("tty", ShellCheck.Formatter.TTY.format options)
|
||||||
]
|
]
|
||||||
|
|
||||||
forTty options files = do
|
|
||||||
output <- mapM doFile files
|
|
||||||
return $ and output
|
|
||||||
where
|
|
||||||
clear = ansi 0
|
|
||||||
ansi n = "\x1B[" ++ show n ++ "m"
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
colorComment level comment =
|
|
||||||
ansi (colorForLevel level) ++ comment ++ clear
|
|
||||||
|
|
||||||
doFile path = do
|
|
||||||
contents <- readContents path
|
|
||||||
doInput path contents
|
|
||||||
|
|
||||||
doInput filename contents = do
|
|
||||||
let fileLines = lines contents
|
|
||||||
let lineCount = length fileLines
|
|
||||||
let comments = getComments options contents
|
|
||||||
let groups = groupWith scLine comments
|
|
||||||
colorFunc <- getColorFunc
|
|
||||||
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
|
|
||||||
return $ null comments
|
|
||||||
|
|
||||||
cuteIndent comment =
|
|
||||||
replicate (scColumn comment - 1) ' ' ++
|
|
||||||
"^-- " ++ code (scCode comment) ++ ": " ++ scMessage comment
|
|
||||||
|
|
||||||
code code = "SC" ++ (show code)
|
|
||||||
|
|
||||||
getColorFunc = do
|
|
||||||
term <- hIsTerminalDevice stdout
|
|
||||||
return $ if term then colorComment else const id
|
|
||||||
|
|
||||||
-- This totally ignores the filenames. Fixme?
|
|
||||||
forJson options files = do
|
|
||||||
comments <- liftM concat $ mapM (commentsFor options) files
|
|
||||||
putStrLn $ encodeStrict comments
|
|
||||||
return . null $ comments
|
|
||||||
|
|
||||||
-- Mimic GCC "file:line:col: (error|warning|note): message" format
|
|
||||||
forGcc options files = do
|
|
||||||
files <- mapM process files
|
|
||||||
return $ and files
|
|
||||||
where
|
|
||||||
process file = do
|
|
||||||
contents <- readContents file
|
|
||||||
let comments = makeNonVirtual (getComments options contents) contents
|
|
||||||
mapM_ (putStrLn . format file) comments
|
|
||||||
return $ null comments
|
|
||||||
|
|
||||||
format filename c = concat [
|
|
||||||
filename, ":",
|
|
||||||
show $ scLine c, ":",
|
|
||||||
show $ scColumn c, ": ",
|
|
||||||
case scSeverity c of
|
|
||||||
"error" -> "error"
|
|
||||||
"warning" -> "warning"
|
|
||||||
_ -> "note",
|
|
||||||
": ",
|
|
||||||
concat . lines $ scMessage c,
|
|
||||||
" [SC", show $ scCode c, "]"
|
|
||||||
]
|
|
||||||
|
|
||||||
-- Checkstyle compatible output. A bit of a hack to avoid XML dependencies
|
|
||||||
forCheckstyle options files = do
|
|
||||||
putStrLn "<?xml version='1.0' encoding='UTF-8'?>"
|
|
||||||
putStrLn "<checkstyle version='4.3'>"
|
|
||||||
statuses <- mapM (\x -> process x `catch` report) files
|
|
||||||
putStrLn "</checkstyle>"
|
|
||||||
return $ and statuses
|
|
||||||
where
|
|
||||||
process file = do
|
|
||||||
comments <- commentsFor options file
|
|
||||||
putStrLn (formatFile file comments)
|
|
||||||
return $ null comments
|
|
||||||
report error = do
|
|
||||||
printErr $ show (error :: SomeException)
|
|
||||||
return False
|
|
||||||
|
|
||||||
severity "error" = "error"
|
|
||||||
severity "warning" = "warning"
|
|
||||||
severity _ = "info"
|
|
||||||
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` " ./")]
|
|
||||||
|
|
||||||
formatFile name comments = concat [
|
|
||||||
"<file ", attr "name" name, ">\n",
|
|
||||||
concatMap format comments,
|
|
||||||
"</file>"
|
|
||||||
]
|
|
||||||
|
|
||||||
format c = concat [
|
|
||||||
"<error ",
|
|
||||||
attr "line" $ show . scLine $ c,
|
|
||||||
attr "column" $ show . scColumn $ c,
|
|
||||||
attr "severity" $ severity . scSeverity $ c,
|
|
||||||
attr "message" $ scMessage c,
|
|
||||||
attr "source" $ "ShellCheck.SC" ++ (show $ scCode c),
|
|
||||||
"/>\n"
|
|
||||||
]
|
|
||||||
|
|
||||||
commentsFor options file =
|
|
||||||
liftM (getComments options) $ readContents file
|
|
||||||
|
|
||||||
getComments options contents =
|
|
||||||
excludeCodes (getExclusions options) $ shellCheck contents analysisOptions
|
|
||||||
where
|
|
||||||
analysisOptions = catMaybes [ shellOption ]
|
|
||||||
shellOption = do
|
|
||||||
option <- getOption options "shell"
|
|
||||||
sh <- shellForExecutable option
|
|
||||||
return $ ForceShell sh
|
|
||||||
|
|
||||||
|
|
||||||
readContents file = if file == "-" then getContents else readFile file
|
|
||||||
|
|
||||||
-- Realign comments from a tabstop of 8 to 1
|
|
||||||
makeNonVirtual comments contents =
|
|
||||||
map fix comments
|
|
||||||
where
|
|
||||||
ls = lines contents
|
|
||||||
fix c = c { scColumn = real (ls !! (scLine c - 1)) 0 0 (scColumn c) }
|
|
||||||
real _ r v target | target <= v = r
|
|
||||||
real [] r v _ = r -- should never happen
|
|
||||||
real ('\t':rest) r v target =
|
|
||||||
real rest (r+1) (v + 8 - (v `mod` 8)) target
|
|
||||||
real (_:rest) r v target = real rest (r+1) (v+1) target
|
|
||||||
|
|
||||||
getOption [] _ = Nothing
|
getOption [] _ = Nothing
|
||||||
getOption (Flag var val:_) name | name == var = return val
|
getOption (Flag var val:_) name | name == var = return val
|
||||||
getOption (_:rest) flag = getOption rest flag
|
getOption (_:rest) flag = getOption rest flag
|
||||||
@@ -240,7 +118,7 @@ split char str =
|
|||||||
where
|
where
|
||||||
split' (a:rest) element =
|
split' (a:rest) element =
|
||||||
if a == char
|
if a == char
|
||||||
then (reverse element) : split' rest []
|
then reverse element : split' rest []
|
||||||
else split' rest (a:element)
|
else split' rest (a:element)
|
||||||
split' [] element = [reverse element]
|
split' [] element = [reverse element]
|
||||||
|
|
||||||
@@ -250,52 +128,182 @@ getExclusions options =
|
|||||||
in
|
in
|
||||||
map (Prelude.read . clean) elements :: [Int]
|
map (Prelude.read . clean) elements :: [Int]
|
||||||
|
|
||||||
excludeCodes codes =
|
toStatus = liftM (either id id) . runExceptT
|
||||||
filter (not . hasCode)
|
|
||||||
|
getEnvArgs = do
|
||||||
|
opts <- getEnv "SHELLCHECK_OPTS" `catch` cantWaitForLookupEnv
|
||||||
|
return . filter (not . null) $ opts `splitOn` mkRegex " +"
|
||||||
where
|
where
|
||||||
hasCode c = scCode c `elem` codes
|
cantWaitForLookupEnv :: IOException -> IO String
|
||||||
|
cantWaitForLookupEnv = const $ return ""
|
||||||
|
|
||||||
main = do
|
main = do
|
||||||
args <- getArgs
|
params <- getArgs
|
||||||
parsedArgs <- parseArguments args
|
envOpts <- getEnvArgs
|
||||||
code <- do
|
let args = envOpts ++ params
|
||||||
status <- process parsedArgs
|
status <- toStatus $ do
|
||||||
return $ if status then ExitSuccess else ExitFailure 1
|
(flags, files) <- parseArguments args
|
||||||
`catch` return
|
process flags files
|
||||||
`catch` \err -> do
|
exitWith $ statusToCode status
|
||||||
printErr $ show (err :: SomeException)
|
|
||||||
return $ ExitFailure 2
|
|
||||||
exitWith code
|
|
||||||
|
|
||||||
process Nothing = return False
|
statusToCode status =
|
||||||
process (Just (options, files)) =
|
case status of
|
||||||
let format = fromMaybe "tty" $ getOption options "format" in
|
NoProblems -> ExitSuccess
|
||||||
case Map.lookup format formats of
|
SomeProblems -> ExitFailure 1
|
||||||
Nothing -> do
|
SyntaxFailure -> ExitFailure 3
|
||||||
printErr $ "Unknown format " ++ format
|
SupportFailure -> ExitFailure 4
|
||||||
printErr $ "Supported formats:"
|
RuntimeException -> ExitFailure 2
|
||||||
mapM_ (printErr . write) $ Map.keys formats
|
|
||||||
exitWith supportFailure
|
|
||||||
where write s = " " ++ s
|
|
||||||
Just f -> do
|
|
||||||
f options files
|
|
||||||
|
|
||||||
verifyOptions opts files = do
|
process :: [Flag] -> [FilePath] -> ExceptT Status IO Status
|
||||||
when (isJust $ getOption opts "version") printVersionAndExit
|
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
|
||||||
|
|
||||||
let shell = getOption opts "shell" in
|
runFormatter :: SystemInterface IO -> Formatter -> Options -> [FilePath]
|
||||||
when (isJust shell && isNothing (shell >>= shellForExecutable)) $ do
|
-> IO Status
|
||||||
printErr $ "Unknown shell: " ++ (fromJust shell)
|
runFormatter sys format options files = do
|
||||||
exitWith supportFailure
|
header format
|
||||||
|
result <- foldM f NoProblems files
|
||||||
|
footer format
|
||||||
|
return result
|
||||||
|
where
|
||||||
|
f :: Status -> FilePath -> IO Status
|
||||||
|
f status file = do
|
||||||
|
newStatus <- process file `catch` handler file
|
||||||
|
return $ status `mappend` newStatus
|
||||||
|
handler :: FilePath -> IOException -> IO Status
|
||||||
|
handler file e = do
|
||||||
|
onFailure format file (show e)
|
||||||
|
return RuntimeException
|
||||||
|
|
||||||
|
process :: FilePath -> IO Status
|
||||||
|
process filename = do
|
||||||
|
contents <- inputFile filename
|
||||||
|
let checkspec = (checkSpec options) {
|
||||||
|
csFilename = filename,
|
||||||
|
csScript = contents
|
||||||
|
}
|
||||||
|
result <- checkScript sys checkspec
|
||||||
|
onResult format result contents
|
||||||
|
return $
|
||||||
|
if null (crComments result)
|
||||||
|
then NoProblems
|
||||||
|
else SomeProblems
|
||||||
|
|
||||||
|
parseColorOption colorOption =
|
||||||
|
case colorOption of
|
||||||
|
"auto" -> ColorAuto
|
||||||
|
"always" -> ColorAlways
|
||||||
|
"never" -> ColorNever
|
||||||
|
_ -> error $ "Bad value for --color `" ++ colorOption ++ "'"
|
||||||
|
|
||||||
|
parseOption flag options =
|
||||||
|
case flag of
|
||||||
|
Flag "shell" str ->
|
||||||
|
fromMaybe (die $ "Unknown shell: " ++ str) $ do
|
||||||
|
shell <- shellForExecutable str
|
||||||
|
return $ return options {
|
||||||
|
checkSpec = (checkSpec options) {
|
||||||
|
csShellTypeOverride = Just shell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Flag "exclude" str -> do
|
||||||
|
new <- mapM parseNum $ split ',' str
|
||||||
|
let old = csExcludedWarnings . checkSpec $ options
|
||||||
|
return options {
|
||||||
|
checkSpec = (checkSpec options) {
|
||||||
|
csExcludedWarnings = new ++ old
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Flag "version" _ -> do
|
||||||
|
liftIO printVersion
|
||||||
|
throwError NoProblems
|
||||||
|
|
||||||
|
Flag "externals" _ ->
|
||||||
|
return options {
|
||||||
|
externalSources = True
|
||||||
|
}
|
||||||
|
|
||||||
|
Flag "color" color ->
|
||||||
|
return options {
|
||||||
|
formatterOptions = (formatterOptions options) {
|
||||||
|
foColorOption = parseColorOption color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ -> return options
|
||||||
|
where
|
||||||
|
die s = do
|
||||||
|
printErr s
|
||||||
|
throwError SupportFailure
|
||||||
|
parseNum ('S':'C':str) = parseNum str
|
||||||
|
parseNum num = do
|
||||||
|
unless (all isDigit num) $ do
|
||||||
|
printErr $ "Bad exclusion: " ++ num
|
||||||
|
throwError SyntaxFailure
|
||||||
|
return (Prelude.read num :: Integer)
|
||||||
|
|
||||||
|
ioInterface options files = do
|
||||||
|
inputs <- mapM normalize files
|
||||||
|
return SystemInterface {
|
||||||
|
siReadFile = get inputs
|
||||||
|
}
|
||||||
|
where
|
||||||
|
get inputs file = do
|
||||||
|
ok <- allowable inputs file
|
||||||
|
if ok
|
||||||
|
then (Right <$> inputFile file) `catch` handler
|
||||||
|
else return $ Left (file ++ " was not specified as input (see shellcheck -x).")
|
||||||
|
|
||||||
|
where
|
||||||
|
handler :: IOException -> IO (Either ErrorMessage String)
|
||||||
|
handler ex = return . Left $ show ex
|
||||||
|
|
||||||
|
allowable inputs x =
|
||||||
|
if externalSources options
|
||||||
|
then return True
|
||||||
|
else do
|
||||||
|
path <- normalize x
|
||||||
|
return $ path `elem` inputs
|
||||||
|
|
||||||
|
normalize x =
|
||||||
|
canonicalizePath x `catch` fallback x
|
||||||
|
where
|
||||||
|
fallback :: FilePath -> IOException -> IO FilePath
|
||||||
|
fallback path _ = return path
|
||||||
|
|
||||||
|
inputFile file = do
|
||||||
|
contents <-
|
||||||
|
if file == "-"
|
||||||
|
then getContents
|
||||||
|
else readFile file
|
||||||
|
|
||||||
|
seq (length contents) $
|
||||||
|
return contents
|
||||||
|
|
||||||
|
verifyFiles files =
|
||||||
when (null files) $ do
|
when (null files) $ do
|
||||||
printErr "No files specified.\n"
|
printErr "No files specified.\n"
|
||||||
printErr $ usageInfo header options
|
printErr $ usageInfo usageHeader options
|
||||||
exitWith syntaxFailure
|
throwError SyntaxFailure
|
||||||
|
|
||||||
printVersionAndExit = do
|
printVersion = do
|
||||||
putStrLn $ "ShellCheck - shell script analysis tool"
|
putStrLn "ShellCheck - shell script analysis tool"
|
||||||
putStrLn $ "version: " ++ shellcheckVersion
|
putStrLn $ "version: " ++ shellcheckVersion
|
||||||
putStrLn $ "license: GNU Affero General Public License, version 3"
|
putStrLn "license: GNU General Public License, version 3"
|
||||||
putStrLn $ "website: http://www.shellcheck.net"
|
putStrLn "website: http://www.shellcheck.net"
|
||||||
exitWith ExitSuccess
|
|
||||||
|
35
stack.yaml
Normal file
35
stack.yaml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# This file was automatically generated by stack init
|
||||||
|
# For more information, see: http://docs.haskellstack.org/en/stable/yaml_configuration/
|
||||||
|
|
||||||
|
# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
|
||||||
|
resolver: lts-5.5
|
||||||
|
|
||||||
|
# Local packages, usually specified by relative directory name
|
||||||
|
packages:
|
||||||
|
- '.'
|
||||||
|
# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3)
|
||||||
|
extra-deps: []
|
||||||
|
|
||||||
|
# Override default flag values for local packages and extra-deps
|
||||||
|
flags: {}
|
||||||
|
|
||||||
|
# Extra package databases containing global packages
|
||||||
|
extra-package-dbs: []
|
||||||
|
|
||||||
|
# Control whether we use the GHC we find on the path
|
||||||
|
# system-ghc: true
|
||||||
|
|
||||||
|
# Require a specific version of stack, using version ranges
|
||||||
|
# require-stack-version: -any # Default
|
||||||
|
# require-stack-version: >= 1.0.0
|
||||||
|
|
||||||
|
# Override the architecture used by stack, especially useful on Windows
|
||||||
|
# arch: i386
|
||||||
|
# arch: x86_64
|
||||||
|
|
||||||
|
# Extra directories used by stack for building
|
||||||
|
# extra-include-dirs: [/path/to/dir]
|
||||||
|
# extra-lib-dirs: [/path/to/dir]
|
||||||
|
|
||||||
|
# Allow a newer minor version of GHC than the snapshot specifies
|
||||||
|
# compiler-check: newer-minor
|
@@ -1,65 +0,0 @@
|
|||||||
#!/usr/bin/env runhaskell
|
|
||||||
-- #!/usr/bin/env runhugs
|
|
||||||
-- $Id: quickcheck,v 1.4 2003/01/08 15:09:22 shae Exp $
|
|
||||||
-- This file defines a command
|
|
||||||
-- quickCheck <options> <files>
|
|
||||||
-- which invokes quickCheck on all properties defined in the files given as
|
|
||||||
-- arguments, by generating an input script for hugs and then invoking it.
|
|
||||||
-- quickCheck recognises the options
|
|
||||||
-- +names print the name of each property before checking it
|
|
||||||
-- -names do not print property names (the default)
|
|
||||||
-- +verbose displays each test case before running
|
|
||||||
-- -verbose do not displays each test case before running (the default)
|
|
||||||
-- Other options (beginning with + or -) are passed unchanged to hugs.
|
|
||||||
--
|
|
||||||
-- Change the first line of this file to the location of runhaskell or runhugs
|
|
||||||
-- on your system.
|
|
||||||
-- Make the file executable.
|
|
||||||
--
|
|
||||||
-- TODO:
|
|
||||||
-- someone on #haskell asked about supporting QC tests inside LaTeX, ex. \{begin} \{end}, how?
|
|
||||||
|
|
||||||
import System.Cmd
|
|
||||||
import System.Directory (findExecutable)
|
|
||||||
import System.Environment
|
|
||||||
import Data.List
|
|
||||||
import Data.Maybe (fromJust)
|
|
||||||
|
|
||||||
main :: IO ()
|
|
||||||
main = do as<-getArgs
|
|
||||||
sequence_ (map (process (filter isOption as))
|
|
||||||
(filter (not.isOption) as))
|
|
||||||
|
|
||||||
-- ugly hack for .lhs files, is there a better way?
|
|
||||||
unlit [] = []
|
|
||||||
unlit x = if (head x) == '>' then (tail x) else x
|
|
||||||
|
|
||||||
process opts file =
|
|
||||||
let (namesOpt,opts') = getOption "names" "-names" opts
|
|
||||||
(verboseOpt,opts'') = getOption "verbose" "-verbose" opts' in
|
|
||||||
do xs<-readFile file
|
|
||||||
let names = nub$ filter (\x -> (("> prop_" `isPrefixOf` x) || ("prop_" `isPrefixOf` x)))
|
|
||||||
(map (fst.head.lex.unlit) (lines xs))
|
|
||||||
if null names then
|
|
||||||
putStr (file++": no properties to check\n")
|
|
||||||
else do writeFile "hugsin"$
|
|
||||||
unlines ((":load "++file):
|
|
||||||
":m +Test.QuickCheck":
|
|
||||||
"let quackCheck p = quickCheckWith (stdArgs { maxSuccess = 1 }) p ":
|
|
||||||
[(if namesOpt=="+names" then
|
|
||||||
"putStr \""++p++": \" >> "
|
|
||||||
else "") ++
|
|
||||||
("quackCheck ")
|
|
||||||
++ p | p<-names])
|
|
||||||
-- To use ghci
|
|
||||||
ghci <- findExecutable "ghci"
|
|
||||||
system (fromJust ghci ++options opts''++" <hugsin")
|
|
||||||
return ()
|
|
||||||
|
|
||||||
isOption xs = head xs `elem` "-+"
|
|
||||||
|
|
||||||
options opts = unwords ["\""++opt++"\"" | opt<-opts]
|
|
||||||
|
|
||||||
getOption name def opts =
|
|
||||||
let opt = head [opt | opt<-opts++[def], isPrefixOf name (drop 1 opt)] in
|
|
||||||
(opt, filter (/=opt) opts)
|
|
@@ -1,22 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Todo: Find a way to make this not suck.
|
|
||||||
|
|
||||||
ulimit -t 60 # Sometimes GHC ends in a spin loop, and this is easier than debugging
|
|
||||||
|
|
||||||
[[ -e test/quackCheck.hs ]] || { echo "Are you running me from the wrong directory?"; exit 1; }
|
|
||||||
[[ $1 == -v ]] && pattern="" || pattern="FAIL"
|
|
||||||
|
|
||||||
find . -name '*.hs' -exec bash -c '
|
|
||||||
grep -v "^module " "$1" > quack.tmp.hs
|
|
||||||
./test/quackCheck.hs +names quack.tmp.hs
|
|
||||||
' -- {} \; 2>&1 | grep -i "$pattern"
|
|
||||||
result=$?
|
|
||||||
rm -f quack.tmp.hs hugsin
|
|
||||||
|
|
||||||
if [[ $result == 0 ]]
|
|
||||||
then
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
22
test/shellcheck.hs
Normal file
22
test/shellcheck.hs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
module Main where
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import System.Exit
|
||||||
|
import qualified ShellCheck.Checker
|
||||||
|
import qualified ShellCheck.Analytics
|
||||||
|
import qualified ShellCheck.AnalyzerLib
|
||||||
|
import qualified ShellCheck.Parser
|
||||||
|
import qualified ShellCheck.Checks.Commands
|
||||||
|
|
||||||
|
main = do
|
||||||
|
putStrLn "Running ShellCheck tests..."
|
||||||
|
results <- sequence [
|
||||||
|
ShellCheck.Checker.runTests,
|
||||||
|
ShellCheck.Checks.Commands.runTests,
|
||||||
|
ShellCheck.Analytics.runTests,
|
||||||
|
ShellCheck.AnalyzerLib.runTests,
|
||||||
|
ShellCheck.Parser.runTests
|
||||||
|
]
|
||||||
|
if and results
|
||||||
|
then exitSuccess
|
||||||
|
else exitFailure
|
Reference in New Issue
Block a user