mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 00:39:19 +08:00
Compare commits
110 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 |
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
*.hi
|
||||
*.o
|
||||
.tests
|
||||
jsoncheck
|
||||
shellcheck
|
||||
shellcheck.1
|
||||
dist
|
14
Makefile
14
Makefile
@@ -2,23 +2,23 @@
|
||||
|
||||
GHCFLAGS=-O9
|
||||
|
||||
all: shellcheck jsoncheck .tests
|
||||
all: shellcheck .tests shellcheck.1
|
||||
: 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
|
||||
|
||||
shellcheck.1: shellcheck.1.md
|
||||
pandoc -s -t man $< -o $@
|
||||
|
||||
clean:
|
||||
rm -f .tests shellcheck *.hi *.o ShellCheck/*.hi ShellCheck/*.o
|
||||
rm -f .tests shellcheck shellcheck.1
|
||||
rm -f *.hi *.o ShellCheck/*.hi ShellCheck/*.o
|
||||
rm -rf dist
|
||||
|
||||
regardless:
|
||||
|
||||
|
22
README
22
README
@@ -18,9 +18,27 @@ The goals of ShellCheck are:
|
||||
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
|
||||
On Fedora, these can be installed with:
|
||||
yum install cabal-install ghc ghc-parsec-devel ghc-QuickCheck-devel ghc-json-devel ghc-regex-compat-devel
|
||||
|
||||
On Ubuntu and similar, use:
|
||||
apt-get install ghc libghc-parsec3-dev libghc-json-dev libghc-regex-compat-dev libghc-quickcheck2-dev cabal-install
|
||||
|
||||
For older releases, you may have to use:
|
||||
apt-get install ghc6 libghc6-parsec3-dev libghc6-quickcheck2-dev libghc6-json-dev libghc-regex-compat-dev cabal-install
|
||||
|
||||
On Mac OS X with homebrew (http://brew.sh/), use:
|
||||
brew install cabal-install
|
||||
|
||||
On Mac OS X with MacPorts (http://www.macports.org/), use:
|
||||
port install hs-cabal-install
|
||||
|
||||
Executables can be built with cabal. Tests currently still rely on a Makefile.
|
||||
|
||||
Install:
|
||||
cabal install
|
||||
|
||||
which shellcheck
|
||||
~/.cabal/bin/shellcheck
|
||||
|
||||
Happy ShellChecking!
|
||||
|
@@ -1,20 +1,35 @@
|
||||
Name: ShellCheck
|
||||
Version: 0.1.0
|
||||
Description: Shell script analysis tool
|
||||
-- Must also be updated in ShellCheck/Data.hs :
|
||||
Version: 0.3.1
|
||||
Synopsis: Shell script analysis tool
|
||||
License: OtherLicense
|
||||
License-file: LICENSE
|
||||
Category: Static Analysis
|
||||
Author: Vidar Holen
|
||||
Maintainer: vidar@vidarholen.net
|
||||
Homepage: http://www.shellcheck.net/
|
||||
Build-Type: Simple
|
||||
Cabal-Version: >= 1.2
|
||||
Cabal-Version: >= 1.6
|
||||
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.
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: git://github.com/koalaman/shellcheck.git
|
||||
|
||||
library
|
||||
build-depends: base >= 4, parsec, containers, regex-compat, mtl, directory
|
||||
exposed-modules: ShellCheck.AST, ShellCheck.Parser, ShellCheck.Analytics, ShellCheck.Simple
|
||||
build-depends: base >= 4, base < 5, parsec, containers, regex-compat, mtl, directory, json
|
||||
exposed-modules: ShellCheck.AST, ShellCheck.Data, ShellCheck.Parser, ShellCheck.Analytics, ShellCheck.Simple
|
||||
|
||||
executable shellcheck
|
||||
main-is: shellcheck.hs
|
||||
|
||||
executable jsoncheck
|
||||
build-depends: json
|
||||
main-is: jsoncheck.hs
|
||||
|
@@ -23,6 +23,12 @@ import qualified Text.Regex as Re
|
||||
|
||||
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 Token =
|
||||
TA_Base Id String Token
|
||||
| TA_Binary Id String Token Token
|
||||
@@ -42,7 +48,7 @@ data Token =
|
||||
| T_AndIf Id (Token) (Token)
|
||||
| T_Arithmetic Id Token
|
||||
| T_Array Id [Token]
|
||||
| T_Assignment Id String Token
|
||||
| T_Assignment Id AssignmentMode String (Maybe Token) Token
|
||||
| T_Backgrounded Id Token
|
||||
| T_Backticked Id [Token]
|
||||
| T_Bang Id
|
||||
@@ -76,11 +82,11 @@ data Token =
|
||||
| T_For Id
|
||||
| T_ForArithmetic Id Token Token Token [Token]
|
||||
| T_ForIn Id String [Token] [Token]
|
||||
| T_Function Id String Token
|
||||
| T_Function Id FunctionKeyword FunctionParentheses String Token
|
||||
| T_GREATAND Id
|
||||
| T_Glob Id String
|
||||
| T_Greater Id
|
||||
| T_HereDoc Id Bool Bool String
|
||||
| T_HereDoc Id Dashed Quoted String [Token]
|
||||
| T_HereString Id Token
|
||||
| T_If Id
|
||||
| T_IfExpression Id [([Token],[Token])] [Token]
|
||||
@@ -113,8 +119,10 @@ data Token =
|
||||
| T_UntilExpression Id [Token] [Token]
|
||||
| T_While Id
|
||||
| T_WhileExpression Id [Token] [Token]
|
||||
| T_Annotation Id [Annotation] Token
|
||||
deriving (Show)
|
||||
|
||||
data Annotation = DisableComment Integer deriving (Show, Eq)
|
||||
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
|
||||
|
||||
-- I apologize for nothing!
|
||||
@@ -134,6 +142,11 @@ analyze f g i t =
|
||||
return . i $ newT
|
||||
roundAll = mapM round
|
||||
|
||||
roundMaybe Nothing = return Nothing
|
||||
roundMaybe (Just v) = do
|
||||
s <- round v
|
||||
return (Just s)
|
||||
|
||||
dl l v = do
|
||||
x <- roundAll l
|
||||
return $ v x
|
||||
@@ -159,7 +172,10 @@ analyze f g i t =
|
||||
delve (T_IoFile id op file) = d2 op file $ T_IoFile id
|
||||
delve (T_HereString id word) = d1 word $ T_HereString id
|
||||
delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v
|
||||
delve (T_Assignment id v t) = d1 t $ T_Assignment id v
|
||||
delve (T_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_Redirecting id redirs cmd) = do
|
||||
a <- roundAll redirs
|
||||
@@ -204,10 +220,11 @@ analyze f g i t =
|
||||
return $ T_ForArithmetic id x y z list
|
||||
|
||||
delve (T_Script id s l) = dl l $ T_Script id s
|
||||
delve (T_Function id name body) = d1 body $ T_Function id name
|
||||
delve (T_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_Extglob id str l) = dl l $ T_Extglob id str
|
||||
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_Or id typ str t1 t2) = d2 t1 t2 $ TC_Or id typ str
|
||||
@@ -226,6 +243,7 @@ analyze f g i t =
|
||||
return $ TA_Trinary id a b c
|
||||
delve (TA_Expansion id t) = d1 t $ TA_Expansion id
|
||||
delve (TA_Base id b t) = d1 t $ TA_Base id b
|
||||
delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns
|
||||
delve t = return t
|
||||
|
||||
getId t = case t of
|
||||
@@ -272,10 +290,10 @@ getId t = case t of
|
||||
T_DollarArithmetic id _ -> id
|
||||
T_BraceExpansion id _ -> id
|
||||
T_IoFile id _ _ -> id
|
||||
T_HereDoc id _ _ _ -> id
|
||||
T_HereDoc id _ _ _ _ -> id
|
||||
T_HereString id _ -> id
|
||||
T_FdRedirect id _ _ -> id
|
||||
T_Assignment id _ _ -> id
|
||||
T_Assignment id _ _ _ _ -> id
|
||||
T_Array id _ -> id
|
||||
T_Redirecting id _ _ -> id
|
||||
T_SimpleCommand id _ _ -> id
|
||||
@@ -292,7 +310,7 @@ getId t = case t of
|
||||
T_ForIn id _ _ _ -> id
|
||||
T_SelectIn id _ _ _ -> id
|
||||
T_CaseExpression id _ _ -> id
|
||||
T_Function id _ _ -> id
|
||||
T_Function id _ _ _ _ -> id
|
||||
T_Arithmetic id _ -> id
|
||||
T_Script id _ _ -> id
|
||||
T_Condition id _ _ -> id
|
||||
@@ -318,6 +336,7 @@ getId t = case t of
|
||||
T_DollarSingleQuoted id _ -> id
|
||||
T_DollarDoubleQuoted id _ -> id
|
||||
T_DollarBracket id _ -> id
|
||||
T_Annotation id _ _ -> id
|
||||
|
||||
blank :: Monad m => Token -> m ()
|
||||
blank = const $ return ()
|
||||
@@ -325,3 +344,10 @@ doAnalysis f t = analyze f blank id t
|
||||
doStackAnalysis startToken endToken t = analyze startToken endToken id t
|
||||
doTransform i t = runIdentity $ analyze blank blank i t
|
||||
|
||||
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
76
ShellCheck/Data.hs
Normal file
76
ShellCheck/Data.hs
Normal file
@@ -0,0 +1,76 @@
|
||||
module ShellCheck.Data where
|
||||
|
||||
shellcheckVersion = "0.3.1" -- Must also be updated in ShellCheck.cabal
|
||||
|
||||
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"
|
||||
]
|
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Simple (shellCheck, ShellCheckComment, scLine, scColumn, scSeverity, scMessage) where
|
||||
module ShellCheck.Simple (shellCheck, ShellCheckComment, scLine, scColumn, scSeverity, scCode, scMessage) where
|
||||
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Analytics
|
||||
@@ -23,21 +23,38 @@ import Data.Maybe
|
||||
import Text.Parsec.Pos
|
||||
import Data.List
|
||||
|
||||
shellCheck :: String -> [ShellCheckComment]
|
||||
shellCheck script =
|
||||
|
||||
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, map) <- result
|
||||
let newMap = runAllAnalytics tree map
|
||||
return $ notesFromMap newMap
|
||||
(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, scMessage :: String }
|
||||
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, ": ", scMessage c]
|
||||
show c = concat ["(", show $ scLine c, ",", show $ scColumn c, ") ", scSeverity c, ": ", show (scCode c), " ", scMessage c]
|
||||
|
||||
severityToString s =
|
||||
case s of
|
||||
@@ -46,4 +63,5 @@ severityToString s =
|
||||
InfoC -> "info"
|
||||
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
|
||||
|
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
|
121
shellcheck.1.md
Normal file
121
shellcheck.1.md
Normal file
@@ -0,0 +1,121 @@
|
||||
% 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
|
||||
|
||||
**-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.
|
||||
|
||||
**-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.
|
||||
|
||||
# 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)
|
256
shellcheck.hs
256
shellcheck.hs
@@ -15,16 +15,74 @@
|
||||
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 Control.Exception
|
||||
import Control.Monad
|
||||
import Data.Char
|
||||
import Data.Maybe
|
||||
import GHC.Exts
|
||||
import GHC.IO.Device
|
||||
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
|
||||
|
||||
header = "Usage: shellcheck [OPTIONS...] FILES..."
|
||||
options = [
|
||||
Option ['f'] ["format"]
|
||||
(ReqArg (Flag "format") "FORMAT") "output format",
|
||||
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"]
|
||||
(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
|
||||
|
||||
parseArguments argv =
|
||||
case getOpt Permute options argv of
|
||||
(opts, files, []) -> do
|
||||
verifyOptions opts files
|
||||
return $ Just (opts, files)
|
||||
|
||||
(_, _, errors) -> do
|
||||
printErr $ (concat errors) ++ "\n" ++ usageInfo header options
|
||||
exitWith syntaxFailure
|
||||
|
||||
formats = Map.fromList [
|
||||
("json", forJson),
|
||||
("gcc", forGcc),
|
||||
("checkstyle", forCheckstyle),
|
||||
("tty", forTty)
|
||||
]
|
||||
|
||||
forTty options files = do
|
||||
output <- mapM doFile files
|
||||
return $ and output
|
||||
where
|
||||
clear = ansi 0
|
||||
ansi n = "\x1B[" ++ (show n) ++ "m"
|
||||
|
||||
@@ -38,21 +96,16 @@ colorForLevel _ = 0 -- none
|
||||
|
||||
colorComment level comment = (ansi $ colorForLevel level) ++ comment ++ clear
|
||||
|
||||
doFile path colorFunc = do
|
||||
let actualPath = if path == "-" then "/dev/stdin" else path
|
||||
exists <- doesFileExist actualPath
|
||||
if exists then do
|
||||
contents <- readFile actualPath
|
||||
doInput path contents colorFunc
|
||||
else do
|
||||
putStrLn (colorFunc "error" $ "No such file: " ++ actualPath)
|
||||
doFile path = do
|
||||
contents <- readContents path
|
||||
doInput path contents
|
||||
|
||||
doInput filename contents colorFunc = do
|
||||
doInput filename contents = do
|
||||
let fileLines = lines contents
|
||||
let lineCount = length fileLines
|
||||
let comments = shellCheck contents
|
||||
let comments = getComments options contents
|
||||
let groups = groupWith scLine comments
|
||||
if not $ null comments then do
|
||||
colorFunc <- getColorFunc
|
||||
mapM_ (\x -> do
|
||||
let lineNum = scLine (head x)
|
||||
let line = if lineNum < 1 || lineNum > lineCount
|
||||
@@ -64,23 +117,184 @@ doInput filename contents colorFunc = do
|
||||
mapM (\c -> putStrLn (colorFunc (scSeverity c) $ cuteIndent c)) x
|
||||
putStrLn ""
|
||||
) groups
|
||||
else do
|
||||
putStrLn ("No comments for " ++ filename)
|
||||
return $ null comments
|
||||
|
||||
cuteIndent comment =
|
||||
(replicate ((scColumn comment) - 1) ' ') ++ "^-- " ++ (scMessage 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 msg = concatMap escape' msg
|
||||
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
|
||||
|
||||
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 comments =
|
||||
filter (not . hasCode) comments
|
||||
where
|
||||
hasCode c = scCode c `elem` codes
|
||||
|
||||
main = do
|
||||
args <- getArgs
|
||||
colors <- getColorFunc
|
||||
if null args then do
|
||||
hPutStrLn stderr "shellcheck -- bash/sh script static analysis tool"
|
||||
hPutStrLn stderr "Usage: shellcheck filenames..."
|
||||
exitFailure
|
||||
else
|
||||
mapM (\f -> doFile f colors) args
|
||||
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
|
||||
|
||||
process Nothing = return False
|
||||
process (Just (options, files)) = do
|
||||
let format = fromMaybe "tty" $ getOption options "format" in
|
||||
case Map.lookup format formats of
|
||||
Nothing -> do
|
||||
printErr $ "Unknown format " ++ format
|
||||
printErr $ "Supported formats:"
|
||||
mapM_ (printErr . write) $ Map.keys formats
|
||||
exitWith supportFailure
|
||||
where write s = " " ++ s
|
||||
Just f -> do
|
||||
f options files
|
||||
|
||||
verifyOptions opts files = do
|
||||
when (isJust $ getOption opts "version") printVersionAndExit
|
||||
|
||||
let shell = getOption opts "shell" in
|
||||
if isNothing shell
|
||||
then return ()
|
||||
else when (isNothing $ shell >>= shellForExecutable) $ do
|
||||
printErr $ "Unknown shell: " ++ (fromJust shell)
|
||||
exitWith supportFailure
|
||||
|
||||
when (null files) $ do
|
||||
printErr "No files specified.\n"
|
||||
printErr $ usageInfo header options
|
||||
exitWith syntaxFailure
|
||||
|
||||
printVersionAndExit = 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
|
||||
|
Reference in New Issue
Block a user