mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 00:39:19 +08:00
Compare commits
23 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 |
6
README
6
README
@@ -27,6 +27,12 @@ On Ubuntu and similar, use:
|
||||
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:
|
||||
|
@@ -1,5 +1,6 @@
|
||||
Name: ShellCheck
|
||||
Version: 0.3.0
|
||||
-- Must also be updated in ShellCheck/Data.hs :
|
||||
Version: 0.3.1
|
||||
Synopsis: Shell script analysis tool
|
||||
License: OtherLicense
|
||||
License-file: LICENSE
|
||||
|
@@ -26,6 +26,8 @@ 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
|
||||
@@ -80,7 +82,7 @@ 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
|
||||
@@ -218,7 +220,7 @@ 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
|
||||
@@ -308,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
|
||||
@@ -342,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
@@ -1,8 +1,10 @@
|
||||
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",
|
||||
@@ -41,13 +43,13 @@ internalVariables = [
|
||||
]
|
||||
|
||||
variablesWithoutSpaces = [
|
||||
"$", "-", "?", "!",
|
||||
"$", "-", "?", "!",
|
||||
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
|
||||
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
|
||||
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
|
||||
]
|
||||
|
||||
commonCommands = [
|
||||
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",
|
||||
@@ -70,5 +72,5 @@ commonCommands = [
|
||||
"unalias", "uname", "uncompress", "unexpand", "unget", "uniq",
|
||||
"unlink", "unset", "uucp", "uudecode", "uuencode", "uustat", "uux",
|
||||
"val", "vi", "wait", "wc", "what", "who", "write", "xargs", "yacc",
|
||||
"zcat"
|
||||
"zcat"
|
||||
]
|
||||
|
@@ -17,7 +17,7 @@
|
||||
-}
|
||||
{-# LANGUAGE NoMonomorphismRestriction #-}
|
||||
|
||||
module ShellCheck.Parser (Note(..), Severity(..), parseShell, ParseResult(..), ParseNote(..), notesFromMap, Metadata(..), sortNotes) where
|
||||
module ShellCheck.Parser (Note(..), Severity(..), parseShell, ParseResult(..), ParseNote(..), sortNotes, noteToParseNote) where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.Data
|
||||
@@ -34,8 +34,6 @@ import System.IO
|
||||
import Text.Parsec.Error
|
||||
import GHC.Exts (sortWith)
|
||||
|
||||
lastError = 1074
|
||||
|
||||
backslash = char '\\'
|
||||
linefeed = (optional carriageReturn) >> char '\n'
|
||||
singleQuote = char '\'' <|> unicodeSingleQuote
|
||||
@@ -100,20 +98,20 @@ nbsp = do
|
||||
return ' '
|
||||
|
||||
--------- Message/position annotation on top of user state
|
||||
data Note = Note Severity Code String deriving (Show, Eq)
|
||||
data Note = Note Id Severity Code String deriving (Show, Eq)
|
||||
data ParseNote = ParseNote SourcePos Severity Code String deriving (Show, Eq)
|
||||
data Metadata = Metadata SourcePos [Note] deriving (Show)
|
||||
data Severity = ErrorC | WarningC | InfoC | StyleC deriving (Show, Eq, Ord)
|
||||
data Context = ContextName SourcePos String | ContextAnnotation [Annotation]
|
||||
type Code = Integer
|
||||
|
||||
codeForNote (Note _ code _) = code
|
||||
codeForParseNote (ParseNote _ _ code _) = code
|
||||
noteToParseNote map (Note id severity code message) =
|
||||
ParseNote pos severity code message
|
||||
where
|
||||
pos = fromJust $ Map.lookup id map
|
||||
|
||||
initialState = (Id $ -1, Map.empty, [])
|
||||
|
||||
getInitialMeta pos = Metadata pos []
|
||||
|
||||
getLastId = do
|
||||
(id, _, _) <- getState
|
||||
return id
|
||||
@@ -121,7 +119,7 @@ getLastId = do
|
||||
getNextIdAt sourcepos = do
|
||||
(id, map, notes) <- getState
|
||||
let newId = incId id
|
||||
let newMap = Map.insert newId (getInitialMeta sourcepos) map
|
||||
let newMap = Map.insert newId sourcepos map
|
||||
putState (newId, newMap, notes)
|
||||
return newId
|
||||
where incId (Id n) = (Id $ n+1)
|
||||
@@ -189,11 +187,6 @@ parseProblemAt pos level code msg = do
|
||||
Ms.modify (\(list, current) -> ((ParseNote pos level code msg):list, current))
|
||||
|
||||
-- Store non-parse problems inside
|
||||
addNoteFor id note = modifyMap $ Map.adjust (\(Metadata pos notes) -> Metadata pos (note:notes)) id
|
||||
|
||||
addNote note = do
|
||||
id <- getLastId
|
||||
addNoteFor id note
|
||||
|
||||
parseNote c l a = do
|
||||
pos <- getPosition
|
||||
@@ -269,6 +262,7 @@ readConditionContents single = do
|
||||
where
|
||||
typ = if single then SingleBracket else DoubleBracket
|
||||
readCondBinaryOp = try $ do
|
||||
optional guardArithmetic
|
||||
id <- getNextId
|
||||
op <- (choice $ (map tryOp ["==", "!=", "<=", ">=", "=~", ">", "<", "=", "\\<=", "\\>=", "\\<", "\\>"])) <|> otherOp
|
||||
hardCondSpacing
|
||||
@@ -284,6 +278,13 @@ readConditionContents single = do
|
||||
when (s == "-a" || s == "-o") $ fail "Wrong operator"
|
||||
return $ TC_Binary id typ s
|
||||
|
||||
guardArithmetic = do
|
||||
try . lookAhead $ disregard (oneOf "+*/%") <|> disregard (string "- ")
|
||||
parseProblem ErrorC 1076 $
|
||||
if single
|
||||
then "Trying to do math? Use e.g. [ $((i/2+7)) -ge 18 ]."
|
||||
else "Trying to do math? Use e.g. [[ $((i/2+7)) -ge 18 ]]."
|
||||
|
||||
readCondUnaryExp = do
|
||||
op <- readCondUnaryOp
|
||||
pos <- getPosition
|
||||
@@ -325,16 +326,13 @@ readConditionContents single = do
|
||||
readCondAndOp = do
|
||||
id <- getNextId
|
||||
x <- try (string "&&" <|> string "-a")
|
||||
when (single && x == "&&") $ addNoteFor id $ Note ErrorC 1022 "You can't use && inside [..]. Use [[..]] instead."
|
||||
when (not single && x == "-a") $ addNoteFor id $ Note ErrorC 1023 "In [[..]], use && instead of -a."
|
||||
softCondSpacing
|
||||
return $ TC_And id typ x
|
||||
|
||||
readCondOrOp = do
|
||||
optional guardArithmetic
|
||||
id <- getNextId
|
||||
x <- try (string "||" <|> string "-o")
|
||||
when (single && x == "||") $ addNoteFor id $ Note ErrorC 1024 "You can't use || inside [..]. Use [[..]] instead."
|
||||
when (not single && x == "-o") $ addNoteFor id $ Note ErrorC 1025 "In [[..]], use || instead of -o."
|
||||
softCondSpacing
|
||||
return $ TC_Or id typ x
|
||||
|
||||
@@ -444,6 +442,8 @@ prop_aA = isOk readArithmeticContents "! $?"
|
||||
prop_aB = isOk readArithmeticContents "10#08 * 16#f"
|
||||
prop_aC = isOk readArithmeticContents "\"$((3+2))\" + '37'"
|
||||
prop_aD = isOk readArithmeticContents "foo[9*y+x]++"
|
||||
prop_aE = isOk readArithmeticContents "1+`echo 2`"
|
||||
prop_aF = isOk readArithmeticContents "foo[`echo foo | sed s/foo/4/g` * 3] + 4"
|
||||
readArithmeticContents =
|
||||
readSequence
|
||||
where
|
||||
@@ -477,7 +477,7 @@ readArithmeticContents =
|
||||
|
||||
readExpansion = do
|
||||
id <- getNextId
|
||||
x <- readNormalDollar
|
||||
x <- readNormalDollar <|> readBackTicked
|
||||
spacing
|
||||
return $ TA_Expansion id x
|
||||
|
||||
@@ -1348,6 +1348,8 @@ readCmdWord = do
|
||||
prop_readIfClause = isOk readIfClause "if false; then foo; elif true; then stuff; more stuff; else cows; fi"
|
||||
prop_readIfClause2 = isWarning readIfClause "if false; then; echo oo; fi"
|
||||
prop_readIfClause3 = isWarning readIfClause "if false; then true; else; echo lol; fi"
|
||||
prop_readIfClause4 = isWarning readIfClause "if false; then true; else if true; then echo lol; fi"
|
||||
prop_readIfClause5 = isOk readIfClause "if false; then true; else\nif true; then echo lol; fi; fi"
|
||||
readIfClause = called "if expression" $ do
|
||||
id <- getNextId
|
||||
pos <- getPosition
|
||||
@@ -1389,7 +1391,9 @@ readIfPart = do
|
||||
|
||||
readElifPart = called "elif clause" $ do
|
||||
pos <- getPosition
|
||||
g_Elif
|
||||
correctElif <- elif
|
||||
when (not correctElif) $
|
||||
parseProblemAt pos ErrorC 1075 "Use 'elif' instead of 'else if'."
|
||||
allspacing
|
||||
condition <- readTerm
|
||||
g_Then
|
||||
@@ -1398,8 +1402,12 @@ readElifPart = called "elif clause" $ do
|
||||
verifyNotEmptyIf "then"
|
||||
action <- readTerm
|
||||
return (condition, action)
|
||||
where
|
||||
elif = (g_Elif >> return True) <|>
|
||||
(try $ g_Else >> g_If >> return False)
|
||||
|
||||
readElsePart = called "else clause" $ do
|
||||
pos <- getPosition
|
||||
g_Else
|
||||
acceptButWarn g_Semi ErrorC 1053 "No semicolons directly after 'else'."
|
||||
allspacing
|
||||
@@ -1478,15 +1486,14 @@ prop_readForClause6 = isOk readForClause "for ((;;))\ndo echo $i\ndone"
|
||||
prop_readForClause7 = isOk readForClause "for ((;;)) do echo $i\ndone"
|
||||
prop_readForClause8 = isOk readForClause "for ((;;)) ; do echo $i\ndone"
|
||||
prop_readForClause9 = isOk readForClause "for i do true; done"
|
||||
prop_readForClause10= isOk readForClause "for ((;;)) { true; }"
|
||||
readForClause = called "for loop" $ do
|
||||
pos <- getPosition
|
||||
(T_For id) <- g_For
|
||||
spacing
|
||||
typ <- (readRegular <|> readArithmetic)
|
||||
group <- readDoGroup pos
|
||||
typ id group
|
||||
readRegular id pos <|> readArithmetic id pos
|
||||
where
|
||||
readArithmetic = called "arithmetic for condition" $ do
|
||||
readArithmetic id pos = called "arithmetic for condition" $ do
|
||||
try $ string "(("
|
||||
x <- readArithmeticContents
|
||||
char ';' >> spacing
|
||||
@@ -1497,13 +1504,19 @@ readForClause = called "for loop" $ do
|
||||
string "))"
|
||||
spacing
|
||||
optional $ readSequentialSep >> spacing
|
||||
return $ \id group -> (return $ T_ForArithmetic id x y z group)
|
||||
group <- readBraced <|> readDoGroup pos
|
||||
return $ T_ForArithmetic id x y z group
|
||||
|
||||
readRegular = do
|
||||
readBraced = do
|
||||
(T_BraceGroup _ list) <- readBraceGroup
|
||||
return list
|
||||
|
||||
readRegular id pos = do
|
||||
name <- readVariableName
|
||||
spacing
|
||||
values <- readInClause <|> (optional readSequentialSep >> return [])
|
||||
return $ \id group -> (return $ T_ForIn id name values group)
|
||||
group <- readDoGroup pos
|
||||
return $ T_ForIn id name values group
|
||||
|
||||
prop_readSelectClause1 = isOk readSelectClause "select foo in *; do echo $foo; done"
|
||||
prop_readSelectClause2 = isOk readSelectClause "select foo; do echo $foo; done"
|
||||
@@ -1569,61 +1582,52 @@ readCaseItem = called "case item" $ do
|
||||
|
||||
prop_readFunctionDefinition = isOk readFunctionDefinition "foo() { command foo --lol \"$@\"; }"
|
||||
prop_readFunctionDefinition1 = isOk readFunctionDefinition "foo (){ command foo --lol \"$@\"; }"
|
||||
prop_readFunctionDefinition2 = isWarning readFunctionDefinition "function foo() { command foo --lol \"$@\"; }"
|
||||
prop_readFunctionDefinition3 = isWarning readFunctionDefinition "function foo { lol; }"
|
||||
prop_readFunctionDefinition4 = isWarning readFunctionDefinition "foo(a, b) { true; }"
|
||||
prop_readFunctionDefinition5 = isOk readFunctionDefinition ":(){ :|:;}"
|
||||
prop_readFunctionDefinition6 = isOk readFunctionDefinition "?(){ foo; }"
|
||||
prop_readFunctionDefinition7 = isOk readFunctionDefinition "..(){ cd ..; }"
|
||||
prop_readFunctionDefinition8 = isOk readFunctionDefinition "foo() (ls)"
|
||||
readFunctionDefinition = called "function" $ do
|
||||
id <- getNextId
|
||||
name <- try readFunctionSignature
|
||||
functionSignature <- try readFunctionSignature
|
||||
allspacing
|
||||
(disregard (lookAhead $ oneOf "{(") <|> parseProblem ErrorC 1064 "Expected a { to open the function definition.")
|
||||
group <- readBraceGroup <|> readSubshell
|
||||
return $ T_Function id name group
|
||||
|
||||
|
||||
readFunctionSignature = do
|
||||
readWithFunction <|> readWithoutFunction
|
||||
return $ functionSignature group
|
||||
where
|
||||
readWithFunction = do
|
||||
pos <- getPosition
|
||||
try $ do
|
||||
string "function"
|
||||
whitespace
|
||||
parseProblemAt pos InfoC 1005 "Drop the keyword 'function'. It's optional in Bash but invalid in other shells."
|
||||
spacing
|
||||
name <- readFunctionName
|
||||
optional spacing
|
||||
pos <- getPosition
|
||||
readParens <|> do
|
||||
parseProblemAt pos InfoC 1006 "Include '()' after the function name (in addition to dropping 'function')."
|
||||
return name
|
||||
readFunctionSignature = do
|
||||
readWithFunction <|> readWithoutFunction
|
||||
where
|
||||
readWithFunction = do
|
||||
id <- getNextId
|
||||
try $ do
|
||||
string "function"
|
||||
whitespace
|
||||
spacing
|
||||
name <- readFunctionName
|
||||
optional spacing
|
||||
hasParens <- wasIncluded readParens
|
||||
return $ T_Function id (FunctionKeyword True) (FunctionParentheses hasParens) name
|
||||
|
||||
readWithoutFunction = try $ do
|
||||
name <- readFunctionName
|
||||
optional spacing
|
||||
readParens
|
||||
return name
|
||||
|
||||
readParens = do
|
||||
g_Lparen
|
||||
optional spacing
|
||||
g_Rparen <|> do
|
||||
parseProblem ErrorC 1065 "Trying to declare parameters? Don't. Use () and refer to params as $1, $2.."
|
||||
many $ noneOf "\n){"
|
||||
g_Rparen
|
||||
return ()
|
||||
|
||||
readFunctionName = many1 functionChars
|
||||
readWithoutFunction = try $ do
|
||||
id <- getNextId
|
||||
name <- readFunctionName
|
||||
optional spacing
|
||||
readParens
|
||||
return $ T_Function id (FunctionKeyword False) (FunctionParentheses True) name
|
||||
|
||||
readParens = do
|
||||
g_Lparen
|
||||
optional spacing
|
||||
g_Rparen <|> do
|
||||
parseProblem ErrorC 1065 "Trying to declare parameters? Don't. Use () and refer to params as $1, $2.."
|
||||
many $ noneOf "\n){"
|
||||
g_Rparen
|
||||
return ()
|
||||
|
||||
readFunctionName = many1 functionChars
|
||||
|
||||
readPattern = (readNormalWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing)
|
||||
|
||||
|
||||
prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null"
|
||||
readCompoundCommand = do
|
||||
id <- getNextId
|
||||
@@ -1865,7 +1869,7 @@ isOk p s = (fst cs) && (null . snd $ cs) where cs = checkString p s
|
||||
|
||||
checkString parser string =
|
||||
case rp (parser >> eof >> getState) "-" string of
|
||||
(Right (tree, map, notes), (problems, _)) -> (True, (notesFromMap map) ++ notes ++ problems)
|
||||
(Right (tree, map, notes), (problems, _)) -> (True, notes ++ problems)
|
||||
(Left _, (n, _)) -> (False, n)
|
||||
|
||||
parseWithNotes parser = do
|
||||
@@ -1874,16 +1878,11 @@ parseWithNotes parser = do
|
||||
parseNotes <- getParseNotes
|
||||
return (item, map, nub . sortNotes $ parseNotes)
|
||||
|
||||
toParseNotes (Metadata pos list) = map (\(Note level code note) -> ParseNote pos level code note) list
|
||||
notesFromMap map = Map.fold (\x -> (++) (toParseNotes x)) [] map
|
||||
|
||||
getAllNotes result = (concatMap (notesFromMap . snd) (maybeToList . parseResult $ result)) ++ (parseNotes result)
|
||||
|
||||
compareNotes (ParseNote pos1 level1 _ s1) (ParseNote pos2 level2 _ s2) = compare (pos1, level1) (pos2, level2)
|
||||
sortNotes = sortBy compareNotes
|
||||
|
||||
|
||||
data ParseResult = ParseResult { parseResult :: Maybe (Token, Map.Map Id Metadata), parseNotes :: [ParseNote] } deriving (Show)
|
||||
data ParseResult = ParseResult { parseResult :: Maybe (Token, Map.Map Id SourcePos), parseNotes :: [ParseNote] } deriving (Show)
|
||||
|
||||
makeErrorFor parsecError =
|
||||
ParseNote (errorPos parsecError) ErrorC 1072 $ getStringFromParsec $ errorMessages parsecError
|
||||
@@ -1903,9 +1902,11 @@ getStringFromParsec errors =
|
||||
|
||||
parseShell filename contents = do
|
||||
case rp (parseWithNotes readScript) filename contents of
|
||||
(Right (script, map, notes), (parsenotes, _)) -> ParseResult (Just (script, map)) (nub $ sortNotes $ notes ++ parsenotes)
|
||||
(Left err, (p, context)) -> ParseResult Nothing (nub $ sortNotes $ p ++ (notesForContext context) ++ ([makeErrorFor err]))
|
||||
|
||||
(Right (script, map, notes), (parsenotes, _)) ->
|
||||
ParseResult (Just (script, map)) (nub $ sortNotes $ notes ++ parsenotes)
|
||||
(Left err, (p, context)) ->
|
||||
ParseResult Nothing
|
||||
(nub $ sortNotes $ p ++ (notesForContext context) ++ ([makeErrorFor err]))
|
||||
where
|
||||
isName (ContextName _ _) = True
|
||||
isName _ = False
|
||||
|
@@ -25,28 +25,28 @@ import Data.List
|
||||
|
||||
|
||||
prop_findsParseIssue =
|
||||
let comments = shellCheck "echo \"$12\"" in
|
||||
let comments = shellCheck "echo \"$12\"" [] in
|
||||
(length comments) == 1 && (scCode $ head comments) == 1037
|
||||
prop_commentDisablesParseIssue1 =
|
||||
null $ shellCheck "#shellcheck disable=SC1037\necho \"$12\""
|
||||
null $ shellCheck "#shellcheck disable=SC1037\necho \"$12\"" []
|
||||
prop_commentDisablesParseIssue2 =
|
||||
null $ shellCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\""
|
||||
null $ shellCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\"" []
|
||||
|
||||
prop_findsAnalysisIssue =
|
||||
let comments = shellCheck "echo $1" in
|
||||
let comments = shellCheck "echo $1" [] in
|
||||
(length comments) == 1 && (scCode $ head comments) == 2086
|
||||
prop_commentDisablesAnalysisIssue1 =
|
||||
null $ shellCheck "#shellcheck disable=SC2086\necho $1"
|
||||
null $ shellCheck "#shellcheck disable=SC2086\necho $1" []
|
||||
prop_commentDisablesAnalysisIssue2 =
|
||||
null $ shellCheck "#shellcheck disable=SC2086\n#lol\necho $1"
|
||||
null $ shellCheck "#shellcheck disable=SC2086\n#lol\necho $1" []
|
||||
|
||||
shellCheck :: String -> [ShellCheckComment]
|
||||
shellCheck script =
|
||||
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 $ filterByAnnotation tree newMap
|
||||
(tree, posMap) <- result
|
||||
let list = runAnalytics options tree
|
||||
return $ map (noteToParseNote posMap) $ filterByAnnotation tree list
|
||||
)
|
||||
in
|
||||
map formatNote $ nub $ sortNotes allNotes
|
||||
|
@@ -30,8 +30,11 @@ corner cases can cause delayed failures.
|
||||
options are cumulative, but all the codes can be specified at once,
|
||||
comma-separated as a single argument.
|
||||
|
||||
Also note that shellcheck supports multiple Bourne shell dialects, and
|
||||
examines the file's shebang to determine which one to use.
|
||||
**-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
|
||||
|
||||
|
@@ -18,10 +18,13 @@
|
||||
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
|
||||
@@ -37,7 +40,11 @@ options = [
|
||||
Option ['f'] ["format"]
|
||||
(ReqArg (Flag "format") "FORMAT") "output format",
|
||||
Option ['e'] ["exclude"]
|
||||
(ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings"
|
||||
(ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings",
|
||||
Option ['s'] ["shell"]
|
||||
(ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (bash,sh,ksh,zsh)",
|
||||
Option ['V'] ["version"]
|
||||
(NoArg $ Flag "version" "true") "Print version information"
|
||||
]
|
||||
|
||||
printErr = hPutStrLn stderr
|
||||
@@ -57,14 +64,9 @@ instance JSON ShellCheckComment where
|
||||
|
||||
parseArguments argv =
|
||||
case getOpt Permute options argv of
|
||||
(opts, files, []) ->
|
||||
if not $ null files
|
||||
then
|
||||
return $ Just (opts, files)
|
||||
else do
|
||||
printErr "No files specified.\n"
|
||||
printErr $ usageInfo header options
|
||||
exitWith syntaxFailure
|
||||
(opts, files, []) -> do
|
||||
verifyOptions opts files
|
||||
return $ Just (opts, files)
|
||||
|
||||
(_, _, errors) -> do
|
||||
printErr $ (concat errors) ++ "\n" ++ usageInfo header options
|
||||
@@ -200,7 +202,14 @@ commentsFor options file =
|
||||
liftM (getComments options) $ readContents file
|
||||
|
||||
getComments options contents =
|
||||
excludeCodes (getExclusions options) $ shellCheck 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
|
||||
|
||||
@@ -216,9 +225,9 @@ makeNonVirtual comments contents =
|
||||
real rest (r+1) (v + 8 - (v `mod` 8)) target
|
||||
real (_:rest) r v target = real rest (r+1) (v+1) target
|
||||
|
||||
getOption [] _ def = def
|
||||
getOption ((Flag var val):_) name _ | name == var = val
|
||||
getOption (_:rest) flag def = getOption rest flag def
|
||||
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
|
||||
@@ -256,8 +265,8 @@ main = do
|
||||
exitWith code
|
||||
|
||||
process Nothing = return False
|
||||
process (Just (options, files)) =
|
||||
let format = getOption options "format" "tty" in
|
||||
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
|
||||
@@ -268,3 +277,24 @@ process (Just (options, files)) =
|
||||
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