mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 08:49:20 +08:00
Compare commits
255 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
c5479b8ca3 | ||
|
d9dd58bec8 | ||
|
af1bb93aba | ||
|
e909c8ac42 | ||
|
93140e31a0 | ||
|
97f3834852 | ||
|
0369f43bac | ||
|
eb2eae2888 | ||
|
30c0c1f27d | ||
|
bff5d11566 | ||
|
eccb9f3f71 | ||
|
2814572116 | ||
|
90bafb9aba | ||
|
39b88bbaac | ||
|
39805ab200 | ||
|
9dadce96c0 | ||
|
1a0e208cc3 | ||
|
a69e27b774 | ||
|
b05c12223f | ||
|
38ead0385b | ||
|
9e8a11e57c | ||
|
6b84b35ec0 | ||
|
669fdf8e5e | ||
|
dccfb3c4a1 | ||
|
40ce949a56 | ||
|
9f3802138f | ||
|
2f3533fff6 | ||
|
f9c346cfd7 | ||
|
5f7419ca37 | ||
|
8494509150 | ||
|
8ba1f2fdf2 | ||
|
dbadca9f61 | ||
|
0347ce1b7a | ||
|
7fbe66e1c6 | ||
|
b000b05507 | ||
|
39423ddf81 | ||
|
875c2d2aad | ||
|
64cc7c691a | ||
|
b9784cbcc0 | ||
|
1a3f6aadaf | ||
|
35756c2cd6 | ||
|
0fd351404f | ||
|
4caa7e7900 | ||
|
c11c0196d5 | ||
|
b035331d4a | ||
|
d13253973b | ||
|
d9c622ae33 | ||
|
aac7d76047 | ||
|
fc421adb45 | ||
|
e0d3c6923a | ||
|
9772ba9de4 | ||
|
3a944de606 | ||
|
3dd592a02a | ||
|
61531cbb10 | ||
|
d53087f056 | ||
|
39756b420e | ||
|
52d4efc951 | ||
|
5dac723593 | ||
|
2364fd58b6 | ||
|
cde364c97b | ||
|
98b790f87a | ||
|
726a4e5848 | ||
|
0a9ed917e7 | ||
|
b10d31c8b7 | ||
|
133c779701 | ||
|
b18ee3fdef | ||
|
3fcc6c44d8 | ||
|
d830a36bc8 | ||
|
1af23fd131 | ||
|
d21b3362b2 | ||
|
6cd454e88b | ||
|
0b5f6b9762 | ||
|
3824e9cfc2 | ||
|
fdce0116da | ||
|
b069f7ed27 | ||
|
c4181d45d2 | ||
|
680f838c63 | ||
|
e6d81ca7b7 | ||
|
fd909eeca0 | ||
|
deab146fab | ||
|
f9aeabc245 | ||
|
558d8ffc6c | ||
|
e96c4c3ffa | ||
|
c566efd442 | ||
|
47c220d59c | ||
|
4bd902c5c4 | ||
|
033ce6d941 | ||
|
6ad3f557fe | ||
|
d0bad6c057 | ||
|
58c362f97c | ||
|
5f568dd207 | ||
|
2c1e414ac5 | ||
|
6699109ab8 | ||
|
423ca82296 | ||
|
0a263579e0 | ||
|
d63406abe4 | ||
|
81956d324d | ||
|
f549aad809 | ||
|
f9f965693d | ||
|
727d940e10 | ||
|
c26c2b8536 | ||
|
d8878ed852 | ||
|
c3cc5f649f | ||
|
8bd4365cdb | ||
|
a00a6fb53b | ||
|
3332eba9a0 | ||
|
ad08bb64aa | ||
|
f01e6e1a99 | ||
|
de0145fb29 | ||
|
0d4ae95e1d | ||
|
50db49e2fb | ||
|
60aafae21d | ||
|
902cb9c303 | ||
|
4f1fd43360 | ||
|
ca5af5c55a | ||
|
503cac3bb3 | ||
|
2a9c9ae0ad | ||
|
def4551991 | ||
|
67f4a0d6eb | ||
|
f92f934688 | ||
|
d4059c30b7 | ||
|
b68de7f42b | ||
|
7dacb62d36 | ||
|
3423cde931 | ||
|
b2d1aa01f7 | ||
|
19e1bdf11f | ||
|
75d51087c8 | ||
|
ed524fb77f | ||
|
97045c4af1 | ||
|
1b806f6c9f | ||
|
632c1614a1 | ||
|
00d9ef12e7 | ||
|
d07294810b | ||
|
948b750754 | ||
|
41ae95116d | ||
|
bf3c942294 | ||
|
055b40462d | ||
|
b087b7efb1 | ||
|
5d8d57cf07 | ||
|
661091a9da | ||
|
2ec60c2627 | ||
|
8b4909b238 | ||
|
95a3be6546 | ||
|
968e34e002 | ||
|
197b3e3f20 | ||
|
0e464ea476 | ||
|
811df6f0da | ||
|
4e5d32b05a | ||
|
c5141b77bf | ||
|
9dfeb6b42a | ||
|
77916d2645 | ||
|
4968e7d9ff | ||
|
075d58ee90 | ||
|
6a4a5a815e | ||
|
76a39f254b | ||
|
8ec9fa43fd | ||
|
e8634a3c27 | ||
|
9ae776530b | ||
|
0ec62390d5 | ||
|
82328cd86e | ||
|
5b58da7249 | ||
|
8676517270 | ||
|
4262c4b1bf | ||
|
7ad0110443 | ||
|
e9bba2f75a | ||
|
74ea5eaeec | ||
|
b7ee5f4410 | ||
|
e294db171e | ||
|
8c3d8d7cfa | ||
|
380d6c3317 | ||
|
16bd52333a | ||
|
cfb44b3fe2 | ||
|
43ed5e748d | ||
|
4dca88aade | ||
|
1d2c7a8551 | ||
|
ba080e7e34 | ||
|
fc716738eb | ||
|
659709d529 | ||
|
5b4729d940 | ||
|
b936f28763 | ||
|
78d9a7ad97 | ||
|
d540a98d33 | ||
|
8c00850134 | ||
|
d1990e3396 | ||
|
91fc4a046c | ||
|
95ebe1cd07 | ||
|
27822a1f56 | ||
|
eb06b06475 | ||
|
5d72432046 | ||
|
da51b14789 | ||
|
7be8485b8b | ||
|
a4d36ba0d2 | ||
|
d4bc0f6e10 | ||
|
1011ae7b3c | ||
|
d603ee1e89 | ||
|
4fc518c877 | ||
|
7fda86d6e2 | ||
|
6905373b6c | ||
|
1d8401d583 | ||
|
a89aee1a34 | ||
|
4853dce3fe | ||
|
a793e09bab | ||
|
fbd85e93ee | ||
|
77f754fa32 | ||
|
01d557abe6 | ||
|
68cc00b6e8 | ||
|
8b7c0be06f | ||
|
473bb666d8 | ||
|
376d407ea1 | ||
|
2e13cedc4b | ||
|
17515ad706 | ||
|
d8b5d6393a | ||
|
d404bc703d | ||
|
e5e08df1d9 | ||
|
1988cba147 | ||
|
4cee7fd27f | ||
|
b75fe02aac | ||
|
83c3dd3418 | ||
|
020850dbbb | ||
|
8d265aa25e | ||
|
c343217fd2 | ||
|
71bc26aefa | ||
|
8a3d259ae6 | ||
|
3a9ae0ebf1 | ||
|
d6b903e6cc | ||
|
b9f7f82e29 | ||
|
6d0bfcf37a | ||
|
e0bbb89d00 | ||
|
a0a58d432a | ||
|
206900fb64 | ||
|
794a5523d1 | ||
|
389c7b670c | ||
|
b1af7bb8f2 | ||
|
157fea73da | ||
|
b439f02b8e | ||
|
710a28c572 | ||
|
702d57b655 | ||
|
34e69556b1 | ||
|
7c411b39ac | ||
|
5a959bc340 | ||
|
fb5f72951d | ||
|
7630136d6c | ||
|
dacb8c597f | ||
|
d99aaaf8dc | ||
|
876831b419 | ||
|
24580609b8 | ||
|
5828abe324 | ||
|
c229d3929a | ||
|
31907ca51d | ||
|
58b8e0ab70 | ||
|
9586a46c9c | ||
|
bb49cf8e65 | ||
|
de1fa61560 | ||
|
07b1fd6f44 | ||
|
d0caa1e1df |
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Created by http://www.gitignore.io
|
||||||
|
|
||||||
|
### Haskell ###
|
||||||
|
dist
|
||||||
|
cabal-dev
|
||||||
|
*.o
|
||||||
|
*.hi
|
||||||
|
*.chi
|
||||||
|
*.chs.h
|
||||||
|
.virtualenv
|
||||||
|
.hsenv
|
||||||
|
.cabal-sandbox/
|
||||||
|
cabal.sandbox.config
|
||||||
|
cabal.config
|
||||||
|
|
24
Makefile
24
Makefile
@@ -1,24 +0,0 @@
|
|||||||
# TODO: Phase out Makefile in favor of Cabal
|
|
||||||
|
|
||||||
GHCFLAGS=-O9
|
|
||||||
|
|
||||||
all: shellcheck jsoncheck .tests
|
|
||||||
: Done
|
|
||||||
|
|
||||||
shellcheck: regardless
|
|
||||||
: Conditionally compiling shellcheck
|
|
||||||
ghc $(GHCFLAGS) --make shellcheck
|
|
||||||
|
|
||||||
jsoncheck: regardless
|
|
||||||
: Conditionally compiling shellcheck
|
|
||||||
ghc $(GHCFLAGS) --make jsoncheck
|
|
||||||
|
|
||||||
.tests: *.hs */*.hs
|
|
||||||
: Running unit tests
|
|
||||||
./test/runQuack && touch .tests
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -f .tests shellcheck *.hi *.o ShellCheck/*.hi ShellCheck/*.o
|
|
||||||
|
|
||||||
regardless:
|
|
||||||
|
|
26
README
26
README
@@ -1,26 +0,0 @@
|
|||||||
ShellCheck - A shell script static analysis tool
|
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
|
||||||
|
|
||||||
Copyright 2012, Vidar 'koala_man' Holen
|
|
||||||
Licensed under the GNU Affero General Public License, v3
|
|
||||||
|
|
||||||
The goals of ShellCheck are:
|
|
||||||
|
|
||||||
- To point out and clarify typical beginner's syntax issues,
|
|
||||||
that causes a shell to give cryptic error messages.
|
|
||||||
|
|
||||||
- To point out and clarify typical intermediate level semantic problems,
|
|
||||||
that causes a shell to behave strangely and counter-intuitively.
|
|
||||||
|
|
||||||
- To point out subtle caveats, corner cases and pitfalls, that may cause an
|
|
||||||
advanced user's otherwise working script to fail under future circumstances.
|
|
||||||
|
|
||||||
ShellCheck is written in Haskell, and requires GHC, Parsec3 and Text.Regex.
|
|
||||||
To build the JSON interface and run the unit tests, it also requires QuickCheck2 and JSON.
|
|
||||||
|
|
||||||
On Ubuntu and similar, these are called:
|
|
||||||
ghc6 libghc6-parsec3-dev libghc6-quickcheck2-dev libghc6-json-dev libghc-regex-compat-dev
|
|
||||||
|
|
||||||
Executables can be built with cabal. Tests currently still rely on a Makefile.
|
|
||||||
|
|
||||||
Happy ShellChecking!
|
|
88
README.md
Normal file
88
README.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# ShellCheck - A shell script static analysis tool
|
||||||
|
|
||||||
|
http://www.shellcheck.net
|
||||||
|
|
||||||
|
Copyright 2012-2014, Vidar 'koala_man' Holen
|
||||||
|
Licensed under the GNU Affero General Public License, v3
|
||||||
|
|
||||||
|
The goals of ShellCheck are:
|
||||||
|
|
||||||
|
- To point out and clarify typical beginner's syntax issues,
|
||||||
|
that causes a shell to give cryptic error messages.
|
||||||
|
|
||||||
|
- To point out and clarify typical intermediate level semantic problems,
|
||||||
|
that causes a shell to behave strangely and counter-intuitively.
|
||||||
|
|
||||||
|
- To point out subtle caveats, corner cases and pitfalls, that may cause an
|
||||||
|
advanced user's otherwise working script to fail under future circumstances.
|
||||||
|
|
||||||
|
ShellCheck is written in Haskell, and requires 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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
On Fedora:
|
||||||
|
|
||||||
|
yum install cabal-install
|
||||||
|
|
||||||
|
On Mac OS X with homebrew (http://brew.sh/):
|
||||||
|
|
||||||
|
brew install cabal-install
|
||||||
|
|
||||||
|
On Mac OS X with MacPorts (http://www.macports.org/):
|
||||||
|
|
||||||
|
port install hs-cabal-install
|
||||||
|
|
||||||
|
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 file):
|
||||||
|
|
||||||
|
export PATH=$HOME/.cabal/bin:$PATH
|
||||||
|
|
||||||
|
Verify that your PATH is set up correctly:
|
||||||
|
|
||||||
|
$ which shellcheck
|
||||||
|
~/.cabal/bin/shellcheck
|
||||||
|
|
||||||
|
## Running tests
|
||||||
|
|
||||||
|
To run the unit test suite:
|
||||||
|
|
||||||
|
cabal configure --enable-tests
|
||||||
|
cabal build
|
||||||
|
cabal test
|
||||||
|
|
||||||
|
Happy ShellChecking!
|
@@ -1,20 +1,84 @@
|
|||||||
Name: ShellCheck
|
Name: ShellCheck
|
||||||
Version: 0.1.0
|
Version: 0.3.5
|
||||||
Description: Shell script analysis tool
|
Synopsis: Shell script analysis tool
|
||||||
|
License: AGPL-3
|
||||||
License-file: LICENSE
|
License-file: LICENSE
|
||||||
|
Category: Static Analysis
|
||||||
Author: Vidar Holen
|
Author: Vidar Holen
|
||||||
Maintainer: vidar@vidarholen.net
|
Maintainer: vidar@vidarholen.net
|
||||||
Homepage: http://www.shellcheck.net/
|
Homepage: http://www.shellcheck.net/
|
||||||
Build-Type: Simple
|
Build-Type: Simple
|
||||||
Cabal-Version: >= 1.2
|
Cabal-Version: >= 1.8
|
||||||
|
Bug-reports: https://github.com/koalaman/shellcheck/issues
|
||||||
|
Description:
|
||||||
|
The goals of ShellCheck are:
|
||||||
|
.
|
||||||
|
* To point out and clarify typical beginner's syntax issues,
|
||||||
|
that causes a shell to give cryptic error messages.
|
||||||
|
.
|
||||||
|
* To point out and clarify typical intermediate level semantic problems,
|
||||||
|
that causes a shell to behave strangely and counter-intuitively.
|
||||||
|
.
|
||||||
|
* To point out subtle caveats, corner cases and pitfalls, that may cause an
|
||||||
|
advanced user's otherwise working script to fail under future circumstances.
|
||||||
|
|
||||||
|
Extra-Source-Files:
|
||||||
|
-- documentation
|
||||||
|
README.md
|
||||||
|
shellcheck.1.md
|
||||||
|
-- tests
|
||||||
|
test/shellcheck.hs
|
||||||
|
|
||||||
|
source-repository head
|
||||||
|
type: git
|
||||||
|
location: git://github.com/koalaman/shellcheck.git
|
||||||
|
|
||||||
library
|
library
|
||||||
build-depends: base >= 4, parsec, containers, regex-compat, mtl, directory
|
build-depends:
|
||||||
exposed-modules: ShellCheck.AST, ShellCheck.Parser, ShellCheck.Analytics, ShellCheck.Simple
|
base >= 4 && < 5,
|
||||||
|
containers,
|
||||||
|
directory,
|
||||||
|
json,
|
||||||
|
mtl,
|
||||||
|
parsec,
|
||||||
|
regex-compat,
|
||||||
|
QuickCheck >= 2.7.4
|
||||||
|
exposed-modules:
|
||||||
|
ShellCheck.Analytics
|
||||||
|
ShellCheck.AST
|
||||||
|
ShellCheck.Data
|
||||||
|
ShellCheck.Options
|
||||||
|
ShellCheck.Parser
|
||||||
|
ShellCheck.Simple
|
||||||
|
other-modules:
|
||||||
|
Paths_ShellCheck
|
||||||
|
|
||||||
executable shellcheck
|
executable shellcheck
|
||||||
|
build-depends:
|
||||||
|
ShellCheck,
|
||||||
|
base >= 4 && < 5,
|
||||||
|
containers,
|
||||||
|
directory,
|
||||||
|
json,
|
||||||
|
mtl,
|
||||||
|
parsec,
|
||||||
|
regex-compat,
|
||||||
|
transformers,
|
||||||
|
QuickCheck >= 2.7.4
|
||||||
main-is: shellcheck.hs
|
main-is: shellcheck.hs
|
||||||
|
|
||||||
executable jsoncheck
|
test-suite test-shellcheck
|
||||||
build-depends: json
|
type: exitcode-stdio-1.0
|
||||||
main-is: jsoncheck.hs
|
build-depends:
|
||||||
|
ShellCheck,
|
||||||
|
base >= 4 && < 5,
|
||||||
|
containers,
|
||||||
|
directory,
|
||||||
|
json,
|
||||||
|
mtl,
|
||||||
|
parsec,
|
||||||
|
regex-compat,
|
||||||
|
transformers,
|
||||||
|
QuickCheck >= 2.7.4
|
||||||
|
main-is: test/shellcheck.hs
|
||||||
|
|
||||||
|
@@ -23,15 +23,21 @@ import qualified Text.Regex as Re
|
|||||||
|
|
||||||
data Id = Id Int deriving (Show, Eq, Ord)
|
data Id = Id Int deriving (Show, Eq, Ord)
|
||||||
|
|
||||||
|
data Quoted = Quoted | Unquoted deriving (Show, Eq)
|
||||||
|
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 ForInType = NormalForIn | ShortForIn deriving (Show, Eq)
|
||||||
|
data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
|
||||||
|
|
||||||
data Token =
|
data Token =
|
||||||
TA_Base Id String Token
|
TA_Binary Id String Token Token
|
||||||
| TA_Binary Id String Token Token
|
| TA_Expansion Id [Token]
|
||||||
| TA_Expansion Id Token
|
| TA_Index Id Token
|
||||||
| TA_Literal Id String
|
|
||||||
| TA_Sequence Id [Token]
|
| TA_Sequence Id [Token]
|
||||||
| TA_Trinary Id Token Token Token
|
| TA_Trinary Id Token Token Token
|
||||||
| TA_Unary Id String Token
|
| TA_Unary Id String Token
|
||||||
| TA_Variable Id String
|
|
||||||
| TC_And Id ConditionType String Token Token
|
| TC_And Id ConditionType String Token Token
|
||||||
| TC_Binary Id ConditionType String Token Token
|
| TC_Binary Id ConditionType String Token Token
|
||||||
| TC_Group Id ConditionType Token
|
| TC_Group Id ConditionType Token
|
||||||
@@ -42,7 +48,9 @@ data Token =
|
|||||||
| T_AndIf Id (Token) (Token)
|
| T_AndIf Id (Token) (Token)
|
||||||
| T_Arithmetic Id Token
|
| T_Arithmetic Id Token
|
||||||
| T_Array Id [Token]
|
| T_Array Id [Token]
|
||||||
| T_Assignment Id String Token
|
| T_IndexedElement Id Token Token
|
||||||
|
| T_ Id [Token]
|
||||||
|
| T_Assignment Id AssignmentMode String (Maybe Token) Token
|
||||||
| T_Backgrounded Id Token
|
| T_Backgrounded Id Token
|
||||||
| T_Backticked Id [Token]
|
| T_Backticked Id [Token]
|
||||||
| T_Bang Id
|
| T_Bang Id
|
||||||
@@ -51,7 +59,7 @@ data Token =
|
|||||||
| T_BraceGroup Id [Token]
|
| T_BraceGroup Id [Token]
|
||||||
| T_CLOBBER Id
|
| T_CLOBBER Id
|
||||||
| T_Case Id
|
| T_Case Id
|
||||||
| T_CaseExpression Id Token [([Token],[Token])]
|
| T_CaseExpression Id Token [(CaseType, [Token], [Token])]
|
||||||
| T_Condition Id ConditionType Token
|
| T_Condition Id ConditionType Token
|
||||||
| T_DGREAT Id
|
| T_DGREAT Id
|
||||||
| T_DLESS Id
|
| T_DLESS Id
|
||||||
@@ -75,12 +83,12 @@ data Token =
|
|||||||
| T_Fi Id
|
| T_Fi Id
|
||||||
| T_For Id
|
| T_For Id
|
||||||
| T_ForArithmetic Id Token Token Token [Token]
|
| T_ForArithmetic Id Token Token Token [Token]
|
||||||
| T_ForIn Id String [Token] [Token]
|
| T_ForIn Id ForInType [String] [Token] [Token]
|
||||||
| T_Function Id String Token
|
| T_Function Id FunctionKeyword FunctionParentheses String Token
|
||||||
| T_GREATAND Id
|
| T_GREATAND Id
|
||||||
| T_Glob Id String
|
| T_Glob Id String
|
||||||
| T_Greater Id
|
| T_Greater Id
|
||||||
| T_HereDoc Id Bool Bool String
|
| T_HereDoc Id Dashed Quoted String [Token]
|
||||||
| T_HereString Id Token
|
| T_HereString Id Token
|
||||||
| T_If Id
|
| T_If Id
|
||||||
| T_IfExpression Id [([Token],[Token])] [Token]
|
| T_IfExpression Id [([Token],[Token])] [Token]
|
||||||
@@ -96,7 +104,7 @@ data Token =
|
|||||||
| T_NormalWord Id [Token]
|
| T_NormalWord Id [Token]
|
||||||
| T_OR_IF Id
|
| T_OR_IF Id
|
||||||
| T_OrIf Id (Token) (Token)
|
| T_OrIf Id (Token) (Token)
|
||||||
| T_Pipeline Id [Token]
|
| T_Pipeline Id [Token] [Token] -- [Pipe separators] [Commands]
|
||||||
| T_ProcSub Id String [Token]
|
| T_ProcSub Id String [Token]
|
||||||
| T_Rbrace Id
|
| T_Rbrace Id
|
||||||
| T_Redirecting Id [Token] Token
|
| T_Redirecting Id [Token] Token
|
||||||
@@ -113,19 +121,22 @@ data Token =
|
|||||||
| T_UntilExpression Id [Token] [Token]
|
| T_UntilExpression Id [Token] [Token]
|
||||||
| T_While Id
|
| T_While Id
|
||||||
| T_WhileExpression Id [Token] [Token]
|
| T_WhileExpression Id [Token] [Token]
|
||||||
|
| T_Annotation Id [Annotation] Token
|
||||||
|
| T_Pipe Id String
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
|
data Annotation = DisableComment Integer deriving (Show, Eq)
|
||||||
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
|
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
|
||||||
|
|
||||||
-- I apologize for nothing!
|
-- I apologize for nothing!
|
||||||
lolHax s = Re.subRegex (Re.mkRegex "(Id [0-9]+)") (show s) "(Id 0)"
|
lolHax s = Re.subRegex (Re.mkRegex "(Id [0-9]+)") (show s) "(Id 0)"
|
||||||
instance Eq Token where
|
instance Eq Token where
|
||||||
(==) a b = (lolHax a) == (lolHax b)
|
(==) a b = lolHax a == lolHax b
|
||||||
|
|
||||||
|
|
||||||
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> Token) -> Token -> m Token
|
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> Token) -> Token -> m Token
|
||||||
analyze f g i t =
|
analyze f g i =
|
||||||
round t
|
round
|
||||||
where
|
where
|
||||||
round t = do
|
round t = do
|
||||||
f t
|
f t
|
||||||
@@ -134,6 +145,11 @@ analyze f g i t =
|
|||||||
return . i $ newT
|
return . i $ newT
|
||||||
roundAll = mapM round
|
roundAll = mapM round
|
||||||
|
|
||||||
|
roundMaybe Nothing = return Nothing
|
||||||
|
roundMaybe (Just v) = do
|
||||||
|
s <- round v
|
||||||
|
return (Just s)
|
||||||
|
|
||||||
dl l v = do
|
dl l v = do
|
||||||
x <- roundAll l
|
x <- roundAll l
|
||||||
return $ v x
|
return $ v x
|
||||||
@@ -159,14 +175,18 @@ analyze f g i t =
|
|||||||
delve (T_IoFile id op file) = d2 op file $ T_IoFile id
|
delve (T_IoFile id op file) = d2 op file $ T_IoFile id
|
||||||
delve (T_HereString id word) = d1 word $ T_HereString id
|
delve (T_HereString id word) = d1 word $ T_HereString id
|
||||||
delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v
|
delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v
|
||||||
delve (T_Assignment id v t) = d1 t $ T_Assignment id v
|
delve (T_Assignment id mode var index value) = do
|
||||||
|
a <- roundMaybe index
|
||||||
|
b <- round value
|
||||||
|
return $ T_Assignment id mode var a b
|
||||||
delve (T_Array id t) = dl t $ T_Array id
|
delve (T_Array id t) = dl t $ T_Array id
|
||||||
|
delve (T_IndexedElement id t1 t2) = d2 t1 t2 $ T_IndexedElement id
|
||||||
delve (T_Redirecting id redirs cmd) = do
|
delve (T_Redirecting id redirs cmd) = do
|
||||||
a <- roundAll redirs
|
a <- roundAll redirs
|
||||||
b <- round cmd
|
b <- round cmd
|
||||||
return $ T_Redirecting id a b
|
return $ T_Redirecting id a b
|
||||||
delve (T_SimpleCommand id vars cmds) = dll vars cmds $ T_SimpleCommand id
|
delve (T_SimpleCommand id vars cmds) = dll vars cmds $ T_SimpleCommand id
|
||||||
delve (T_Pipeline id l) = dl l $ T_Pipeline id
|
delve (T_Pipeline id l1 l2) = dll l1 l2 $ T_Pipeline id
|
||||||
delve (T_Banged id l) = d1 l $ T_Banged id
|
delve (T_Banged id l) = d1 l $ T_Banged id
|
||||||
delve (T_AndIf id t u) = d2 t u $ T_AndIf id
|
delve (T_AndIf id t u) = d2 t u $ T_AndIf id
|
||||||
delve (T_OrIf id t u) = d2 t u $ T_OrIf id
|
delve (T_OrIf id t u) = d2 t u $ T_OrIf id
|
||||||
@@ -185,14 +205,14 @@ analyze f g i t =
|
|||||||
delve (T_BraceGroup id l) = dl l $ T_BraceGroup id
|
delve (T_BraceGroup id l) = dl l $ T_BraceGroup id
|
||||||
delve (T_WhileExpression id c l) = dll c l $ T_WhileExpression id
|
delve (T_WhileExpression id c l) = dll c l $ T_WhileExpression id
|
||||||
delve (T_UntilExpression id c l) = dll c l $ T_UntilExpression id
|
delve (T_UntilExpression id c l) = dll c l $ T_UntilExpression id
|
||||||
delve (T_ForIn id v w l) = dll w l $ T_ForIn id v
|
delve (T_ForIn id t v w l) = dll w l $ T_ForIn id t v
|
||||||
delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v
|
delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v
|
||||||
delve (T_CaseExpression id word cases) = do
|
delve (T_CaseExpression id word cases) = do
|
||||||
newWord <- round word
|
newWord <- round word
|
||||||
newCases <- mapM (\(c, t) -> do
|
newCases <- mapM (\(o, c, t) -> do
|
||||||
x <- mapM round c
|
x <- mapM round c
|
||||||
y <- mapM round t
|
y <- mapM round t
|
||||||
return (x,y)
|
return (o, x,y)
|
||||||
) cases
|
) cases
|
||||||
return $ T_CaseExpression id newWord newCases
|
return $ T_CaseExpression id newWord newCases
|
||||||
|
|
||||||
@@ -204,10 +224,11 @@ analyze f g i t =
|
|||||||
return $ T_ForArithmetic id x y z list
|
return $ T_ForArithmetic id x y z list
|
||||||
|
|
||||||
delve (T_Script id s l) = dl l $ T_Script id s
|
delve (T_Script id s l) = dl l $ T_Script id s
|
||||||
delve (T_Function id name body) = d1 body $ T_Function id name
|
delve (T_Function id a b name body) = d1 body $ T_Function id a b name
|
||||||
delve (T_Condition id typ token) = d1 token $ T_Condition id typ
|
delve (T_Condition id typ token) = d1 token $ T_Condition id typ
|
||||||
delve (T_Extglob id str l) = dl l $ T_Extglob id str
|
delve (T_Extglob id str l) = dl l $ T_Extglob id str
|
||||||
delve (T_DollarBraced id op) = d1 op $ T_DollarBraced id
|
delve (T_DollarBraced id op) = d1 op $ T_DollarBraced id
|
||||||
|
delve (T_HereDoc id d q str l) = dl l $ T_HereDoc id d q str
|
||||||
|
|
||||||
delve (TC_And id typ str t1 t2) = d2 t1 t2 $ TC_And id typ str
|
delve (TC_And id typ str t1 t2) = d2 t1 t2 $ TC_And id typ str
|
||||||
delve (TC_Or id typ str t1 t2) = d2 t1 t2 $ TC_Or id typ str
|
delve (TC_Or id typ str t1 t2) = d2 t1 t2 $ TC_Or id typ str
|
||||||
@@ -224,8 +245,9 @@ analyze f g i t =
|
|||||||
b <- round t2
|
b <- round t2
|
||||||
c <- round t3
|
c <- round t3
|
||||||
return $ TA_Trinary id a b c
|
return $ TA_Trinary id a b c
|
||||||
delve (TA_Expansion id t) = d1 t $ TA_Expansion id
|
delve (TA_Expansion id t) = dl t $ TA_Expansion id
|
||||||
delve (TA_Base id b t) = d1 t $ TA_Base id b
|
delve (TA_Index id t) = d1 t $ TA_Index id
|
||||||
|
delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns
|
||||||
delve t = return t
|
delve t = return t
|
||||||
|
|
||||||
getId t = case t of
|
getId t = case t of
|
||||||
@@ -272,14 +294,15 @@ getId t = case t of
|
|||||||
T_DollarArithmetic id _ -> id
|
T_DollarArithmetic id _ -> id
|
||||||
T_BraceExpansion id _ -> id
|
T_BraceExpansion id _ -> id
|
||||||
T_IoFile id _ _ -> id
|
T_IoFile id _ _ -> id
|
||||||
T_HereDoc id _ _ _ -> id
|
T_HereDoc id _ _ _ _ -> id
|
||||||
T_HereString id _ -> id
|
T_HereString id _ -> id
|
||||||
T_FdRedirect id _ _ -> id
|
T_FdRedirect id _ _ -> id
|
||||||
T_Assignment id _ _ -> id
|
T_Assignment id _ _ _ _ -> id
|
||||||
T_Array id _ -> id
|
T_Array id _ -> id
|
||||||
|
T_IndexedElement id _ _ -> id
|
||||||
T_Redirecting id _ _ -> id
|
T_Redirecting id _ _ -> id
|
||||||
T_SimpleCommand id _ _ -> id
|
T_SimpleCommand id _ _ -> id
|
||||||
T_Pipeline id _ -> id
|
T_Pipeline id _ _ -> id
|
||||||
T_Banged id _ -> id
|
T_Banged id _ -> id
|
||||||
T_AndIf id _ _ -> id
|
T_AndIf id _ _ -> id
|
||||||
T_OrIf id _ _ -> id
|
T_OrIf id _ _ -> id
|
||||||
@@ -289,10 +312,10 @@ getId t = case t of
|
|||||||
T_BraceGroup id _ -> id
|
T_BraceGroup id _ -> id
|
||||||
T_WhileExpression id _ _ -> id
|
T_WhileExpression id _ _ -> id
|
||||||
T_UntilExpression id _ _ -> id
|
T_UntilExpression id _ _ -> id
|
||||||
T_ForIn id _ _ _ -> id
|
T_ForIn id _ _ _ _ -> id
|
||||||
T_SelectIn id _ _ _ -> id
|
T_SelectIn id _ _ _ -> id
|
||||||
T_CaseExpression id _ _ -> id
|
T_CaseExpression id _ _ -> id
|
||||||
T_Function id _ _ -> id
|
T_Function id _ _ _ _ -> id
|
||||||
T_Arithmetic id _ -> id
|
T_Arithmetic id _ -> id
|
||||||
T_Script id _ _ -> id
|
T_Script id _ _ -> id
|
||||||
T_Condition id _ _ -> id
|
T_Condition id _ _ -> id
|
||||||
@@ -307,21 +330,28 @@ getId t = case t of
|
|||||||
TA_Binary id _ _ _ -> id
|
TA_Binary id _ _ _ -> id
|
||||||
TA_Unary id _ _ -> id
|
TA_Unary id _ _ -> id
|
||||||
TA_Sequence id _ -> id
|
TA_Sequence id _ -> id
|
||||||
TA_Variable id _ -> id
|
|
||||||
TA_Trinary id _ _ _ -> id
|
TA_Trinary id _ _ _ -> id
|
||||||
TA_Expansion id _ -> id
|
TA_Expansion id _ -> id
|
||||||
TA_Literal id _ -> id
|
TA_Index id _ -> id
|
||||||
TA_Base id _ _ -> id
|
|
||||||
T_ProcSub id _ _ -> id
|
T_ProcSub id _ _ -> id
|
||||||
T_Glob id _ -> id
|
T_Glob id _ -> id
|
||||||
T_ForArithmetic id _ _ _ _ -> id
|
T_ForArithmetic id _ _ _ _ -> id
|
||||||
T_DollarSingleQuoted id _ -> id
|
T_DollarSingleQuoted id _ -> id
|
||||||
T_DollarDoubleQuoted id _ -> id
|
T_DollarDoubleQuoted id _ -> id
|
||||||
T_DollarBracket id _ -> id
|
T_DollarBracket id _ -> id
|
||||||
|
T_Annotation id _ _ -> id
|
||||||
|
T_Pipe id _ -> id
|
||||||
|
|
||||||
blank :: Monad m => Token -> m ()
|
blank :: Monad m => Token -> m ()
|
||||||
blank = const $ return ()
|
blank = const $ return ()
|
||||||
doAnalysis f t = analyze f blank id t
|
doAnalysis f = analyze f blank id
|
||||||
doStackAnalysis startToken endToken t = analyze startToken endToken id t
|
doStackAnalysis startToken endToken = analyze startToken endToken id
|
||||||
doTransform i t = runIdentity $ analyze blank blank i t
|
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
|
||||||
|
File diff suppressed because it is too large
Load Diff
87
ShellCheck/Data.hs
Normal file
87
ShellCheck/Data.hs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
module ShellCheck.Data where
|
||||||
|
|
||||||
|
import Data.Version (showVersion)
|
||||||
|
import Paths_ShellCheck (version)
|
||||||
|
|
||||||
|
shellcheckVersion = showVersion version
|
||||||
|
|
||||||
|
internalVariables = [
|
||||||
|
-- Generic
|
||||||
|
"", "_", "rest", "REST",
|
||||||
|
|
||||||
|
-- Bash
|
||||||
|
"BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
|
||||||
|
"BASH_ARGV", "BASH_CMDS", "BASH_COMMAND", "BASH_EXECUTION_STRING",
|
||||||
|
"BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL",
|
||||||
|
"BASH_VERSINFO", "BASH_VERSION", "COMP_CWORD", "COMP_KEY",
|
||||||
|
"COMP_LINE", "COMP_POINT", "COMP_TYPE", "COMP_WORDBREAKS",
|
||||||
|
"COMP_WORDS", "COPROC", "DIRSTACK", "EUID", "FUNCNAME", "GROUPS",
|
||||||
|
"HISTCMD", "HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE",
|
||||||
|
"OLDPWD", "OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD",
|
||||||
|
"RANDOM", "READLINE_LINE", "READLINE_POINT", "REPLY", "SECONDS",
|
||||||
|
"SHELLOPTS", "SHLVL", "UID", "BASH_ENV", "BASH_XTRACEFD", "CDPATH",
|
||||||
|
"COLUMNS", "COMPREPLY", "EMACS", "ENV", "FCEDIT", "FIGNORE",
|
||||||
|
"FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",
|
||||||
|
"HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",
|
||||||
|
"IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
|
||||||
|
"LC_MESSAGES", "LC_NUMERIC", "LINES", "MAIL", "MAILCHECK", "MAILPATH",
|
||||||
|
"OPTERR", "PATH", "POSIXLY_CORRECT", "PROMPT_COMMAND",
|
||||||
|
"PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT",
|
||||||
|
"TMOUT", "TMPDIR", "auto_resume", "histchars",
|
||||||
|
|
||||||
|
-- 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"
|
||||||
|
]
|
||||||
|
|
||||||
|
variablesWithoutSpaces = [
|
||||||
|
"$", "-", "?", "!",
|
||||||
|
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
|
||||||
|
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
|
||||||
|
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
|
||||||
|
]
|
||||||
|
|
||||||
|
commonCommands = [
|
||||||
|
"admin", "alias", "ar", "asa", "at", "awk", "basename", "batch",
|
||||||
|
"bc", "bg", "break", "c99", "cal", "cat", "cd", "cflow", "chgrp",
|
||||||
|
"chmod", "chown", "cksum", "cmp", "colon", "comm", "command",
|
||||||
|
"compress", "continue", "cp", "crontab", "csplit", "ctags", "cut",
|
||||||
|
"cxref", "date", "dd", "delta", "df", "diff", "dirname", "dot",
|
||||||
|
"du", "echo", "ed", "env", "eval", "ex", "exec", "exit", "expand",
|
||||||
|
"export", "expr", "fc", "fg", "file", "find", "fold", "fort77",
|
||||||
|
"fuser", "gencat", "get", "getconf", "getopts", "grep", "hash",
|
||||||
|
"head", "iconv", "ipcrm", "ipcs", "jobs", "join", "kill", "lex",
|
||||||
|
"link", "ln", "locale", "localedef", "logger", "logname", "lp",
|
||||||
|
"ls", "m4", "mailx", "make", "man", "mesg", "mkdir", "mkfifo",
|
||||||
|
"more", "mv", "newgrp", "nice", "nl", "nm", "nohup", "od", "paste",
|
||||||
|
"patch", "pathchk", "pax", "pr", "printf", "prs", "ps", "pwd",
|
||||||
|
"qalter", "qdel", "qhold", "qmove", "qmsg", "qrerun", "qrls",
|
||||||
|
"qselect", "qsig", "qstat", "qsub", "read", "readonly", "renice",
|
||||||
|
"return", "rm", "rmdel", "rmdir", "sact", "sccs", "sed", "set",
|
||||||
|
"sh", "shift", "sleep", "sort", "split", "strings", "strip", "stty",
|
||||||
|
"tabs", "tail", "talk", "tee", "test", "time", "times", "touch",
|
||||||
|
"tput", "tr", "trap", "tsort", "tty", "type", "ulimit", "umask",
|
||||||
|
"unalias", "uname", "uncompress", "unexpand", "unget", "uniq",
|
||||||
|
"unlink", "unset", "uucp", "uudecode", "uuencode", "uustat", "uux",
|
||||||
|
"val", "vi", "wait", "wc", "what", "who", "write", "xargs", "yacc",
|
||||||
|
"zcat"
|
||||||
|
]
|
||||||
|
|
||||||
|
sampleWords = [
|
||||||
|
"alpha", "bravo", "charlie", "delta", "echo", "foxtrot",
|
||||||
|
"golf", "hotel", "india", "juliett", "kilo", "lima", "mike",
|
||||||
|
"november", "oscar", "papa", "quebec", "romeo", "sierra",
|
||||||
|
"tango", "uniform", "victor", "whiskey", "xray", "yankee",
|
||||||
|
"zulu"
|
||||||
|
]
|
14
ShellCheck/Options.hs
Normal file
14
ShellCheck/Options.hs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module ShellCheck.Options where
|
||||||
|
|
||||||
|
data Shell = Ksh | Zsh | Sh | Bash
|
||||||
|
deriving (Show, Eq)
|
||||||
|
|
||||||
|
data AnalysisOptions = AnalysisOptions {
|
||||||
|
optionShellType :: Maybe Shell,
|
||||||
|
optionExcludes :: [Integer]
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultAnalysisOptions = AnalysisOptions {
|
||||||
|
optionShellType = Nothing,
|
||||||
|
optionExcludes = []
|
||||||
|
}
|
1243
ShellCheck/Parser.hs
1243
ShellCheck/Parser.hs
File diff suppressed because it is too large
Load Diff
@@ -15,29 +15,32 @@
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.Simple (shellCheck, ShellCheckComment, scLine, scColumn, scSeverity, scMessage) where
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
module ShellCheck.Simple (shellCheck, ShellCheckComment, scLine, scColumn, scSeverity, scCode, scMessage, runTests) where
|
||||||
|
|
||||||
import ShellCheck.Parser
|
|
||||||
import ShellCheck.Analytics
|
|
||||||
import Data.Maybe
|
|
||||||
import Text.Parsec.Pos
|
|
||||||
import Data.List
|
import Data.List
|
||||||
|
import Data.Maybe
|
||||||
|
import ShellCheck.Analytics hiding (runTests)
|
||||||
|
import ShellCheck.Options
|
||||||
|
import ShellCheck.Parser hiding (runTests)
|
||||||
|
import Test.QuickCheck.All (quickCheckAll)
|
||||||
|
import Text.Parsec.Pos
|
||||||
|
|
||||||
shellCheck :: String -> [ShellCheckComment]
|
shellCheck :: AnalysisOptions -> String -> [ShellCheckComment]
|
||||||
shellCheck script =
|
shellCheck options script =
|
||||||
let (ParseResult result notes) = parseShell "-" script in
|
let (ParseResult result notes) = parseShell "-" script in
|
||||||
let allNotes = notes ++ (concat $ maybeToList $ do
|
let allNotes = notes ++ concat (maybeToList $ do
|
||||||
(tree, map) <- result
|
(tree, posMap) <- result
|
||||||
let newMap = runAllAnalytics tree map
|
let list = runAnalytics options tree
|
||||||
return $ notesFromMap newMap
|
return $ map (noteToParseNote posMap) $ filterByAnnotation tree list
|
||||||
)
|
)
|
||||||
in
|
in
|
||||||
map formatNote $ nub $ sortNotes allNotes
|
map formatNote $ nub $ sortNotes allNotes
|
||||||
|
|
||||||
data ShellCheckComment = ShellCheckComment { scLine :: Int, scColumn :: Int, scSeverity :: String, scMessage :: String }
|
data ShellCheckComment = ShellCheckComment { scLine :: Int, scColumn :: Int, scSeverity :: String, scCode :: Int, scMessage :: String }
|
||||||
|
|
||||||
instance Show ShellCheckComment where
|
instance Show ShellCheckComment where
|
||||||
show c = concat ["(", show $ scLine c, ",", show $ scColumn c, ") ", scSeverity c, ": ", scMessage c]
|
show c = concat ["(", show $ scLine c, ",", show $ scColumn c, ") ", scSeverity c, ": ", show (scCode c), " ", scMessage c]
|
||||||
|
|
||||||
severityToString s =
|
severityToString s =
|
||||||
case s of
|
case s of
|
||||||
@@ -46,4 +49,29 @@ severityToString s =
|
|||||||
InfoC -> "info"
|
InfoC -> "info"
|
||||||
StyleC -> "style"
|
StyleC -> "style"
|
||||||
|
|
||||||
formatNote (ParseNote pos severity text) = ShellCheckComment (sourceLine pos) (sourceColumn pos) (severityToString severity) text
|
formatNote (ParseNote pos severity code text) =
|
||||||
|
ShellCheckComment (sourceLine pos) (sourceColumn pos) (severityToString severity) (fromIntegral code) text
|
||||||
|
|
||||||
|
testCheck = shellCheck defaultAnalysisOptions { optionExcludes = [2148] } -- Ignore #! warnings
|
||||||
|
prop_findsParseIssue =
|
||||||
|
let comments = testCheck "echo \"$12\"" in
|
||||||
|
length comments == 1 && scCode (head comments) == 1037
|
||||||
|
prop_commentDisablesParseIssue1 =
|
||||||
|
null $ testCheck "#shellcheck disable=SC1037\necho \"$12\""
|
||||||
|
prop_commentDisablesParseIssue2 =
|
||||||
|
null $ testCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\""
|
||||||
|
|
||||||
|
prop_findsAnalysisIssue =
|
||||||
|
let comments = testCheck "echo $1" in
|
||||||
|
length comments == 1 && scCode (head comments) == 2086
|
||||||
|
prop_commentDisablesAnalysisIssue1 =
|
||||||
|
null $ testCheck "#shellcheck disable=SC2086\necho $1"
|
||||||
|
prop_commentDisablesAnalysisIssue2 =
|
||||||
|
null $ testCheck "#shellcheck disable=SC2086\n#lol\necho $1"
|
||||||
|
|
||||||
|
prop_optionDisablesIssue1 =
|
||||||
|
null $ shellCheck (defaultAnalysisOptions { optionExcludes = [2086, 2148] }) "echo $1"
|
||||||
|
|
||||||
|
return []
|
||||||
|
runTests = $quickCheckAll
|
||||||
|
|
||||||
|
32
jsoncheck.hs
32
jsoncheck.hs
@@ -1,32 +0,0 @@
|
|||||||
{-
|
|
||||||
This file is part of ShellCheck.
|
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
ShellCheck is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
-}
|
|
||||||
import ShellCheck.Simple
|
|
||||||
import Text.JSON
|
|
||||||
|
|
||||||
instance JSON ShellCheckComment where
|
|
||||||
showJSON c = makeObj [
|
|
||||||
("line", showJSON $ scLine c),
|
|
||||||
("column", showJSON $ scColumn c),
|
|
||||||
("level", showJSON $ scSeverity c),
|
|
||||||
("message", showJSON $ scMessage c)
|
|
||||||
]
|
|
||||||
readJSON = undefined
|
|
||||||
|
|
||||||
main = do
|
|
||||||
script <- getContents
|
|
||||||
putStrLn $ encodeStrict $ shellCheck script
|
|
125
shellcheck.1.md
Normal file
125
shellcheck.1.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
% SHELLCHECK(1) Shell script analysis tool
|
||||||
|
|
||||||
|
# NAME
|
||||||
|
|
||||||
|
shellcheck - Shell script analysis tool
|
||||||
|
|
||||||
|
# SYNOPSIS
|
||||||
|
|
||||||
|
**shellcheck** [*OPTIONS*...] *FILES*...
|
||||||
|
|
||||||
|
# DESCRIPTION
|
||||||
|
|
||||||
|
ShellCheck is a static analysis and linting tool for sh/bash scripts. It's
|
||||||
|
mainly focused on handling typical beginner and intermediate level syntax
|
||||||
|
errors and pitfalls where the shell just gives a cryptic error message or
|
||||||
|
strange behavior, but it also reports on a few more advanced issues where
|
||||||
|
corner cases can cause delayed failures.
|
||||||
|
|
||||||
|
# OPTIONS
|
||||||
|
|
||||||
|
**-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...]
|
||||||
|
|
||||||
|
: Explicitly exclude the specified codes from the report. Subsequent **-e**
|
||||||
|
options are cumulative, but all the codes can be specified at once,
|
||||||
|
comma-separated as a single argument.
|
||||||
|
|
||||||
|
**-f** *FORMAT*, **--format=***FORMAT*
|
||||||
|
|
||||||
|
: Specify the output format of shellcheck, which prints its results in the
|
||||||
|
standard output. Subsequent **-f** options are ignored, see **FORMATS**
|
||||||
|
below for more information.
|
||||||
|
|
||||||
|
**-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.
|
||||||
|
|
||||||
|
**-V**\ *version*,\ **--version**
|
||||||
|
|
||||||
|
: Print version and exit.
|
||||||
|
|
||||||
|
# FORMATS
|
||||||
|
|
||||||
|
**tty**
|
||||||
|
|
||||||
|
: Plain text, human readable output. This is the default.
|
||||||
|
|
||||||
|
**gcc**
|
||||||
|
|
||||||
|
: GCC compatible output. Useful for editors that support compiling and
|
||||||
|
showing syntax errors.
|
||||||
|
|
||||||
|
For example, in Vim, `:set makeprg=shellcheck\ -f\ gcc\ %` will allow
|
||||||
|
using `:make` to check the script, and `:cnext` to jump to the next error.
|
||||||
|
|
||||||
|
<file>:<line>:<column>: <type>: <message>
|
||||||
|
|
||||||
|
**checkstyle**
|
||||||
|
|
||||||
|
: Checkstyle compatible XML output. Supported directly or through plugins
|
||||||
|
by many IDEs and build monitoring systems.
|
||||||
|
|
||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<checkstyle version='4.3'>
|
||||||
|
<file name='file'>
|
||||||
|
<error
|
||||||
|
line='line'
|
||||||
|
column='column'
|
||||||
|
severity='severity'
|
||||||
|
message='message'
|
||||||
|
source='ShellCheck.SC####' />
|
||||||
|
...
|
||||||
|
</file>
|
||||||
|
...
|
||||||
|
</checkstyle>
|
||||||
|
|
||||||
|
**json**
|
||||||
|
|
||||||
|
: Json is a popular serialization format that is more suitable for web
|
||||||
|
applications. ShellCheck's json is compact and contains only the bare
|
||||||
|
minimum.
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"line": line,
|
||||||
|
"column": column,
|
||||||
|
"level": level,
|
||||||
|
"code": ####,
|
||||||
|
"message": message
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
# DIRECTIVES
|
||||||
|
ShellCheck directives can be specified as comments in the shell script
|
||||||
|
before a command or block:
|
||||||
|
|
||||||
|
# shellcheck key=value key=value
|
||||||
|
command-or-structure
|
||||||
|
|
||||||
|
For example, to suppress SC2035 about using `./*.jpg`:
|
||||||
|
|
||||||
|
# shellcheck disable=SC2035
|
||||||
|
echo "Files: " *.jpg
|
||||||
|
|
||||||
|
Valid keys are:
|
||||||
|
|
||||||
|
**disable**
|
||||||
|
: Disables a comma separated list of error codes for the following command.
|
||||||
|
The command can be a simple command like `echo foo`, or a compound command
|
||||||
|
like a function definition, subshell block or loop.
|
||||||
|
|
||||||
|
|
||||||
|
# AUTHOR
|
||||||
|
ShellCheck is written and maintained by Vidar Holen.
|
||||||
|
|
||||||
|
# REPORTING BUGS
|
||||||
|
Bugs and issues can be reported on GitHub:
|
||||||
|
|
||||||
|
https://github.com/koalaman/shellcheck/issues
|
||||||
|
|
||||||
|
# SEE ALSO
|
||||||
|
|
||||||
|
sh(1) bash(1)
|
342
shellcheck.hs
342
shellcheck.hs
@@ -15,72 +15,334 @@
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
|
import Control.Exception
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
|
import Control.Monad.Trans
|
||||||
|
import Control.Monad.Trans.Error
|
||||||
|
import Data.Char
|
||||||
|
import Data.Maybe
|
||||||
|
import Data.Monoid
|
||||||
import GHC.Exts
|
import GHC.Exts
|
||||||
import GHC.IO.Device
|
import GHC.IO.Device
|
||||||
|
import Prelude hiding (catch)
|
||||||
|
import ShellCheck.Data
|
||||||
|
import ShellCheck.Options
|
||||||
import ShellCheck.Simple
|
import ShellCheck.Simple
|
||||||
|
import ShellCheck.Analytics
|
||||||
|
import System.Console.GetOpt
|
||||||
import System.Directory
|
import System.Directory
|
||||||
import System.Environment
|
import System.Environment
|
||||||
import System.Exit
|
import System.Exit
|
||||||
import System.IO
|
import System.IO
|
||||||
|
import Text.JSON
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
|
data Flag = Flag String String
|
||||||
|
data Status = NoProblems | SomeProblems | BadInput | SupportFailure | SyntaxFailure | RuntimeException deriving (Ord, Eq)
|
||||||
|
|
||||||
|
instance Error Status where
|
||||||
|
noMsg = RuntimeException
|
||||||
|
|
||||||
|
instance Monoid Status where
|
||||||
|
mempty = NoProblems
|
||||||
|
mappend = max
|
||||||
|
|
||||||
|
header = "Usage: shellcheck [OPTIONS...] FILES..."
|
||||||
|
options = [
|
||||||
|
Option "e" ["exclude"]
|
||||||
|
(ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings",
|
||||||
|
Option "f" ["format"]
|
||||||
|
(ReqArg (Flag "format") "FORMAT") "output format",
|
||||||
|
Option "s" ["shell"]
|
||||||
|
(ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (bash,sh,ksh,zsh)",
|
||||||
|
Option "V" ["version"]
|
||||||
|
(NoArg $ Flag "version" "true") "Print version information"
|
||||||
|
]
|
||||||
|
|
||||||
|
printErr = hPutStrLn stderr
|
||||||
|
|
||||||
|
|
||||||
clear = ansi 0
|
instance JSON ShellCheckComment where
|
||||||
ansi n = "\x1B[" ++ (show n) ++ "m"
|
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
|
||||||
|
|
||||||
colorForLevel "error" = 31 -- red
|
parseArguments :: [String] -> ErrorT Status IO ([Flag], [FilePath])
|
||||||
colorForLevel "warning" = 33 -- yellow
|
parseArguments argv =
|
||||||
colorForLevel "info" = 32 -- green
|
case getOpt Permute options argv of
|
||||||
colorForLevel "style" = 32 -- green
|
(opts, files, []) -> return (opts, files)
|
||||||
colorForLevel "message" = 1 -- bold
|
(_, _, errors) -> do
|
||||||
colorForLevel "source" = 0 -- none
|
liftIO . printErr $ concat errors ++ "\n" ++ usageInfo header options
|
||||||
colorForLevel _ = 0 -- none
|
throwError SyntaxFailure
|
||||||
|
|
||||||
colorComment level comment = (ansi $ colorForLevel level) ++ comment ++ clear
|
formats :: Map.Map String (AnalysisOptions -> [FilePath] -> IO Status)
|
||||||
|
formats = Map.fromList [
|
||||||
|
("json", forJson),
|
||||||
|
("gcc", forGcc),
|
||||||
|
("checkstyle", forCheckstyle),
|
||||||
|
("tty", forTty)
|
||||||
|
]
|
||||||
|
|
||||||
doFile path colorFunc = do
|
toStatus = liftM (either id (const NoProblems)) . runErrorT
|
||||||
let actualPath = if path == "-" then "/dev/stdin" else path
|
|
||||||
exists <- doesFileExist actualPath
|
|
||||||
if exists then do
|
|
||||||
contents <- readFile actualPath
|
|
||||||
doInput path contents colorFunc
|
|
||||||
else do
|
|
||||||
putStrLn (colorFunc "error" $ "No such file: " ++ actualPath)
|
|
||||||
|
|
||||||
doInput filename contents colorFunc = do
|
catchExceptions :: IO Status -> IO Status
|
||||||
let fileLines = lines contents
|
catchExceptions action = action -- action `catch` handler
|
||||||
let lineCount = length fileLines
|
where
|
||||||
let comments = shellCheck contents
|
handler err = do
|
||||||
let groups = groupWith scLine comments
|
printErr $ show (err :: SomeException)
|
||||||
if not $ null comments then do
|
return RuntimeException
|
||||||
|
|
||||||
|
checkComments comments = if null comments then NoProblems else SomeProblems
|
||||||
|
|
||||||
|
forTty :: AnalysisOptions -> [FilePath] -> IO Status
|
||||||
|
forTty options files = do
|
||||||
|
output <- mapM doFile files
|
||||||
|
return $ mconcat 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 = catchExceptions $ 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
|
mapM_ (\x -> do
|
||||||
let lineNum = scLine (head x)
|
let lineNum = scLine (head x)
|
||||||
let line = if lineNum < 1 || lineNum > lineCount
|
let line = if lineNum < 1 || lineNum > lineCount
|
||||||
then ""
|
then ""
|
||||||
else fileLines !! (lineNum - 1)
|
else fileLines !! (lineNum - 1)
|
||||||
putStrLn ""
|
putStrLn ""
|
||||||
putStrLn $ colorFunc "message" ("In " ++ filename ++" line " ++ (show $ lineNum) ++ ":")
|
putStrLn $ colorFunc "message"
|
||||||
|
("In " ++ filename ++" line " ++ show lineNum ++ ":")
|
||||||
putStrLn (colorFunc "source" line)
|
putStrLn (colorFunc "source" line)
|
||||||
mapM (\c -> putStrLn (colorFunc (scSeverity c) $ cuteIndent c)) x
|
mapM_ (\c -> putStrLn (colorFunc (scSeverity c) $ cuteIndent c)) x
|
||||||
putStrLn ""
|
putStrLn ""
|
||||||
) groups
|
) groups
|
||||||
else do
|
return . checkComments $ comments
|
||||||
putStrLn ("No comments for " ++ filename)
|
|
||||||
|
|
||||||
cuteIndent comment =
|
cuteIndent comment =
|
||||||
(replicate ((scColumn comment) - 1) ' ') ++ "^-- " ++ (scMessage comment)
|
replicate (scColumn comment - 1) ' ' ++
|
||||||
|
"^-- " ++ code (scCode comment) ++ ": " ++ scMessage comment
|
||||||
|
|
||||||
getColorFunc = do
|
code code = "SC" ++ show code
|
||||||
term <- hIsTerminalDevice stdout
|
|
||||||
return $ if term then colorComment else const id
|
getColorFunc = do
|
||||||
|
term <- hIsTerminalDevice stdout
|
||||||
|
return $ if term then colorComment else const id
|
||||||
|
|
||||||
|
-- This totally ignores the filenames. Fixme?
|
||||||
|
forJson :: AnalysisOptions -> [FilePath] -> IO Status
|
||||||
|
forJson options files = catchExceptions $ do
|
||||||
|
comments <- liftM concat $ mapM (commentsFor options) files
|
||||||
|
putStrLn $ encodeStrict comments
|
||||||
|
return $ checkComments comments
|
||||||
|
|
||||||
|
-- Mimic GCC "file:line:col: (error|warning|note): message" format
|
||||||
|
forGcc :: AnalysisOptions -> [FilePath] -> IO Status
|
||||||
|
forGcc options files = do
|
||||||
|
files <- mapM process files
|
||||||
|
return $ mconcat files
|
||||||
|
where
|
||||||
|
process file = catchExceptions $ do
|
||||||
|
contents <- readContents file
|
||||||
|
let comments = makeNonVirtual (getComments options contents) contents
|
||||||
|
mapM_ (putStrLn . format file) comments
|
||||||
|
return $ checkComments 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 :: AnalysisOptions -> [FilePath] -> IO Status
|
||||||
|
forCheckstyle options files = do
|
||||||
|
putStrLn "<?xml version='1.0' encoding='UTF-8'?>"
|
||||||
|
putStrLn "<checkstyle version='4.3'>"
|
||||||
|
statuses <- mapM process files
|
||||||
|
putStrLn "</checkstyle>"
|
||||||
|
return $ mconcat statuses
|
||||||
|
where
|
||||||
|
process file = catchExceptions $ do
|
||||||
|
comments <- commentsFor options file
|
||||||
|
putStrLn (formatFile file comments)
|
||||||
|
return $ checkComments comments
|
||||||
|
|
||||||
|
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 = shellCheck
|
||||||
|
|
||||||
|
readContents :: FilePath -> IO String
|
||||||
|
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 =
|
||||||
|
if scLine c > 0 && scLine c <= length ls
|
||||||
|
then real (ls !! (scLine c - 1)) 0 0 (scColumn c)
|
||||||
|
else scColumn c
|
||||||
|
}
|
||||||
|
real _ r v target | target <= v = r
|
||||||
|
real [] r v _ = 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
|
||||||
|
|
||||||
|
getOptions options name =
|
||||||
|
map (\(Flag _ val) -> val) . filter (\(Flag var _) -> var == name) $ options
|
||||||
|
|
||||||
|
split char str =
|
||||||
|
split' str []
|
||||||
|
where
|
||||||
|
split' (a:rest) element =
|
||||||
|
if a == char
|
||||||
|
then reverse element : split' rest []
|
||||||
|
else split' rest (a:element)
|
||||||
|
split' [] element = [reverse element]
|
||||||
|
|
||||||
|
getExclusions options =
|
||||||
|
let elements = concatMap (split ',') $ getOptions options "exclude"
|
||||||
|
clean = dropWhile (not . isDigit)
|
||||||
|
in
|
||||||
|
map (Prelude.read . clean) elements :: [Int]
|
||||||
|
|
||||||
|
excludeCodes codes =
|
||||||
|
filter (not . hasCode)
|
||||||
|
where
|
||||||
|
hasCode c = scCode c `elem` codes
|
||||||
|
|
||||||
main = do
|
main = do
|
||||||
args <- getArgs
|
args <- getArgs
|
||||||
colors <- getColorFunc
|
status <- toStatus $ do
|
||||||
if null args then do
|
(flags, files) <- parseArguments args
|
||||||
hPutStrLn stderr "shellcheck -- bash/sh script static analysis tool"
|
process flags files
|
||||||
hPutStrLn stderr "Usage: shellcheck filenames..."
|
exitWith $ statusToCode status
|
||||||
exitFailure
|
|
||||||
else
|
|
||||||
mapM (\f -> doFile f colors) args
|
|
||||||
|
|
||||||
|
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] -> ErrorT Status IO ()
|
||||||
|
process flags files = do
|
||||||
|
options <- foldM (flip parseOption) defaultAnalysisOptions flags
|
||||||
|
verifyFiles files
|
||||||
|
let format = fromMaybe "tty" $ getOption flags "format"
|
||||||
|
case Map.lookup format formats of
|
||||||
|
Nothing -> do
|
||||||
|
liftIO $ do
|
||||||
|
printErr $ "Unknown format " ++ format
|
||||||
|
printErr "Supported formats:"
|
||||||
|
mapM_ (printErr . write) $ Map.keys formats
|
||||||
|
throwError SupportFailure
|
||||||
|
where write s = " " ++ s
|
||||||
|
Just f -> ErrorT $ liftM Left $ f options files
|
||||||
|
|
||||||
|
parseOption flag options =
|
||||||
|
case flag of
|
||||||
|
Flag "shell" str ->
|
||||||
|
fromMaybe (die $ "Unknown shell: " ++ str) $ do
|
||||||
|
shell <- shellForExecutable str
|
||||||
|
return $ return options { optionShellType = Just shell }
|
||||||
|
|
||||||
|
Flag "exclude" str -> do
|
||||||
|
new <- mapM parseNum $ split ',' str
|
||||||
|
let old = optionExcludes options
|
||||||
|
return options { optionExcludes = new ++ old }
|
||||||
|
|
||||||
|
Flag "version" _ -> do
|
||||||
|
liftIO printVersion
|
||||||
|
throwError NoProblems
|
||||||
|
|
||||||
|
_ -> return options
|
||||||
|
where
|
||||||
|
die s = do
|
||||||
|
liftIO $ printErr s
|
||||||
|
throwError SupportFailure
|
||||||
|
parseNum ('S':'C':str) = parseNum str
|
||||||
|
parseNum num = do
|
||||||
|
unless (all isDigit num) $ do
|
||||||
|
liftIO . printErr $ "Bad exclusion: " ++ num
|
||||||
|
throwError SyntaxFailure
|
||||||
|
return (Prelude.read num :: Integer)
|
||||||
|
|
||||||
|
verifyFiles files =
|
||||||
|
when (null files) $ do
|
||||||
|
liftIO $ printErr "No files specified.\n"
|
||||||
|
liftIO $ printErr $ usageInfo header options
|
||||||
|
throwError SyntaxFailure
|
||||||
|
|
||||||
|
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"
|
||||||
|
@@ -1,65 +0,0 @@
|
|||||||
#!/usr/bin/env runhaskell
|
|
||||||
-- #!/usr/bin/env runhugs
|
|
||||||
-- $Id: quickcheck,v 1.4 2003/01/08 15:09:22 shae Exp $
|
|
||||||
-- This file defines a command
|
|
||||||
-- quickCheck <options> <files>
|
|
||||||
-- which invokes quickCheck on all properties defined in the files given as
|
|
||||||
-- arguments, by generating an input script for hugs and then invoking it.
|
|
||||||
-- quickCheck recognises the options
|
|
||||||
-- +names print the name of each property before checking it
|
|
||||||
-- -names do not print property names (the default)
|
|
||||||
-- +verbose displays each test case before running
|
|
||||||
-- -verbose do not displays each test case before running (the default)
|
|
||||||
-- Other options (beginning with + or -) are passed unchanged to hugs.
|
|
||||||
--
|
|
||||||
-- Change the first line of this file to the location of runhaskell or runhugs
|
|
||||||
-- on your system.
|
|
||||||
-- Make the file executable.
|
|
||||||
--
|
|
||||||
-- TODO:
|
|
||||||
-- someone on #haskell asked about supporting QC tests inside LaTeX, ex. \{begin} \{end}, how?
|
|
||||||
|
|
||||||
import System.Cmd
|
|
||||||
import System.Directory (findExecutable)
|
|
||||||
import System.Environment
|
|
||||||
import Data.List
|
|
||||||
import Data.Maybe (fromJust)
|
|
||||||
|
|
||||||
main :: IO ()
|
|
||||||
main = do as<-getArgs
|
|
||||||
sequence_ (map (process (filter isOption as))
|
|
||||||
(filter (not.isOption) as))
|
|
||||||
|
|
||||||
-- ugly hack for .lhs files, is there a better way?
|
|
||||||
unlit [] = []
|
|
||||||
unlit x = if (head x) == '>' then (tail x) else x
|
|
||||||
|
|
||||||
process opts file =
|
|
||||||
let (namesOpt,opts') = getOption "names" "-names" opts
|
|
||||||
(verboseOpt,opts'') = getOption "verbose" "-verbose" opts' in
|
|
||||||
do xs<-readFile file
|
|
||||||
let names = nub$ filter (\x -> (("> prop_" `isPrefixOf` x) || ("prop_" `isPrefixOf` x)))
|
|
||||||
(map (fst.head.lex.unlit) (lines xs))
|
|
||||||
if null names then
|
|
||||||
putStr (file++": no properties to check\n")
|
|
||||||
else do writeFile "hugsin"$
|
|
||||||
unlines ((":load "++file):
|
|
||||||
":m +Test.QuickCheck":
|
|
||||||
"let quackCheck p = quickCheckWith (stdArgs { maxSuccess = 1 }) p ":
|
|
||||||
[(if namesOpt=="+names" then
|
|
||||||
"putStr \""++p++": \" >> "
|
|
||||||
else "") ++
|
|
||||||
("quackCheck ")
|
|
||||||
++ p | p<-names])
|
|
||||||
-- To use ghci
|
|
||||||
ghci <- findExecutable "ghci"
|
|
||||||
system (fromJust ghci ++options opts''++" <hugsin")
|
|
||||||
return ()
|
|
||||||
|
|
||||||
isOption xs = head xs `elem` "-+"
|
|
||||||
|
|
||||||
options opts = unwords ["\""++opt++"\"" | opt<-opts]
|
|
||||||
|
|
||||||
getOption name def opts =
|
|
||||||
let opt = head [opt | opt<-opts++[def], isPrefixOf name (drop 1 opt)] in
|
|
||||||
(opt, filter (/=opt) opts)
|
|
@@ -1,22 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Todo: Find a way to make this not suck.
|
|
||||||
|
|
||||||
ulimit -t 60 # Sometimes GHC ends in a spin loop, and this is easier than debugging
|
|
||||||
|
|
||||||
[[ -e test/quackCheck.hs ]] || { echo "Are you running me from the wrong directory?"; exit 1; }
|
|
||||||
[[ $1 == -v ]] && pattern="" || pattern="FAIL"
|
|
||||||
|
|
||||||
find . -name '*.hs' -exec bash -c '
|
|
||||||
grep -v "^module " "$1" > quack.tmp.hs
|
|
||||||
./test/quackCheck.hs +names quack.tmp.hs
|
|
||||||
' -- {} \; 2>&1 | grep -i "$pattern"
|
|
||||||
result=$?
|
|
||||||
rm -f quack.tmp.hs hugsin
|
|
||||||
|
|
||||||
if [[ $result == 0 ]]
|
|
||||||
then
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
16
test/shellcheck.hs
Normal file
16
test/shellcheck.hs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
module Main where
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import System.Exit
|
||||||
|
import qualified ShellCheck.Simple
|
||||||
|
import qualified ShellCheck.Analytics
|
||||||
|
import qualified ShellCheck.Parser
|
||||||
|
|
||||||
|
main = do
|
||||||
|
putStrLn "Running ShellCheck tests..."
|
||||||
|
results <- sequence [ShellCheck.Simple.runTests,
|
||||||
|
ShellCheck.Analytics.runTests,
|
||||||
|
ShellCheck.Parser.runTests]
|
||||||
|
if and results then exitSuccess
|
||||||
|
else exitFailure
|
||||||
|
|
Reference in New Issue
Block a user