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:
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:

View File

@@ -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

View File

@@ -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

View File

@@ -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"
]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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