23 Commits

Author SHA1 Message Date
Vidar Holen
0e464ea476 Stable version 0.3.1
This release is dedicated to the Flycheck team,
even though ShellCheck is written entirely in Vim.
2014-02-03 20:22:30 -08:00
Vidar Holen
811df6f0da Fixed cabal file 2014-02-03 20:21:26 -08:00
Vidar Holen
4e5d32b05a Added --version flag 2014-02-03 20:06:59 -08:00
Vidar Holen
c5141b77bf Fixed parser not accepting `` in arithmetic contex 2014-02-03 16:45:48 -08:00
Vidar Holen
9dfeb6b42a Added -s to the man page 2014-02-02 21:56:04 -08:00
Vidar Holen
77916d2645 Fixed compilation error on GHC7 2014-02-02 21:47:02 -08:00
Vidar Holen
4968e7d9ff Added -s flag to override dialect, e.g. -s ksh 2014-02-02 19:28:09 -08:00
Vidar Holen
075d58ee90 Replaced parser error for 'function' with shell-aware check. 2014-02-02 13:39:44 -08:00
Vidar Holen
6a4a5a815e Don't consider last stage of pipeline a subshell for Ksh/Zsh
Also fixes the problem where pipelines were considered a single subshell.
2014-02-02 13:03:26 -08:00
Vidar Holen
76a39f254b Refactoring, 25% speedup.
* Checks now use Writer monad instead of State

* Parser no longer emits notes unrelated to parsing.

* All checks are now passed a parameter value, containing shell type,
  map from notes to parents and such. This eliminates recalculation
  and removes the need for a special group of parent examining checks.
