mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 00:39:19 +08:00
Compare commits
214 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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
|
||||
*.o
|
||||
.tests
|
||||
jsoncheck
|
||||
shellcheck
|
||||
shellcheck.1
|
||||
# Created by http://www.gitignore.io
|
||||
|
||||
### Haskell ###
|
||||
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
|
||||
Version 3, 19 November 2007
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
@@ -7,15 +7,17 @@
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@@ -24,34 +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
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@@ -60,7 +72,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@@ -537,45 +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
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@@ -633,29 +635,40 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
You should have received a copy of the GNU General Public License
|
||||
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.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<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
|
||||
|
||||
Copyright 2012-2014, Vidar 'koala_man' Holen
|
||||
Licensed under the GNU Affero General Public License, v3
|
||||
Copyright 2012-2015, Vidar 'koala_man' Holen
|
||||
Licensed under the GNU General Public License, v3
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@@ -37,32 +57,54 @@ On Mac OS X with MacPorts (http://www.macports.org/):
|
||||
|
||||
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
|
||||
...
|
||||
|
||||
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
|
||||
~/.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
|
||||
Text.Regex. To run the unit tests, it also requires QuickCheck2.
|
||||
In Powershell ISE, you may need to additionally update the output encoding:
|
||||
|
||||
On Fedora, these can be installed with:
|
||||
> [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
yum install ghc ghc-parsec-devel ghc-QuickCheck-devel \
|
||||
ghc-json-devel ghc-regex-compat-devel
|
||||
## Running tests
|
||||
|
||||
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
|
||||
cabal configure --enable-tests
|
||||
cabal build
|
||||
cabal test
|
||||
|
||||
To build and run the tests, cd to the shellcheck source directory and:
|
||||
## Reporting bugs
|
||||
|
||||
$ make
|
||||
Please use the Github issue tracker for any bugs or feature suggestions:
|
||||
|
||||
https://github.com/koalaman/shellcheck/issues
|
||||
|
||||
Happy ShellChecking!
|
||||
|
45
Setup.hs
45
Setup.hs
@@ -1,2 +1,43 @@
|
||||
import Distribution.Simple
|
||||
main = defaultMain
|
||||
import Distribution.PackageDescription (
|
||||
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
|
||||
-- Must also be updated in ShellCheck/Data.hs :
|
||||
Version: 0.3.2
|
||||
Version: 0.4.1
|
||||
Synopsis: Shell script analysis tool
|
||||
License: OtherLicense
|
||||
License: GPL-3
|
||||
License-file: LICENSE
|
||||
Category: Static Analysis
|
||||
Author: Vidar Holen
|
||||
@@ -23,6 +22,15 @@ Description:
|
||||
* To point out subtle caveats, corner cases and pitfalls, that may cause an
|
||||
advanced user's otherwise working script to fail under future circumstances.
|
||||
|
||||
Extra-Source-Files:
|
||||
-- documentation
|
||||
README.md
|
||||
shellcheck.1.md
|
||||
-- built with a cabal sdist hook
|
||||
shellcheck.1
|
||||
-- tests
|
||||
test/shellcheck.hs
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: git://github.com/koalaman/shellcheck.git
|
||||
@@ -33,15 +41,27 @@ library
|
||||
containers,
|
||||
directory,
|
||||
json,
|
||||
mtl,
|
||||
mtl >= 2.2.1,
|
||||
parsec,
|
||||
regex-compat
|
||||
regex-tdfa,
|
||||
QuickCheck >= 2.7.4
|
||||
exposed-modules:
|
||||
ShellCheck.Analytics
|
||||
ShellCheck.AST
|
||||
ShellCheck.ASTLib
|
||||
ShellCheck.Analytics
|
||||
ShellCheck.Analyzer
|
||||
ShellCheck.Checker
|
||||
ShellCheck.Data
|
||||
ShellCheck.Formatter.Format
|
||||
ShellCheck.Formatter.CheckStyle
|
||||
ShellCheck.Formatter.GCC
|
||||
ShellCheck.Formatter.JSON
|
||||
ShellCheck.Formatter.TTY
|
||||
ShellCheck.Interface
|
||||
ShellCheck.Parser
|
||||
ShellCheck.Simple
|
||||
ShellCheck.Regex
|
||||
other-modules:
|
||||
Paths_ShellCheck
|
||||
|
||||
executable shellcheck
|
||||
build-depends:
|
||||
@@ -50,7 +70,23 @@ executable shellcheck
|
||||
containers,
|
||||
directory,
|
||||
json,
|
||||
mtl,
|
||||
mtl >= 2.2.1,
|
||||
parsec,
|
||||
regex-compat
|
||||
regex-tdfa,
|
||||
QuickCheck >= 2.7.4
|
||||
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.
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
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.AST where
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.Identity
|
||||
import qualified Text.Regex as Re
|
||||
import qualified ShellCheck.Regex as Re
|
||||
|
||||
data Id = Id Int deriving (Show, Eq, Ord)
|
||||
|
||||
@@ -28,16 +30,15 @@ data Dashed = Dashed | Undashed deriving (Show, Eq)
|
||||
data AssignmentMode = Assign | Append deriving (Show, Eq)
|
||||
data FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
|
||||
data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
|
||||
data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
|
||||
|
||||
data Token =
|
||||
TA_Base Id String Token
|
||||
| TA_Binary Id String Token Token
|
||||
| TA_Expansion Id Token
|
||||
| TA_Literal Id String
|
||||
TA_Binary Id String Token Token
|
||||
| TA_Expansion Id [Token]
|
||||
| TA_Index Id Token
|
||||
| TA_Sequence Id [Token]
|
||||
| TA_Trinary Id Token Token Token
|
||||
| TA_Unary Id String Token
|
||||
| TA_Variable Id String
|
||||
| TC_And Id ConditionType String Token Token
|
||||
| TC_Binary Id ConditionType String Token Token
|
||||
| TC_Group Id ConditionType Token
|
||||
@@ -48,16 +49,17 @@ data Token =
|
||||
| T_AndIf Id (Token) (Token)
|
||||
| T_Arithmetic Id Token
|
||||
| T_Array Id [Token]
|
||||
| T_IndexedElement Id Token Token
|
||||
| T_Assignment Id AssignmentMode String (Maybe Token) Token
|
||||
| T_Backgrounded Id Token
|
||||
| T_Backticked Id [Token]
|
||||
| T_Bang Id
|
||||
| T_Banged Id Token
|
||||
| T_BraceExpansion Id String
|
||||
| T_BraceExpansion Id [Token]
|
||||
| T_BraceGroup Id [Token]
|
||||
| T_CLOBBER Id
|
||||
| T_Case Id
|
||||
| T_CaseExpression Id Token [([Token],[Token])]
|
||||
| T_CaseExpression Id Token [(CaseType, [Token], [Token])]
|
||||
| T_Condition Id ConditionType Token
|
||||
| T_DGREAT Id
|
||||
| T_DLESS Id
|
||||
@@ -70,6 +72,7 @@ data Token =
|
||||
| T_DollarDoubleQuoted Id [Token]
|
||||
| T_DollarExpansion Id [Token]
|
||||
| T_DollarSingleQuoted Id String
|
||||
| T_DollarBraceCommandExpansion Id [Token]
|
||||
| T_Done Id
|
||||
| T_DoubleQuoted Id [Token]
|
||||
| T_EOF Id
|
||||
@@ -121,16 +124,21 @@ data Token =
|
||||
| T_WhileExpression Id [Token] [Token]
|
||||
| T_Annotation Id [Annotation] Token
|
||||
| T_Pipe Id String
|
||||
| T_CoProc Id (Maybe String) Token
|
||||
| T_CoProcBody Id Token
|
||||
| T_Include Id Token Token -- . & source: SimpleCommand T_Script
|
||||
deriving (Show)
|
||||
|
||||
data Annotation = DisableComment Integer deriving (Show, Eq)
|
||||
data Annotation = DisableComment Integer | SourceOverride String deriving (Show, Eq)
|
||||
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
|
||||
|
||||
-- I apologize for nothing!
|
||||
lolHax s = Re.subRegex (Re.mkRegex "(Id [0-9]+)") (show s) "(Id 0)"
|
||||
instance Eq Token where
|
||||
(==) a b = lolHax a == lolHax b
|
||||
-- This is an abomination.
|
||||
tokenEquals :: Token -> Token -> Bool
|
||||
tokenEquals a b = kludge a == kludge b
|
||||
where kludge s = Re.subRegex (Re.mkRegex "\\(Id [0-9]+\\)") (show s) "(Id 0)"
|
||||
|
||||
instance Eq Token where
|
||||
(==) = tokenEquals
|
||||
|
||||
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> Token) -> Token -> m Token
|
||||
analyze f g i =
|
||||
@@ -167,6 +175,8 @@ analyze f g i =
|
||||
delve (T_DoubleQuoted id list) = dl list $ T_DoubleQuoted id
|
||||
delve (T_DollarDoubleQuoted id list) = dl list $ T_DollarDoubleQuoted id
|
||||
delve (T_DollarExpansion id list) = dl list $ T_DollarExpansion id
|
||||
delve (T_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_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id
|
||||
delve (T_DollarBracket id c) = d1 c $ T_DollarBracket id
|
||||
@@ -178,6 +188,7 @@ analyze f g i =
|
||||
b <- round value
|
||||
return $ T_Assignment id mode var a b
|
||||
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
|
||||
a <- roundAll redirs
|
||||
b <- round cmd
|
||||
@@ -206,10 +217,10 @@ analyze f g i =
|
||||
delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v
|
||||
delve (T_CaseExpression id word cases) = do
|
||||
newWord <- round word
|
||||
newCases <- mapM (\(c, t) -> do
|
||||
newCases <- mapM (\(o, c, t) -> do
|
||||
x <- mapM round c
|
||||
y <- mapM round t
|
||||
return (x,y)
|
||||
return (o, x,y)
|
||||
) cases
|
||||
return $ T_CaseExpression id newWord newCases
|
||||
|
||||
@@ -242,9 +253,12 @@ analyze f g i =
|
||||
b <- round t2
|
||||
c <- round t3
|
||||
return $ TA_Trinary id a b c
|
||||
delve (TA_Expansion id t) = d1 t $ TA_Expansion id
|
||||
delve (TA_Base id b t) = d1 t $ TA_Base id b
|
||||
delve (TA_Expansion id t) = dl t $ TA_Expansion id
|
||||
delve (TA_Index id t) = d1 t $ TA_Index id
|
||||
delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns
|
||||
delve (T_CoProc id var body) = d1 body $ T_CoProc id var
|
||||
delve (T_CoProcBody id t) = d1 t $ T_CoProcBody id
|
||||
delve (T_Include id includer script) = d2 includer script $ T_Include id
|
||||
delve t = return t
|
||||
|
||||
getId t = case t of
|
||||
@@ -290,12 +304,14 @@ getId t = case t of
|
||||
T_DollarBraced id _ -> id
|
||||
T_DollarArithmetic id _ -> id
|
||||
T_BraceExpansion id _ -> id
|
||||
T_DollarBraceCommandExpansion id _ -> id
|
||||
T_IoFile id _ _ -> id
|
||||
T_HereDoc id _ _ _ _ -> id
|
||||
T_HereString id _ -> id
|
||||
T_FdRedirect id _ _ -> id
|
||||
T_Assignment id _ _ _ _ -> id
|
||||
T_Array id _ -> id
|
||||
T_IndexedElement id _ _ -> id
|
||||
T_Redirecting id _ _ -> id
|
||||
T_SimpleCommand id _ _ -> id
|
||||
T_Pipeline id _ _ -> id
|
||||
@@ -326,11 +342,9 @@ getId t = case t of
|
||||
TA_Binary id _ _ _ -> id
|
||||
TA_Unary id _ _ -> id
|
||||
TA_Sequence id _ -> id
|
||||
TA_Variable id _ -> id
|
||||
TA_Trinary id _ _ _ -> id
|
||||
TA_Expansion id _ -> id
|
||||
TA_Literal id _ -> id
|
||||
TA_Base id _ _ -> id
|
||||
TA_Index id _ -> id
|
||||
T_ProcSub id _ _ -> id
|
||||
T_Glob id _ -> id
|
||||
T_ForArithmetic id _ _ _ _ -> id
|
||||
@@ -339,6 +353,9 @@ getId t = case t of
|
||||
T_DollarBracket id _ -> id
|
||||
T_Annotation id _ _ -> id
|
||||
T_Pipe id _ -> id
|
||||
T_CoProc id _ _ -> id
|
||||
T_CoProcBody id _ -> id
|
||||
T_Include id _ _ -> id
|
||||
|
||||
blank :: Monad m => Token -> m ()
|
||||
blank = const $ return ()
|
||||
@@ -346,10 +363,3 @@ doAnalysis f = analyze f blank id
|
||||
doStackAnalysis startToken endToken = analyze startToken endToken id
|
||||
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
|
||||
|
240
ShellCheck/ASTLib.hs
Normal file
240
ShellCheck/ASTLib.hs
Normal file
@@ -0,0 +1,240 @@
|
||||
{-
|
||||
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).
|
||||
getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
|
||||
let textArgs = takeWhile (not . stopCondition . snd) $ map (\x -> (x, concat $ oversimplify x)) args in
|
||||
concatMap flag textArgs
|
||||
where
|
||||
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
|
||||
flag (x, '-':args) = map (\v -> (x, [v])) args
|
||||
flag _ = []
|
||||
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
|
||||
|
||||
-- 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
27
ShellCheck/Analyzer.hs
Normal file
27
ShellCheck/Analyzer.hs
Normal file
@@ -0,0 +1,27 @@
|
||||
{-
|
||||
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.Interface
|
||||
import ShellCheck.Analytics
|
||||
|
||||
-- TODO: Clean up the cruft this is layered on
|
||||
analyzeScript :: AnalysisSpec -> AnalysisResult
|
||||
analyzeScript = runAnalytics
|
159
ShellCheck/Checker.hs
Normal file
159
ShellCheck/Checker.hs
Normal file
@@ -0,0 +1,159 @@
|
||||
{-
|
||||
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_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
|
@@ -1,6 +1,10 @@
|
||||
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 = [
|
||||
-- Generic
|
||||
@@ -24,22 +28,10 @@ internalVariables = [
|
||||
"LC_MESSAGES", "LC_NUMERIC", "LINES", "MAIL", "MAILCHECK", "MAILPATH",
|
||||
"OPTERR", "PATH", "POSIXLY_CORRECT", "PROMPT_COMMAND",
|
||||
"PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT",
|
||||
"TMOUT", "TMPDIR", "auto_resume", "histchars",
|
||||
"TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC",
|
||||
|
||||
-- Zsh
|
||||
"ARGV0", "BAUD", "cdpath", "COLUMNS", "CORRECT_IGNORE",
|
||||
"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"
|
||||
-- Other
|
||||
"USER", "TZ", "TERM"
|
||||
]
|
||||
|
||||
variablesWithoutSpaces = [
|
||||
@@ -74,3 +66,23 @@ commonCommands = [
|
||||
"val", "vi", "wait", "wc", "what", "who", "write", "xargs", "yacc",
|
||||
"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 "sh" = return Sh
|
||||
shellForExecutable "ash" = return Sh
|
||||
shellForExecutable "dash" = return Sh
|
||||
|
||||
shellForExecutable "ksh" = return Ksh
|
||||
shellForExecutable "ksh88" = return Ksh
|
||||
shellForExecutable "ksh93" = return Ksh
|
||||
|
||||
shellForExecutable "bash" = return Bash
|
||||
shellForExecutable _ = 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
|
||||
|
86
ShellCheck/Formatter/TTY.hs
Normal file
86
ShellCheck/Formatter/TTY.hs
Normal file
@@ -0,0 +1,86 @@
|
||||
{-
|
||||
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 :: IO Formatter
|
||||
format = return Formatter {
|
||||
header = return (),
|
||||
footer = return (),
|
||||
onFailure = outputError,
|
||||
onResult = outputResult
|
||||
}
|
||||
|
||||
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 file error = do
|
||||
color <- getColorFunc
|
||||
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
|
||||
|
||||
outputResult result contents = do
|
||||
color <- getColorFunc
|
||||
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 = do
|
||||
term <- hIsTerminalDevice stdout
|
||||
let windows = "mingw" `isPrefixOf` os
|
||||
return $ if term && not windows then colorComment else const id
|
||||
where
|
||||
colorComment level comment =
|
||||
ansi (colorForLevel level) ++ comment ++ clear
|
||||
clear = ansi 0
|
||||
ansi n = "\x1B[" ++ show n ++ "m"
|
103
ShellCheck/Interface.hs
Normal file
103
ShellCheck/Interface.hs
Normal file
@@ -0,0 +1,103 @@
|
||||
{-
|
||||
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]
|
||||
}
|
||||
|
||||
-- Supporting data types
|
||||
data Shell = Ksh | Sh | Bash 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)
|
||||
|
||||
-- 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
|
||||
|
1219
ShellCheck/Parser.hs
1219
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
|
@@ -16,25 +16,50 @@ errors and pitfalls where the shell just gives a cryptic error message or
|
||||
strange behavior, but it also reports on a few more advanced issues where
|
||||
corner cases can cause delayed failures.
|
||||
|
||||
ShellCheck gives shell specific advice. Consider 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
|
||||
|
||||
**-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*...]
|
||||
|
||||
: Explicitly exclude the specified codes from the report. Subsequent **-e**
|
||||
options are cumulative, but all the codes can be specified at once,
|
||||
comma-separated as a single argument.
|
||||
|
||||
**-f** *FORMAT*, **--format=***FORMAT*
|
||||
|
||||
: Specify the output format of shellcheck, which prints its results in the
|
||||
standard output. Subsequent **-f** options are ignored, see **FORMATS**
|
||||
below for more information.
|
||||
|
||||
**-s**\ *shell*,\ **--shell=***shell*
|
||||
|
||||
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *ksh* and
|
||||
*zsh*. The default is to use the file's shebang, or *bash* if the target
|
||||
shell can't be determined.
|
||||
: Specify Bourne shell dialect. Valid values are *sh*, *bash* and *ksh*.
|
||||
The default is to use the file's shebang, or *bash* if the target shell
|
||||
can't be determined.
|
||||
|
||||
**-V**,\ **--version**
|
||||
|
||||
: Print version information and exit.
|
||||
|
||||
**-x**,\ **-external-sources**
|
||||
|
||||
: Follow 'source' statements even when the file is not specified as input.
|
||||
By default, `shellcheck` will only follow files specified on the command
|
||||
line (plus `/dev/null`). This option allows following any file the script
|
||||
may `source`.
|
||||
|
||||
# FORMATS
|
||||
|
||||
@@ -79,11 +104,12 @@ corner cases can cause delayed failures.
|
||||
|
||||
[
|
||||
{
|
||||
"line": line,
|
||||
"column": column,
|
||||
"level": level,
|
||||
"code": ####,
|
||||
"message": message
|
||||
"file": "filename",
|
||||
"line": lineNumber,
|
||||
"column": columnNumber,
|
||||
"level": "severitylevel",
|
||||
"code": errorCode,
|
||||
"message": "warning message"
|
||||
},
|
||||
...
|
||||
]
|
||||
@@ -100,6 +126,19 @@ For example, to suppress SC2035 about using `./*.jpg`:
|
||||
# shellcheck disable=SC2035
|
||||
echo "Files: " *.jpg
|
||||
|
||||
To tell ShellCheck where to look for an otherwise dynamically determined file:
|
||||
|
||||
# shellcheck source=./lib.sh
|
||||
source "$(find_install_dir)/lib.sh"
|
||||
|
||||
Here a shell brace group is used to suppress a warning on multiple lines:
|
||||
|
||||
# shellcheck disable=SC2016
|
||||
{
|
||||
echo 'Modifying $PATH'
|
||||
echo 'PATH=foo:$PATH' >> ~/.bashrc
|
||||
}
|
||||
|
||||
Valid keys are:
|
||||
|
||||
**disable**
|
||||
@@ -107,6 +146,18 @@ Valid keys are:
|
||||
The command can be a simple command like `echo foo`, or a compound command
|
||||
like a function definition, subshell block or loop.
|
||||
|
||||
**source**
|
||||
: Overrides the filename included by a `source`/`.` statement. This can be
|
||||
used to tell shellcheck where to look for a file whose name is determined
|
||||
at runtime, or to skip a source by telling it to use `/dev/null`.
|
||||
|
||||
# ENVIRONMENT VARIABLES
|
||||
The environment variable `SHELLCHECK_OPTS` can be set with default flags:
|
||||
|
||||
export SHELLCHECK_OPTS='--shell=bash --exclude=SC2016'
|
||||
|
||||
Its value will be split on spaces and prepended to the command line on each
|
||||
invocation.
|
||||
|
||||
# AUTHOR
|
||||
ShellCheck is written and maintained by Vidar Holen.
|
||||
@@ -116,6 +167,12 @@ Bugs and issues can be reported on GitHub:
|
||||
|
||||
https://github.com/koalaman/shellcheck/issues
|
||||
|
||||
# COPYRIGHT
|
||||
Copyright 2012-2015, Vidar Holen.
|
||||
Licensed under the GNU General Public License version 3 or later,
|
||||
see http://gnu.org/licenses/gpl.html
|
||||
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
sh(1) bash(1)
|
||||
|
436
shellcheck.hs
436
shellcheck.hs
@@ -1,233 +1,105 @@
|
||||
{-
|
||||
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 Affero General Public License as published by
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-}
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Checker
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Regex
|
||||
|
||||
import ShellCheck.Formatter.Format
|
||||
import qualified ShellCheck.Formatter.CheckStyle
|
||||
import qualified ShellCheck.Formatter.GCC
|
||||
import qualified ShellCheck.Formatter.JSON
|
||||
import qualified ShellCheck.Formatter.TTY
|
||||
|
||||
import Control.Exception
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Data.Char
|
||||
import Data.Functor
|
||||
import Data.Either
|
||||
import qualified Data.Map as Map
|
||||
import Data.Maybe
|
||||
import GHC.Exts
|
||||
import GHC.IO.Device
|
||||
import Data.Monoid
|
||||
import Prelude hiding (catch)
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Simple
|
||||
import ShellCheck.Analytics
|
||||
import System.Console.GetOpt
|
||||
import System.Directory
|
||||
import System.Environment
|
||||
import System.Exit
|
||||
import System.IO
|
||||
import Text.JSON
|
||||
import qualified Data.Map as Map
|
||||
|
||||
data Flag = Flag String String
|
||||
data Status =
|
||||
NoProblems
|
||||
| SomeProblems
|
||||
| BadInput
|
||||
| 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
|
||||
}
|
||||
|
||||
defaultOptions = Options {
|
||||
checkSpec = emptyCheckSpec,
|
||||
externalSources = False
|
||||
}
|
||||
|
||||
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
|
||||
options = [
|
||||
Option ['f'] ["format"]
|
||||
(ReqArg (Flag "format") "FORMAT") "output format",
|
||||
Option ['e'] ["exclude"]
|
||||
Option "e" ["exclude"]
|
||||
(ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings",
|
||||
Option ['s'] ["shell"]
|
||||
(ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (bash,sh,ksh,zsh)",
|
||||
Option ['V'] ["version"]
|
||||
Option "f" ["format"]
|
||||
(ReqArg (Flag "format") "FORMAT") "output format",
|
||||
Option "s" ["shell"]
|
||||
(ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (bash,sh,ksh)",
|
||||
Option "x" ["external-sources"]
|
||||
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES.",
|
||||
Option "V" ["version"]
|
||||
(NoArg $ Flag "version" "true") "Print version information"
|
||||
]
|
||||
|
||||
printErr = 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
|
||||
printErr = lift . hPutStrLn stderr
|
||||
|
||||
parseArguments :: [String] -> ExceptT Status IO ([Flag], [FilePath])
|
||||
parseArguments argv =
|
||||
case getOpt Permute options argv of
|
||||
(opts, files, []) -> do
|
||||
verifyOptions opts files
|
||||
return $ Just (opts, files)
|
||||
|
||||
(opts, files, []) -> return (opts, files)
|
||||
(_, _, errors) -> do
|
||||
printErr $ concat errors ++ "\n" ++ usageInfo header options
|
||||
exitWith syntaxFailure
|
||||
printErr $ concat errors ++ "\n" ++ usageInfo usageHeader options
|
||||
throwError SyntaxFailure
|
||||
|
||||
formats :: Map.Map String (IO Formatter)
|
||||
formats = Map.fromList [
|
||||
("json", forJson),
|
||||
("gcc", forGcc),
|
||||
("checkstyle", forCheckstyle),
|
||||
("tty", forTty)
|
||||
("checkstyle", ShellCheck.Formatter.CheckStyle.format),
|
||||
("gcc", ShellCheck.Formatter.GCC.format),
|
||||
("json", ShellCheck.Formatter.JSON.format),
|
||||
("tty", ShellCheck.Formatter.TTY.format)
|
||||
]
|
||||
|
||||
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 (Flag var val:_) name | name == var = return val
|
||||
getOption (_:rest) flag = getOption rest flag
|
||||
@@ -240,7 +112,7 @@ split char str =
|
||||
where
|
||||
split' (a:rest) element =
|
||||
if a == char
|
||||
then (reverse element) : split' rest []
|
||||
then reverse element : split' rest []
|
||||
else split' rest (a:element)
|
||||
split' [] element = [reverse element]
|
||||
|
||||
@@ -250,52 +122,168 @@ getExclusions options =
|
||||
in
|
||||
map (Prelude.read . clean) elements :: [Int]
|
||||
|
||||
excludeCodes codes =
|
||||
filter (not . hasCode)
|
||||
toStatus = liftM (either id id) . runExceptT
|
||||
|
||||
getEnvArgs = do
|
||||
opts <- getEnv "SHELLCHECK_OPTS" `catch` cantWaitForLookupEnv
|
||||
return . filter (not . null) $ opts `splitOn` mkRegex " +"
|
||||
where
|
||||
hasCode c = scCode c `elem` codes
|
||||
cantWaitForLookupEnv :: IOException -> IO String
|
||||
cantWaitForLookupEnv = const $ return ""
|
||||
|
||||
main = do
|
||||
args <- getArgs
|
||||
parsedArgs <- parseArguments args
|
||||
code <- do
|
||||
status <- process parsedArgs
|
||||
return $ if status then ExitSuccess else ExitFailure 1
|
||||
`catch` return
|
||||
`catch` \err -> do
|
||||
printErr $ show (err :: SomeException)
|
||||
return $ ExitFailure 2
|
||||
exitWith code
|
||||
params <- getArgs
|
||||
envOpts <- getEnvArgs
|
||||
let args = envOpts ++ params
|
||||
status <- toStatus $ do
|
||||
(flags, files) <- parseArguments args
|
||||
process flags files
|
||||
exitWith $ statusToCode status
|
||||
|
||||
process Nothing = return False
|
||||
process (Just (options, files)) =
|
||||
let format = fromMaybe "tty" $ getOption options "format" in
|
||||
statusToCode status =
|
||||
case status of
|
||||
NoProblems -> ExitSuccess
|
||||
SomeProblems -> ExitFailure 1
|
||||
BadInput -> ExitFailure 5
|
||||
SyntaxFailure -> ExitFailure 3
|
||||
SupportFailure -> ExitFailure 4
|
||||
RuntimeException -> ExitFailure 2
|
||||
|
||||
process :: [Flag] -> [FilePath] -> ExceptT Status IO Status
|
||||
process flags files = do
|
||||
options <- foldM (flip parseOption) defaultOptions flags
|
||||
verifyFiles files
|
||||
let format = fromMaybe "tty" $ getOption flags "format"
|
||||
formatter <-
|
||||
case Map.lookup format formats of
|
||||
Nothing -> do
|
||||
printErr $ "Unknown format " ++ format
|
||||
printErr $ "Supported formats:"
|
||||
printErr "Supported formats:"
|
||||
mapM_ (printErr . write) $ Map.keys formats
|
||||
exitWith supportFailure
|
||||
throwError SupportFailure
|
||||
where write s = " " ++ s
|
||||
Just f -> do
|
||||
f options files
|
||||
Just f -> ExceptT $ fmap Right f
|
||||
sys <- lift $ ioInterface options files
|
||||
lift $ runFormatter sys formatter options files
|
||||
|
||||
verifyOptions opts files = do
|
||||
when (isJust $ getOption opts "version") printVersionAndExit
|
||||
runFormatter :: SystemInterface IO -> Formatter -> Options -> [FilePath]
|
||||
-> IO Status
|
||||
runFormatter sys format options files = do
|
||||
header format
|
||||
result <- foldM f NoProblems files
|
||||
footer format
|
||||
return result
|
||||
where
|
||||
f :: Status -> FilePath -> IO Status
|
||||
f status file = do
|
||||
newStatus <- process file `catch` handler file
|
||||
return $ status `mappend` newStatus
|
||||
handler :: FilePath -> IOException -> IO Status
|
||||
handler file e = do
|
||||
onFailure format file (show e)
|
||||
return RuntimeException
|
||||
|
||||
let shell = getOption opts "shell" in
|
||||
when (isJust shell && isNothing (shell >>= shellForExecutable)) $ do
|
||||
printErr $ "Unknown shell: " ++ (fromJust shell)
|
||||
exitWith supportFailure
|
||||
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
|
||||
|
||||
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" _ -> do
|
||||
return options {
|
||||
externalSources = True
|
||||
}
|
||||
|
||||
_ -> 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
|
||||
printErr "No files specified.\n"
|
||||
printErr $ usageInfo header options
|
||||
exitWith syntaxFailure
|
||||
printErr $ usageInfo usageHeader options
|
||||
throwError SyntaxFailure
|
||||
|
||||
printVersionAndExit = do
|
||||
putStrLn $ "ShellCheck - shell script analysis tool"
|
||||
printVersion = do
|
||||
putStrLn "ShellCheck - shell script analysis tool"
|
||||
putStrLn $ "version: " ++ shellcheckVersion
|
||||
putStrLn $ "license: GNU Affero General Public License, version 3"
|
||||
putStrLn $ "website: http://www.shellcheck.net"
|
||||
exitWith ExitSuccess
|
||||
putStrLn "license: GNU General Public License, version 3"
|
||||
putStrLn "website: http://www.shellcheck.net"
|
||||
|
@@ -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
|
||||
|
18
test/shellcheck.hs
Normal file
18
test/shellcheck.hs
Normal file
@@ -0,0 +1,18 @@
|
||||
module Main where
|
||||
|
||||
import Control.Monad
|
||||
import System.Exit
|
||||
import qualified ShellCheck.Checker
|
||||
import qualified ShellCheck.Analytics
|
||||
import qualified ShellCheck.Parser
|
||||
|
||||
main = do
|
||||
putStrLn "Running ShellCheck tests..."
|
||||
results <- sequence [
|
||||
ShellCheck.Checker.runTests,
|
||||
ShellCheck.Analytics.runTests,
|
||||
ShellCheck.Parser.runTests
|
||||
]
|
||||
if and results
|
||||
then exitSuccess
|
||||
else exitFailure
|
Reference in New Issue
Block a user