mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 00:39:19 +08:00
Compare commits
165 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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
|
||||||
|
|
||||||
|
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:
|
|
76
README.md
76
README.md
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
http://www.shellcheck.net
|
http://www.shellcheck.net
|
||||||
|
|
||||||
Copyright 2012-2014, Vidar 'koala_man' Holen
|
Copyright 2012-2015, Vidar 'koala_man' Holen
|
||||||
Licensed under the GNU Affero General Public License, v3
|
Licensed under the GNU General Public License, v3
|
||||||
|
|
||||||
The goals of ShellCheck are:
|
The goals of ShellCheck are:
|
||||||
|
|
||||||
@@ -16,12 +16,32 @@ The goals of ShellCheck are:
|
|||||||
- 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.
|
ShellCheck is written in Haskell, and requires 2 GB of memory to compile.
|
||||||
|
|
||||||
|
## Installing
|
||||||
|
|
||||||
|
On systems with Cabal:
|
||||||
|
|
||||||
|
cabal update
|
||||||
|
cabal install shellcheck
|
||||||
|
|
||||||
|
On Debian based distros:
|
||||||
|
|
||||||
|
apt-get install shellcheck
|
||||||
|
|
||||||
|
On OS X with homebrew:
|
||||||
|
|
||||||
|
brew install shellcheck
|
||||||
|
|
||||||
|
ShellCheck is also available as an online service:
|
||||||
|
|
||||||
|
http://www.shellcheck.net
|
||||||
|
|
||||||
## Building with Cabal
|
## Building with Cabal
|
||||||
|
|
||||||
Make sure cabal is installed. On Debian based distros:
|
This sections describes how to build ShellCheck from a source directory.
|
||||||
|
|
||||||
|
First, make sure cabal is installed. On Debian based distros:
|
||||||
|
|
||||||
apt-get install cabal-install
|
apt-get install cabal-install
|
||||||
|
|
||||||
@@ -37,32 +57,48 @@ On Mac OS X with MacPorts (http://www.macports.org/):
|
|||||||
|
|
||||||
port install hs-cabal-install
|
port install hs-cabal-install
|
||||||
|
|
||||||
With cabal installed, cd to the shellcheck source directory and:
|
On native Windows (https://www.haskell.org/platform/):
|
||||||
|
|
||||||
|
Download and install the latest version of the Haskell Platform.
|
||||||
|
|
||||||
|
Let cabal update itself, in case your distro version is outdated:
|
||||||
|
|
||||||
|
$ cabal update
|
||||||
|
$ cabal install cabal-install
|
||||||
|
|
||||||
|
With cabal installed, cd to the ShellCheck source directory and:
|
||||||
|
|
||||||
$ cabal install
|
$ cabal install
|
||||||
...
|
|
||||||
|
This will install ShellCheck to your `~/.cabal/bin` directory.
|
||||||
|
|
||||||
|
Add the directory to your `PATH` (for bash, add this to your `~/.bashrc`):
|
||||||
|
|
||||||
|
export PATH="$HOME/.cabal/bin:$PATH"
|
||||||
|
|
||||||
|
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 \
|
|
||||||
libghc-regex-compat-dev libghc-quickcheck2-dev
|
|
||||||
|
|
||||||
To build and run the tests, cd to the shellcheck source directory and:
|
|
||||||
|
|
||||||
$ make
|
|
||||||
|
|
||||||
|
cabal configure --enable-tests
|
||||||
|
cabal build
|
||||||
|
cabal test
|
||||||
|
|
||||||
Happy ShellChecking!
|
Happy ShellChecking!
|
||||||
|
45
Setup.hs
45
Setup.hs
@@ -1,2 +1,43 @@
|
|||||||
import Distribution.Simple
|
import Distribution.PackageDescription (
|
||||||
main = defaultMain
|
HookedBuildInfo,
|
||||||
|
emptyHookedBuildInfo )
|
||||||
|
import Distribution.Simple (
|
||||||
|
Args,
|
||||||
|
UserHooks ( preSDist ),
|
||||||
|
defaultMainWithHooks,
|
||||||
|
simpleUserHooks )
|
||||||
|
import Distribution.Simple.Setup ( SDistFlags )
|
||||||
|
|
||||||
|
-- | This requires the process package from,
|
||||||
|
--
|
||||||
|
-- https://hackage.haskell.org/package/process
|
||||||
|
--
|
||||||
|
import System.Process ( callCommand )
|
||||||
|
|
||||||
|
|
||||||
|
-- | This will use almost the default implementation, except we switch
|
||||||
|
-- out the default pre-sdist hook with our own, 'myPreSDist'.
|
||||||
|
--
|
||||||
|
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..."
|
||||||
|
putStrLn pandoc_cmd
|
||||||
|
callCommand pandoc_cmd
|
||||||
|
return emptyHookedBuildInfo
|
||||||
|
where
|
||||||
|
pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
Name: ShellCheck
|
Name: ShellCheck
|
||||||
-- Must also be updated in ShellCheck/Data.hs :
|
Version: 0.3.8
|
||||||
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
|
||||||
@@ -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
|
||||||
@@ -35,13 +43,18 @@ library
|
|||||||
json,
|
json,
|
||||||
mtl,
|
mtl,
|
||||||
parsec,
|
parsec,
|
||||||
regex-compat
|
regex-tdfa,
|
||||||
|
QuickCheck >= 2.7.4
|
||||||
exposed-modules:
|
exposed-modules:
|
||||||
ShellCheck.Analytics
|
ShellCheck.Analytics
|
||||||
ShellCheck.AST
|
ShellCheck.AST
|
||||||
ShellCheck.Data
|
ShellCheck.Data
|
||||||
|
ShellCheck.Options
|
||||||
ShellCheck.Parser
|
ShellCheck.Parser
|
||||||
|
ShellCheck.Regex
|
||||||
ShellCheck.Simple
|
ShellCheck.Simple
|
||||||
|
other-modules:
|
||||||
|
Paths_ShellCheck
|
||||||
|
|
||||||
executable shellcheck
|
executable shellcheck
|
||||||
build-depends:
|
build-depends:
|
||||||
@@ -52,5 +65,23 @@ executable shellcheck
|
|||||||
json,
|
json,
|
||||||
mtl,
|
mtl,
|
||||||
parsec,
|
parsec,
|
||||||
regex-compat
|
regex-tdfa,
|
||||||
|
transformers,
|
||||||
|
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,
|
||||||
|
parsec,
|
||||||
|
regex-tdfa,
|
||||||
|
transformers,
|
||||||
|
QuickCheck >= 2.7.4
|
||||||
|
main-is: test/shellcheck.hs
|
||||||
|
|
||||||
|
@@ -3,23 +3,23 @@
|
|||||||
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 +28,15 @@ 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_Expansion Id [Token]
|
||||||
| TA_Expansion Id Token
|
| TA_Index Id Token
|
||||||
| TA_Literal Id String
|
|
||||||
| 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 +47,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
|
||||||
@@ -121,16 +121,20 @@ 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
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
data Annotation = DisableComment Integer deriving (Show, Eq)
|
data Annotation = DisableComment Integer 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 =
|
||||||
@@ -167,6 +171,7 @@ 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_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 +183,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 +212,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
|
||||||
|
|
||||||
@@ -242,9 +248,11 @@ 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 = return t
|
delve t = return t
|
||||||
|
|
||||||
getId t = case t of
|
getId t = case t of
|
||||||
@@ -296,6 +304,7 @@ getId t = case t of
|
|||||||
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
|
||||||
@@ -326,11 +335,9 @@ getId t = case t of
|
|||||||
TA_Binary id _ _ _ -> id
|
TA_Binary 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 +346,8 @@ 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
|
||||||
|
|
||||||
blank :: Monad m => Token -> m ()
|
blank :: Monad m => Token -> m ()
|
||||||
blank = const $ return ()
|
blank = const $ return ()
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,9 @@
|
|||||||
module ShellCheck.Data where
|
module ShellCheck.Data where
|
||||||
|
|
||||||
shellcheckVersion = "0.3.2" -- Must also be updated in ShellCheck.cabal
|
import Data.Version (showVersion)
|
||||||
|
import Paths_ShellCheck (version)
|
||||||
|
|
||||||
|
shellcheckVersion = showVersion version
|
||||||
|
|
||||||
internalVariables = [
|
internalVariables = [
|
||||||
-- Generic
|
-- Generic
|
||||||
@@ -24,22 +27,10 @@ internalVariables = [
|
|||||||
"LC_MESSAGES", "LC_NUMERIC", "LINES", "MAIL", "MAILCHECK", "MAILPATH",
|
"LC_MESSAGES", "LC_NUMERIC", "LINES", "MAIL", "MAILCHECK", "MAILPATH",
|
||||||
"OPTERR", "PATH", "POSIXLY_CORRECT", "PROMPT_COMMAND",
|
"OPTERR", "PATH", "POSIXLY_CORRECT", "PROMPT_COMMAND",
|
||||||
"PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT",
|
"PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT",
|
||||||
"TMOUT", "TMPDIR", "auto_resume", "histchars",
|
"TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC",
|
||||||
|
|
||||||
-- Zsh
|
-- Other
|
||||||
"ARGV0", "BAUD", "cdpath", "COLUMNS", "CORRECT_IGNORE",
|
"USER", "TZ", "TERM"
|
||||||
"DIRSTACKSIZE", "ENV", "FCEDIT", "fignore", "fpath", "histchars",
|
|
||||||
"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 = [
|
||||||
@@ -74,3 +65,11 @@ 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"
|
||||||
|
]
|
||||||
|
14
ShellCheck/Options.hs
Normal file
14
ShellCheck/Options.hs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module ShellCheck.Options where
|
||||||
|
|
||||||
|
data Shell = Ksh | Sh | Bash
|
||||||
|
deriving (Show, Eq)
|
||||||
|
|
||||||
|
data AnalysisOptions = AnalysisOptions {
|
||||||
|
optionShellType :: Maybe Shell,
|
||||||
|
optionExcludes :: [Integer]
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultAnalysisOptions = AnalysisOptions {
|
||||||
|
optionShellType = Nothing,
|
||||||
|
optionExcludes = []
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
71
ShellCheck/Regex.hs
Normal file
71
ShellCheck/Regex.hs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{-
|
||||||
|
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
|
@@ -3,47 +3,33 @@
|
|||||||
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.Simple (shellCheck, ShellCheckComment, scLine, scColumn, scSeverity, scCode, scMessage) where
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
module ShellCheck.Simple (shellCheck, ShellCheckComment, scLine, scColumn, scSeverity, scCode, scMessage, runTests) where
|
||||||
|
|
||||||
import ShellCheck.Parser
|
|
||||||
import ShellCheck.Analytics
|
|
||||||
import Data.Maybe
|
|
||||||
import Text.Parsec.Pos
|
|
||||||
import Data.List
|
import Data.List
|
||||||
|
import Data.Maybe
|
||||||
|
import ShellCheck.Analytics hiding (runTests)
|
||||||
|
import ShellCheck.Options
|
||||||
|
import ShellCheck.Parser hiding (runTests)
|
||||||
|
import Test.QuickCheck.All (quickCheckAll)
|
||||||
|
import Text.Parsec.Pos
|
||||||
|
|
||||||
|
shellCheck :: AnalysisOptions -> String -> [ShellCheckComment]
|
||||||
prop_findsParseIssue =
|
shellCheck options script =
|
||||||
let comments = shellCheck "echo \"$12\"" [] in
|
let (ParseResult result notes) = parseShell options "-" script in
|
||||||
(length comments) == 1 && (scCode $ head comments) == 1037
|
let allNotes = notes ++ concat (maybeToList $ do
|
||||||
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
|
(tree, posMap) <- result
|
||||||
let list = runAnalytics options tree
|
let list = runAnalytics options tree
|
||||||
return $ map (noteToParseNote posMap) $ filterByAnnotation tree list
|
return $ map (noteToParseNote posMap) $ filterByAnnotation tree list
|
||||||
@@ -65,3 +51,30 @@ severityToString s =
|
|||||||
|
|
||||||
formatNote (ParseNote pos severity code text) =
|
formatNote (ParseNote pos severity code text) =
|
||||||
ShellCheckComment (sourceLine pos) (sourceColumn pos) (severityToString severity) (fromIntegral code) text
|
ShellCheckComment (sourceLine pos) (sourceColumn pos) (severityToString severity) (fromIntegral code) text
|
||||||
|
|
||||||
|
testCheck = shellCheck defaultAnalysisOptions { optionExcludes = [2148] } -- Ignore #! warnings
|
||||||
|
prop_findsParseIssue =
|
||||||
|
let comments = testCheck "echo \"$12\"" in
|
||||||
|
length comments == 1 && scCode (head comments) == 1037
|
||||||
|
prop_commentDisablesParseIssue1 =
|
||||||
|
null $ testCheck "#shellcheck disable=SC1037\necho \"$12\""
|
||||||
|
prop_commentDisablesParseIssue2 =
|
||||||
|
null $ testCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\""
|
||||||
|
|
||||||
|
prop_findsAnalysisIssue =
|
||||||
|
let comments = testCheck "echo $1" in
|
||||||
|
length comments == 1 && scCode (head comments) == 2086
|
||||||
|
prop_commentDisablesAnalysisIssue1 =
|
||||||
|
null $ testCheck "#shellcheck disable=SC2086\necho $1"
|
||||||
|
prop_commentDisablesAnalysisIssue2 =
|
||||||
|
null $ testCheck "#shellcheck disable=SC2086\n#lol\necho $1"
|
||||||
|
|
||||||
|
prop_optionDisablesIssue1 =
|
||||||
|
null $ shellCheck (defaultAnalysisOptions { optionExcludes = [2086, 2148] }) "echo $1"
|
||||||
|
|
||||||
|
prop_optionDisablesIssue2 =
|
||||||
|
null $ shellCheck (defaultAnalysisOptions { optionExcludes = [2148, 1037] }) "echo \"$10\""
|
||||||
|
|
||||||
|
return []
|
||||||
|
runTests = $quickCheckAll
|
||||||
|
|
||||||
|
@@ -16,25 +16,43 @@ 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 the 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*
|
|
||||||
|
|
||||||
: 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.
|
|
||||||
|
|
||||||
**-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...]
|
**-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...]
|
||||||
|
|
||||||
: Explicitly exclude the specified codes from the report. Subsequent **-e**
|
: Explicitly exclude the specified codes from the report. Subsequent **-e**
|
||||||
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* 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*,\ **--version**
|
||||||
|
|
||||||
|
: Print version and exit.
|
||||||
|
|
||||||
# FORMATS
|
# FORMATS
|
||||||
|
|
||||||
@@ -79,11 +97,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 +119,14 @@ For example, to suppress SC2035 about using `./*.jpg`:
|
|||||||
# shellcheck disable=SC2035
|
# shellcheck disable=SC2035
|
||||||
echo "Files: " *.jpg
|
echo "Files: " *.jpg
|
||||||
|
|
||||||
|
Here a shell brace group is used to suppress on multiple lines:
|
||||||
|
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
{
|
||||||
|
echo 'Modifying $PATH'
|
||||||
|
echo 'PATH=foo:$PATH' >> ~/.bashrc
|
||||||
|
}
|
||||||
|
|
||||||
Valid keys are:
|
Valid keys are:
|
||||||
|
|
||||||
**disable**
|
**disable**
|
||||||
|
226
shellcheck.hs
226
shellcheck.hs
@@ -3,57 +3,73 @@
|
|||||||
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 Control.Exception
|
import Control.Exception
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
|
import Control.Monad.Trans
|
||||||
|
import Control.Monad.Trans.Error
|
||||||
|
import Control.Monad.Trans.List
|
||||||
import Data.Char
|
import Data.Char
|
||||||
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
|
import Data.Monoid
|
||||||
import GHC.Exts
|
import GHC.Exts
|
||||||
import GHC.IO.Device
|
import GHC.IO.Device
|
||||||
import Prelude hiding (catch)
|
import Prelude hiding (catch)
|
||||||
import ShellCheck.Data
|
import ShellCheck.Data
|
||||||
|
import ShellCheck.Options
|
||||||
import ShellCheck.Simple
|
import ShellCheck.Simple
|
||||||
import ShellCheck.Analytics
|
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.Info
|
||||||
import System.IO
|
import System.IO
|
||||||
import Text.JSON
|
import Text.JSON
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
data Flag = Flag String String
|
data Flag = Flag String String
|
||||||
|
data Status = NoProblems | SomeProblems | BadInput | SupportFailure | SyntaxFailure | RuntimeException deriving (Ord, Eq)
|
||||||
|
|
||||||
|
data JsonComment = JsonComment FilePath ShellCheckComment
|
||||||
|
|
||||||
|
instance Error Status where
|
||||||
|
noMsg = RuntimeException
|
||||||
|
|
||||||
|
instance Monoid Status where
|
||||||
|
mempty = NoProblems
|
||||||
|
mappend = max
|
||||||
|
|
||||||
header = "Usage: shellcheck [OPTIONS...] FILES..."
|
header = "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 "s" ["shell"]
|
||||||
|
(ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (bash,sh,ksh)",
|
||||||
|
Option "V" ["version"]
|
||||||
(NoArg $ Flag "version" "true") "Print version information"
|
(NoArg $ Flag "version" "true") "Print version information"
|
||||||
]
|
]
|
||||||
|
|
||||||
printErr = hPutStrLn stderr
|
printErr = hPutStrLn stderr
|
||||||
|
|
||||||
syntaxFailure = ExitFailure 3
|
|
||||||
supportFailure = ExitFailure 4
|
|
||||||
|
|
||||||
instance JSON ShellCheckComment where
|
instance JSON (JsonComment) where
|
||||||
showJSON c = makeObj [
|
showJSON (JsonComment filename c) = makeObj [
|
||||||
|
("file", showJSON $ filename),
|
||||||
("line", showJSON $ scLine c),
|
("line", showJSON $ scLine c),
|
||||||
("column", showJSON $ scColumn c),
|
("column", showJSON $ scColumn c),
|
||||||
("level", showJSON $ scSeverity c),
|
("level", showJSON $ scSeverity c),
|
||||||
@@ -62,16 +78,15 @@ instance JSON ShellCheckComment where
|
|||||||
]
|
]
|
||||||
readJSON = undefined
|
readJSON = undefined
|
||||||
|
|
||||||
|
parseArguments :: [String] -> ErrorT 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
|
liftIO . printErr $ concat errors ++ "\n" ++ usageInfo header options
|
||||||
exitWith syntaxFailure
|
throwError SyntaxFailure
|
||||||
|
|
||||||
|
formats :: Map.Map String (AnalysisOptions -> [FilePath] -> IO Status)
|
||||||
formats = Map.fromList [
|
formats = Map.fromList [
|
||||||
("json", forJson),
|
("json", forJson),
|
||||||
("gcc", forGcc),
|
("gcc", forGcc),
|
||||||
@@ -79,9 +94,21 @@ formats = Map.fromList [
|
|||||||
("tty", forTty)
|
("tty", forTty)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
toStatus = liftM (either id (const NoProblems)) . runErrorT
|
||||||
|
|
||||||
|
catchExceptions :: IO Status -> IO Status
|
||||||
|
catchExceptions action = action -- action `catch` handler
|
||||||
|
where
|
||||||
|
handler err = do
|
||||||
|
printErr $ show (err :: SomeException)
|
||||||
|
return RuntimeException
|
||||||
|
|
||||||
|
checkComments comments = if null comments then NoProblems else SomeProblems
|
||||||
|
|
||||||
|
forTty :: AnalysisOptions -> [FilePath] -> IO Status
|
||||||
forTty options files = do
|
forTty options files = do
|
||||||
output <- mapM doFile files
|
output <- mapM doFile files
|
||||||
return $ and output
|
return $ mconcat output
|
||||||
where
|
where
|
||||||
clear = ansi 0
|
clear = ansi 0
|
||||||
ansi n = "\x1B[" ++ show n ++ "m"
|
ansi n = "\x1B[" ++ show n ++ "m"
|
||||||
@@ -97,7 +124,7 @@ forTty options files = do
|
|||||||
colorComment level comment =
|
colorComment level comment =
|
||||||
ansi (colorForLevel level) ++ comment ++ clear
|
ansi (colorForLevel level) ++ comment ++ clear
|
||||||
|
|
||||||
doFile path = do
|
doFile path = catchExceptions $ do
|
||||||
contents <- readContents path
|
contents <- readContents path
|
||||||
doInput path contents
|
doInput path contents
|
||||||
|
|
||||||
@@ -119,34 +146,39 @@ forTty options files = do
|
|||||||
mapM_ (\c -> putStrLn (colorFunc (scSeverity c) $ cuteIndent c)) x
|
mapM_ (\c -> putStrLn (colorFunc (scSeverity c) $ cuteIndent c)) x
|
||||||
putStrLn ""
|
putStrLn ""
|
||||||
) groups
|
) groups
|
||||||
return $ null comments
|
return . checkComments $ comments
|
||||||
|
|
||||||
cuteIndent comment =
|
cuteIndent comment =
|
||||||
replicate (scColumn comment - 1) ' ' ++
|
replicate (scColumn comment - 1) ' ' ++
|
||||||
"^-- " ++ code (scCode comment) ++ ": " ++ scMessage comment
|
"^-- " ++ code (scCode comment) ++ ": " ++ scMessage comment
|
||||||
|
|
||||||
code code = "SC" ++ (show code)
|
code code = "SC" ++ show code
|
||||||
|
|
||||||
getColorFunc = do
|
getColorFunc = do
|
||||||
term <- hIsTerminalDevice stdout
|
term <- hIsTerminalDevice stdout
|
||||||
return $ if term then colorComment else const id
|
let windows = "mingw" `isPrefixOf` os
|
||||||
|
return $ if term && not windows then colorComment else const id
|
||||||
|
|
||||||
-- This totally ignores the filenames. Fixme?
|
forJson :: AnalysisOptions -> [FilePath] -> IO Status
|
||||||
forJson options files = do
|
forJson options files = catchExceptions $ do
|
||||||
comments <- liftM concat $ mapM (commentsFor options) files
|
comments <- runListT $ do
|
||||||
|
file <- ListT $ return files
|
||||||
|
comment <- ListT $ commentsFor options file
|
||||||
|
return $ JsonComment file comment
|
||||||
putStrLn $ encodeStrict comments
|
putStrLn $ encodeStrict comments
|
||||||
return . null $ comments
|
return $ checkComments comments
|
||||||
|
|
||||||
-- Mimic GCC "file:line:col: (error|warning|note): message" format
|
-- Mimic GCC "file:line:col: (error|warning|note): message" format
|
||||||
|
forGcc :: AnalysisOptions -> [FilePath] -> IO Status
|
||||||
forGcc options files = do
|
forGcc options files = do
|
||||||
files <- mapM process files
|
files <- mapM process files
|
||||||
return $ and files
|
return $ mconcat files
|
||||||
where
|
where
|
||||||
process file = do
|
process file = catchExceptions $ do
|
||||||
contents <- readContents file
|
contents <- readContents file
|
||||||
let comments = makeNonVirtual (getComments options contents) contents
|
let comments = makeNonVirtual (getComments options contents) contents
|
||||||
mapM_ (putStrLn . format file) comments
|
mapM_ (putStrLn . format file) comments
|
||||||
return $ null comments
|
return $ checkComments comments
|
||||||
|
|
||||||
format filename c = concat [
|
format filename c = concat [
|
||||||
filename, ":",
|
filename, ":",
|
||||||
@@ -162,20 +194,18 @@ forGcc options files = do
|
|||||||
]
|
]
|
||||||
|
|
||||||
-- Checkstyle compatible output. A bit of a hack to avoid XML dependencies
|
-- Checkstyle compatible output. A bit of a hack to avoid XML dependencies
|
||||||
|
forCheckstyle :: AnalysisOptions -> [FilePath] -> IO Status
|
||||||
forCheckstyle options files = do
|
forCheckstyle options files = do
|
||||||
putStrLn "<?xml version='1.0' encoding='UTF-8'?>"
|
putStrLn "<?xml version='1.0' encoding='UTF-8'?>"
|
||||||
putStrLn "<checkstyle version='4.3'>"
|
putStrLn "<checkstyle version='4.3'>"
|
||||||
statuses <- mapM (\x -> process x `catch` report) files
|
statuses <- mapM process files
|
||||||
putStrLn "</checkstyle>"
|
putStrLn "</checkstyle>"
|
||||||
return $ and statuses
|
return $ mconcat statuses
|
||||||
where
|
where
|
||||||
process file = do
|
process file = catchExceptions $ do
|
||||||
comments <- commentsFor options file
|
comments <- commentsFor options file
|
||||||
putStrLn (formatFile file comments)
|
putStrLn (formatFile file comments)
|
||||||
return $ null comments
|
return $ checkComments comments
|
||||||
report error = do
|
|
||||||
printErr $ show (error :: SomeException)
|
|
||||||
return False
|
|
||||||
|
|
||||||
severity "error" = "error"
|
severity "error" = "error"
|
||||||
severity "warning" = "warning"
|
severity "warning" = "warning"
|
||||||
@@ -197,31 +227,31 @@ forCheckstyle options files = do
|
|||||||
attr "column" $ show . scColumn $ c,
|
attr "column" $ show . scColumn $ c,
|
||||||
attr "severity" $ severity . scSeverity $ c,
|
attr "severity" $ severity . scSeverity $ c,
|
||||||
attr "message" $ scMessage c,
|
attr "message" $ scMessage c,
|
||||||
attr "source" $ "ShellCheck.SC" ++ (show $ scCode c),
|
attr "source" $ "ShellCheck.SC" ++ show (scCode c),
|
||||||
"/>\n"
|
"/>\n"
|
||||||
]
|
]
|
||||||
|
|
||||||
commentsFor options file =
|
commentsFor options file = liftM (getComments options) $ readContents file
|
||||||
liftM (getComments options) $ readContents file
|
|
||||||
|
|
||||||
getComments options contents =
|
getComments = shellCheck
|
||||||
excludeCodes (getExclusions options) $ shellCheck contents analysisOptions
|
|
||||||
where
|
|
||||||
analysisOptions = catMaybes [ shellOption ]
|
|
||||||
shellOption = do
|
|
||||||
option <- getOption options "shell"
|
|
||||||
sh <- shellForExecutable option
|
|
||||||
return $ ForceShell sh
|
|
||||||
|
|
||||||
|
readContents :: FilePath -> IO String
|
||||||
readContents file = if file == "-" then getContents else readFile file
|
readContents file =
|
||||||
|
if file == "-"
|
||||||
|
then getContents
|
||||||
|
else readFile file
|
||||||
|
|
||||||
-- Realign comments from a tabstop of 8 to 1
|
-- Realign comments from a tabstop of 8 to 1
|
||||||
makeNonVirtual comments contents =
|
makeNonVirtual comments contents =
|
||||||
map fix comments
|
map fix comments
|
||||||
where
|
where
|
||||||
ls = lines contents
|
ls = lines contents
|
||||||
fix c = c { scColumn = real (ls !! (scLine c - 1)) 0 0 (scColumn c) }
|
fix c = c {
|
||||||
|
scColumn =
|
||||||
|
if scLine c > 0 && scLine c <= length ls
|
||||||
|
then real (ls !! (scLine c - 1)) 0 0 (scColumn c)
|
||||||
|
else scColumn c
|
||||||
|
}
|
||||||
real _ r v target | target <= v = r
|
real _ r v target | target <= v = r
|
||||||
real [] r v _ = r -- should never happen
|
real [] r v _ = r -- should never happen
|
||||||
real ('\t':rest) r v target =
|
real ('\t':rest) r v target =
|
||||||
@@ -240,7 +270,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]
|
||||||
|
|
||||||
@@ -257,45 +287,71 @@ excludeCodes codes =
|
|||||||
|
|
||||||
main = do
|
main = do
|
||||||
args <- getArgs
|
args <- getArgs
|
||||||
parsedArgs <- parseArguments args
|
status <- toStatus $ do
|
||||||
code <- do
|
(flags, files) <- parseArguments args
|
||||||
status <- process parsedArgs
|
process flags files
|
||||||
return $ if status then ExitSuccess else ExitFailure 1
|
exitWith $ statusToCode status
|
||||||
`catch` return
|
|
||||||
`catch` \err -> do
|
|
||||||
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
|
||||||
|
SomeProblems -> ExitFailure 1
|
||||||
|
BadInput -> ExitFailure 5
|
||||||
|
SyntaxFailure -> ExitFailure 3
|
||||||
|
SupportFailure -> ExitFailure 4
|
||||||
|
RuntimeException -> ExitFailure 2
|
||||||
|
|
||||||
|
process :: [Flag] -> [FilePath] -> ErrorT Status IO ()
|
||||||
|
process flags files = do
|
||||||
|
options <- foldM (flip parseOption) defaultAnalysisOptions flags
|
||||||
|
verifyFiles files
|
||||||
|
let format = fromMaybe "tty" $ getOption flags "format"
|
||||||
case Map.lookup format formats of
|
case Map.lookup format formats of
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
printErr $ "Unknown format " ++ format
|
liftIO $ do
|
||||||
printErr $ "Supported formats:"
|
printErr $ "Unknown format " ++ format
|
||||||
mapM_ (printErr . write) $ Map.keys formats
|
printErr "Supported formats:"
|
||||||
exitWith supportFailure
|
mapM_ (printErr . write) $ Map.keys formats
|
||||||
|
throwError SupportFailure
|
||||||
where write s = " " ++ s
|
where write s = " " ++ s
|
||||||
Just f -> do
|
Just f -> ErrorT $ liftM Left $ f options files
|
||||||
f options files
|
|
||||||
|
|
||||||
verifyOptions opts files = do
|
parseOption flag options =
|
||||||
when (isJust $ getOption opts "version") printVersionAndExit
|
case flag of
|
||||||
|
Flag "shell" str ->
|
||||||
|
fromMaybe (die $ "Unknown shell: " ++ str) $ do
|
||||||
|
shell <- shellForExecutable str
|
||||||
|
return $ return options { optionShellType = Just shell }
|
||||||
|
|
||||||
let shell = getOption opts "shell" in
|
Flag "exclude" str -> do
|
||||||
when (isJust shell && isNothing (shell >>= shellForExecutable)) $ do
|
new <- mapM parseNum $ split ',' str
|
||||||
printErr $ "Unknown shell: " ++ (fromJust shell)
|
let old = optionExcludes options
|
||||||
exitWith supportFailure
|
return options { optionExcludes = new ++ old }
|
||||||
|
|
||||||
|
Flag "version" _ -> do
|
||||||
|
liftIO printVersion
|
||||||
|
throwError NoProblems
|
||||||
|
|
||||||
|
_ -> return options
|
||||||
|
where
|
||||||
|
die s = do
|
||||||
|
liftIO $ printErr s
|
||||||
|
throwError SupportFailure
|
||||||
|
parseNum ('S':'C':str) = parseNum str
|
||||||
|
parseNum num = do
|
||||||
|
unless (all isDigit num) $ do
|
||||||
|
liftIO . printErr $ "Bad exclusion: " ++ num
|
||||||
|
throwError SyntaxFailure
|
||||||
|
return (Prelude.read num :: Integer)
|
||||||
|
|
||||||
|
verifyFiles files =
|
||||||
when (null files) $ do
|
when (null files) $ do
|
||||||
printErr "No files specified.\n"
|
liftIO $ printErr "No files specified.\n"
|
||||||
printErr $ usageInfo header options
|
liftIO $ printErr $ usageInfo header 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
|
|
||||||
|
@@ -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
|
|
||||||
|
|
16
test/shellcheck.hs
Normal file
16
test/shellcheck.hs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
module Main where
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import System.Exit
|
||||||
|
import qualified ShellCheck.Simple
|
||||||
|
import qualified ShellCheck.Analytics
|
||||||
|
import qualified ShellCheck.Parser
|
||||||
|
|
||||||
|
main = do
|
||||||
|
putStrLn "Running ShellCheck tests..."
|
||||||
|
results <- sequence [ShellCheck.Simple.runTests,
|
||||||
|
ShellCheck.Analytics.runTests,
|
||||||
|
ShellCheck.Parser.runTests]
|
||||||
|
if and results then exitSuccess
|
||||||
|
else exitFailure
|
||||||
|
|
Reference in New Issue
Block a user