2014-02-02 04:59:17 -08:00
Vidar Holen
8ec9fa43fd Warn about break/continue in subshells and outside loops 2014-02-01 23:45:26 -08:00
Vidar Holen
e8634a3c27 Removed duplicate check for [[ a == b + 1 ]] 2014-02-01 20:45:44 -08:00
Vidar Holen
9ae776530b Check for [[ i + 1 = 2 ]] 2014-01-27 22:47:48 -08:00
Vidar Holen
0ec62390d5 Merge branch 'master' of github.com:koalaman/shellcheck 2014-01-27 22:13:04 -08:00
Vidar Holen
82328cd86e Warn about literal "\ " just like literal quotes.
Also, do it recursively.
2014-01-27 22:11:46 -08:00
koalaman
5b58da7249 Merge pull request #75 from michaelsanford/master
Added MacPorts cabal install information to README
2014-01-27 12:11:19 -08:00
michaelsanford
8676517270 Macports install info thanks to @myint
Capital P on MacPorts
2014-01-27 14:41:40 -05:00
Vidar Holen
4262c4b1bf Allow {} in arithmetic for loops 2014-01-26 12:44:51 -08:00
Vidar Holen
7ad0110443 Don't warn about sed '$d' or '$p' 2014-01-25 14:54:05 -08:00
Vidar Holen
e9bba2f75a Don't warn about comma separation in for f in {a,b} 2014-01-25 14:30:25 -08:00
Vidar Holen
74ea5eaeec Parse but warn about "else if" 2014-01-25 14:12:31 -08:00
koalaman
b7ee5f4410 Merge pull request #67 from michaelsanford/master
Added cabal setup instructions for Mac OS X to README
2014-01-24 21:15:10 -08:00
Michael Sanford
e294db171e Added Mac OS instructions with brew (relates #11)
Un-Markdown text
2014-01-24 15:59:10 -05:00
9 changed files with 661 additions and 473 deletions

6
README
View File

@@ -27,6 +27,12 @@ On Ubuntu and similar, use:
For older releases, you may have to 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 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. Executables can be built with cabal. Tests currently still rely on a Makefile.
Install: Install:

View File

@@ -1,5 +1,6 @@
Name: ShellCheck Name: ShellCheck
Version: 0.3.0 -- Must also be updated in ShellCheck/Data.hs :
Version: 0.3.1
Synopsis: Shell script analysis tool Synopsis: Shell script analysis tool
License: OtherLicense License: OtherLicense
License-file: LICENSE License-file: LICENSE

View File

@@ -26,6 +26,8 @@ data Id = Id Int deriving (Show, Eq, Ord)
data Quoted = Quoted | Unquoted deriving (Show, Eq) data Quoted = Quoted | Unquoted deriving (Show, Eq)
data Dashed = Dashed | Undashed deriving (Show, Eq) data Dashed = Dashed | Undashed deriving (Show, Eq)
data AssignmentMode = Assign | Append deriving (Show, Eq) data AssignmentMode = Assign | Append deriving (Show, Eq)
data FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
data Token = data Token =
TA_Base Id String Token TA_Base Id String Token
@@ -80,7 +82,7 @@ data Token =
| 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 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
@@ -218,7 +220,7 @@ 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
@@ -308,7 +310,7 @@ getId t = case t of
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
@@ -342,3 +344,10 @@ doAnalysis f t = analyze f blank id t
doStackAnalysis startToken endToken t = analyze startToken endToken id t doStackAnalysis startToken endToken t = analyze startToken endToken id t
doTransform i t = runIdentity $ analyze blank blank i 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

View File

@@ -1,8 +1,10 @@
module ShellCheck.Data where module ShellCheck.Data where
shellcheckVersion = "0.3.1" -- Must also be updated in ShellCheck.cabal
internalVariables = [ internalVariables = [
-- Generic -- Generic
"", "_", "", "_", "rest", "REST",
-- Bash -- Bash
"BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC", "BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",

View File

@@ -17,7 +17,7 @@
-} -}
{-# LANGUAGE NoMonomorphismRestriction #-} {-# 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.AST
import ShellCheck.Data import ShellCheck.Data
@@ -34,8 +34,6 @@ import System.IO
import Text.Parsec.Error import Text.Parsec.Error
import GHC.Exts (sortWith) import GHC.Exts (sortWith)
lastError = 1074
backslash = char '\\' backslash = char '\\'
linefeed = (optional carriageReturn) >> char '\n' linefeed = (optional carriageReturn) >> char '\n'
singleQuote = char '\'' <|> unicodeSingleQuote singleQuote = char '\'' <|> unicodeSingleQuote
@@ -100,20 +98,20 @@ nbsp = do
return ' ' return ' '
--------- Message/position annotation on top of user state --------- 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 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 Severity = ErrorC | WarningC | InfoC | StyleC deriving (Show, Eq, Ord)
data Context = ContextName SourcePos String | ContextAnnotation [Annotation] data Context = ContextName SourcePos String | ContextAnnotation [Annotation]
type Code = Integer type Code = Integer
codeForNote (Note _ code _) = code
codeForParseNote (ParseNote _ _ 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, []) initialState = (Id $ -1, Map.empty, [])
getInitialMeta pos = Metadata pos []
getLastId = do getLastId = do
(id, _, _) <- getState (id, _, _) <- getState
return id return id
@@ -121,7 +119,7 @@ getLastId = do
getNextIdAt sourcepos = do getNextIdAt sourcepos = do
(id, map, notes) <- getState (id, map, notes) <- getState
let newId = incId id let newId = incId id
let newMap = Map.insert newId (getInitialMeta sourcepos) map let newMap = Map.insert newId sourcepos map
putState (newId, newMap, notes) putState (newId, newMap, notes)
return newId return newId
where incId (Id n) = (Id $ n+1) 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)) Ms.modify (\(list, current) -> ((ParseNote pos level code msg):list, current))
-- Store non-parse problems inside -- 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 parseNote c l a = do
pos <- getPosition pos <- getPosition
@@ -269,6 +262,7 @@ readConditionContents single = do
where where
typ = if single then SingleBracket else DoubleBracket typ = if single then SingleBracket else DoubleBracket
readCondBinaryOp = try $ do readCondBinaryOp = try $ do
optional guardArithmetic
id <- getNextId id <- getNextId
op <- (choice $ (map tryOp ["==", "!=", "<=", ">=", "=~", ">", "<", "=", "\\<=", "\\>=", "\\<", "\\>"])) <|> otherOp op <- (choice $ (map tryOp ["==", "!=", "<=", ">=", "=~", ">", "<", "=", "\\<=", "\\>=", "\\<", "\\>"])) <|> otherOp
hardCondSpacing hardCondSpacing
@@ -284,6 +278,13 @@ readConditionContents single = do
when (s == "-a" || s == "-o") $ fail "Wrong operator" when (s == "-a" || s == "-o") $ fail "Wrong operator"
return $ TC_Binary id typ s 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 readCondUnaryExp = do
op <- readCondUnaryOp op <- readCondUnaryOp
pos <- getPosition pos <- getPosition
@@ -325,16 +326,13 @@ readConditionContents single = do
readCondAndOp = do readCondAndOp = do
id <- getNextId id <- getNextId
x <- try (string "&&" <|> string "-a") 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 softCondSpacing
return $ TC_And id typ x return $ TC_And id typ x
readCondOrOp = do readCondOrOp = do
optional guardArithmetic
id <- getNextId id <- getNextId
x <- try (string "||" <|> string "-o") 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 softCondSpacing
return $ TC_Or id typ x return $ TC_Or id typ x
@@ -444,6 +442,8 @@ prop_aA = isOk readArithmeticContents "! $?"
prop_aB = isOk readArithmeticContents "10#08 * 16#f" prop_aB = isOk readArithmeticContents "10#08 * 16#f"
prop_aC = isOk readArithmeticContents "\"$((3+2))\" + '37'" prop_aC = isOk readArithmeticContents "\"$((3+2))\" + '37'"
prop_aD = isOk readArithmeticContents "foo[9*y+x]++" 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 = readArithmeticContents =
readSequence readSequence
where where
@@ -477,7 +477,7 @@ readArithmeticContents =
readExpansion = do readExpansion = do
id <- getNextId id <- getNextId
x <- readNormalDollar x <- readNormalDollar <|> readBackTicked
spacing spacing
return $ TA_Expansion id x 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_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_readIfClause2 = isWarning readIfClause "if false; then; echo oo; fi"
prop_readIfClause3 = isWarning readIfClause "if false; then true; else; echo lol; 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 readIfClause = called "if expression" $ do
id <- getNextId id <- getNextId
pos <- getPosition pos <- getPosition
@@ -1389,7 +1391,9 @@ readIfPart = do
readElifPart = called "elif clause" $ do readElifPart = called "elif clause" $ do
pos <- getPosition pos <- getPosition
g_Elif correctElif <- elif
when (not correctElif) $
parseProblemAt pos ErrorC 1075 "Use 'elif' instead of 'else if'."
allspacing allspacing
condition <- readTerm condition <- readTerm
g_Then g_Then
@@ -1398,8 +1402,12 @@ readElifPart = called "elif clause" $ do
verifyNotEmptyIf "then" verifyNotEmptyIf "then"
action <- readTerm action <- readTerm
return (condition, action) return (condition, action)
where
elif = (g_Elif >> return True) <|>
(try $ g_Else >> g_If >> return False)
readElsePart = called "else clause" $ do readElsePart = called "else clause" $ do
pos <- getPosition
g_Else g_Else
acceptButWarn g_Semi ErrorC 1053 "No semicolons directly after 'else'." acceptButWarn g_Semi ErrorC 1053 "No semicolons directly after 'else'."
allspacing allspacing
@@ -1478,15 +1486,14 @@ prop_readForClause6 = isOk readForClause "for ((;;))\ndo echo $i\ndone"
prop_readForClause7 = isOk readForClause "for ((;;)) do echo $i\ndone" prop_readForClause7 = isOk readForClause "for ((;;)) do echo $i\ndone"
prop_readForClause8 = 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_readForClause9 = isOk readForClause "for i do true; done"
prop_readForClause10= isOk readForClause "for ((;;)) { true; }"
readForClause = called "for loop" $ do readForClause = called "for loop" $ do
pos <- getPosition pos <- getPosition
(T_For id) <- g_For (T_For id) <- g_For
spacing spacing
typ <- (readRegular <|> readArithmetic) readRegular id pos <|> readArithmetic id pos
group <- readDoGroup pos
typ id group
where where
readArithmetic = called "arithmetic for condition" $ do readArithmetic id pos = called "arithmetic for condition" $ do
try $ string "((" try $ string "(("
x <- readArithmeticContents x <- readArithmeticContents
char ';' >> spacing char ';' >> spacing
@@ -1497,13 +1504,19 @@ readForClause = called "for loop" $ do
string "))" string "))"
spacing spacing
optional $ readSequentialSep >> 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 name <- readVariableName
spacing spacing
values <- readInClause <|> (optional readSequentialSep >> return []) 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_readSelectClause1 = isOk readSelectClause "select foo in *; do echo $foo; done"
prop_readSelectClause2 = isOk readSelectClause "select foo; 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_readFunctionDefinition = isOk readFunctionDefinition "foo() { command foo --lol \"$@\"; }"
prop_readFunctionDefinition1 = 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_readFunctionDefinition4 = isWarning readFunctionDefinition "foo(a, b) { true; }"
prop_readFunctionDefinition5 = isOk readFunctionDefinition ":(){ :|:;}" prop_readFunctionDefinition5 = isOk readFunctionDefinition ":(){ :|:;}"
prop_readFunctionDefinition6 = isOk readFunctionDefinition "?(){ foo; }" prop_readFunctionDefinition6 = isOk readFunctionDefinition "?(){ foo; }"
prop_readFunctionDefinition7 = isOk readFunctionDefinition "..(){ cd ..; }" prop_readFunctionDefinition7 = isOk readFunctionDefinition "..(){ cd ..; }"
prop_readFunctionDefinition8 = isOk readFunctionDefinition "foo() (ls)" prop_readFunctionDefinition8 = isOk readFunctionDefinition "foo() (ls)"
readFunctionDefinition = called "function" $ do readFunctionDefinition = called "function" $ do
id <- getNextId functionSignature <- try readFunctionSignature
name <- try readFunctionSignature
allspacing allspacing
(disregard (lookAhead $ oneOf "{(") <|> parseProblem ErrorC 1064 "Expected a { to open the function definition.") (disregard (lookAhead $ oneOf "{(") <|> parseProblem ErrorC 1064 "Expected a { to open the function definition.")
group <- readBraceGroup <|> readSubshell group <- readBraceGroup <|> readSubshell
return $ T_Function id name group return $ functionSignature group
readFunctionSignature = do
readWithFunction <|> readWithoutFunction
where where
readWithFunction = do readFunctionSignature = do
pos <- getPosition readWithFunction <|> readWithoutFunction
try $ do where
string "function" readWithFunction = do
whitespace id <- getNextId
parseProblemAt pos InfoC 1005 "Drop the keyword 'function'. It's optional in Bash but invalid in other shells." try $ do
spacing string "function"
name <- readFunctionName whitespace
optional spacing spacing
pos <- getPosition name <- readFunctionName
readParens <|> do optional spacing
parseProblemAt pos InfoC 1006 "Include '()' after the function name (in addition to dropping 'function')." hasParens <- wasIncluded readParens
return name return $ T_Function id (FunctionKeyword True) (FunctionParentheses hasParens) name
readWithoutFunction = try $ do readWithoutFunction = try $ do
name <- readFunctionName id <- getNextId
optional spacing name <- readFunctionName
readParens optional spacing
return name 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
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) readPattern = (readNormalWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing)
prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null" prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null"
readCompoundCommand = do readCompoundCommand = do
id <- getNextId id <- getNextId
@@ -1865,7 +1869,7 @@ isOk p s = (fst cs) && (null . snd $ cs) where cs = checkString p s
checkString parser string = checkString parser string =
case rp (parser >> eof >> getState) "-" string of 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) (Left _, (n, _)) -> (False, n)
parseWithNotes parser = do parseWithNotes parser = do
@@ -1874,16 +1878,11 @@ parseWithNotes parser = do
parseNotes <- getParseNotes parseNotes <- getParseNotes
return (item, map, nub . sortNotes $ parseNotes) 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) compareNotes (ParseNote pos1 level1 _ s1) (ParseNote pos2 level2 _ s2) = compare (pos1, level1) (pos2, level2)
sortNotes = sortBy compareNotes 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 = makeErrorFor parsecError =
ParseNote (errorPos parsecError) ErrorC 1072 $ getStringFromParsec $ errorMessages parsecError ParseNote (errorPos parsecError) ErrorC 1072 $ getStringFromParsec $ errorMessages parsecError
@@ -1903,9 +1902,11 @@ getStringFromParsec errors =
parseShell filename contents = do parseShell filename contents = do
case rp (parseWithNotes readScript) filename contents of case rp (parseWithNotes readScript) filename contents of
(Right (script, map, notes), (parsenotes, _)) -> ParseResult (Just (script, map)) (nub $ sortNotes $ notes ++ parsenotes) (Right (script, map, notes), (parsenotes, _)) ->
(Left err, (p, context)) -> ParseResult Nothing (nub $ sortNotes $ p ++ (notesForContext context) ++ ([makeErrorFor err])) ParseResult (Just (script, map)) (nub $ sortNotes $ notes ++ parsenotes)
(Left err, (p, context)) ->
ParseResult Nothing
(nub $ sortNotes $ p ++ (notesForContext context) ++ ([makeErrorFor err]))
where where
isName (ContextName _ _) = True isName (ContextName _ _) = True
isName _ = False isName _ = False

View File

@@ -25,28 +25,28 @@ import Data.List
prop_findsParseIssue = prop_findsParseIssue =
let comments = shellCheck "echo \"$12\"" in let comments = shellCheck "echo \"$12\"" [] in
(length comments) == 1 && (scCode $ head comments) == 1037 (length comments) == 1 && (scCode $ head comments) == 1037
prop_commentDisablesParseIssue1 = prop_commentDisablesParseIssue1 =
null $ shellCheck "#shellcheck disable=SC1037\necho \"$12\"" null $ shellCheck "#shellcheck disable=SC1037\necho \"$12\"" []
prop_commentDisablesParseIssue2 = prop_commentDisablesParseIssue2 =
null $ shellCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\"" null $ shellCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\"" []
prop_findsAnalysisIssue = prop_findsAnalysisIssue =
let comments = shellCheck "echo $1" in let comments = shellCheck "echo $1" [] in
(length comments) == 1 && (scCode $ head comments) == 2086 (length comments) == 1 && (scCode $ head comments) == 2086
prop_commentDisablesAnalysisIssue1 = prop_commentDisablesAnalysisIssue1 =
null $ shellCheck "#shellcheck disable=SC2086\necho $1" null $ shellCheck "#shellcheck disable=SC2086\necho $1" []
prop_commentDisablesAnalysisIssue2 = prop_commentDisablesAnalysisIssue2 =
null $ shellCheck "#shellcheck disable=SC2086\n#lol\necho $1" null $ shellCheck "#shellcheck disable=SC2086\n#lol\necho $1" []
shellCheck :: String -> [ShellCheckComment] shellCheck :: String -> [AnalysisOption] -> [ShellCheckComment]
shellCheck script = shellCheck script options =
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 $ filterByAnnotation tree newMap return $ map (noteToParseNote posMap) $ filterByAnnotation tree list
) )
in in
map formatNote $ nub $ sortNotes allNotes map formatNote $ nub $ sortNotes allNotes

View File

@@ -30,8 +30,11 @@ corner cases can cause delayed failures.
options are cumulative, but all the codes can be specified at once, options are cumulative, but all the codes can be specified at once,
comma-separated as a single argument. comma-separated as a single argument.
Also note that shellcheck supports multiple Bourne shell dialects, and **-s**\ *shell*,\ **--shell=***shell*
examines the file's shebang to determine which one to use.
: 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 # FORMATS

View File

@@ -18,10 +18,13 @@
import Control.Exception import Control.Exception
import Control.Monad import Control.Monad
import Data.Char import Data.Char
import Data.Maybe
import GHC.Exts import GHC.Exts
import GHC.IO.Device import GHC.IO.Device
import Prelude hiding (catch) import Prelude hiding (catch)
import ShellCheck.Data
import ShellCheck.Simple import ShellCheck.Simple
import ShellCheck.Analytics
import System.Console.GetOpt import System.Console.GetOpt
import System.Directory import System.Directory
import System.Environment import System.Environment
@@ -37,7 +40,11 @@ options = [
Option ['f'] ["format"] Option ['f'] ["format"]
(ReqArg (Flag "format") "FORMAT") "output format", (ReqArg (Flag "format") "FORMAT") "output format",
Option ['e'] ["exclude"] 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 printErr = hPutStrLn stderr
@@ -57,14 +64,9 @@ instance JSON ShellCheckComment where
parseArguments argv = parseArguments argv =
case getOpt Permute options argv of case getOpt Permute options argv of
(opts, files, []) -> (opts, files, []) -> do
if not $ null files verifyOptions opts files
then return $ Just (opts, files)
return $ Just (opts, files)
else do
printErr "No files specified.\n"
printErr $ usageInfo header options
exitWith syntaxFailure
(_, _, errors) -> do (_, _, errors) -> do
printErr $ (concat errors) ++ "\n" ++ usageInfo header options printErr $ (concat errors) ++ "\n" ++ usageInfo header options
@@ -200,7 +202,14 @@ commentsFor options file =
liftM (getComments options) $ readContents file liftM (getComments options) $ readContents file
getComments options contents = 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 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+1) (v + 8 - (v `mod` 8)) target
real (_:rest) r v target = real rest (r+1) (v+1) target real (_:rest) r v target = real rest (r+1) (v+1) target
getOption [] _ def = def getOption [] _ = Nothing
getOption ((Flag var val):_) name _ | name == var = val getOption ((Flag var val):_) name | name == var = return val
getOption (_:rest) flag def = getOption rest flag def getOption (_:rest) flag = getOption rest flag
getOptions options name = getOptions options name =
map (\(Flag _ val) -> val) . filter (\(Flag var _) -> var == name) $ options map (\(Flag _ val) -> val) . filter (\(Flag var _) -> var == name) $ options
@@ -256,8 +265,8 @@ main = do
exitWith code exitWith code
process Nothing = return False process Nothing = return False
process (Just (options, files)) = process (Just (options, files)) = do
let format = getOption options "format" "tty" in let format = fromMaybe "tty" $ getOption options "format" in
case Map.lookup format formats of case Map.lookup format formats of
Nothing -> do Nothing -> do
printErr $ "Unknown format " ++ format printErr $ "Unknown format " ++ format
@@ -268,3 +277,24 @@ process (Just (options, files)) =
Just f -> do Just f -> do
f options files 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