60 Commits

Author SHA1 Message Date
Vidar Holen
de0145fb29 Stable version 0.3.2
This release is dedicated to knirch, mcandre, Dridi,
ptman, pihentagy, Riviera, and everyone else who keeps
submitting bug reports and feature suggestions!
2014-03-22 11:05:27 -07:00
Vidar Holen
0d4ae95e1d Recognize declare -x as exporting variables 2014-03-22 10:43:56 -07:00
Vidar Holen
50db49e2fb Rename Unquotable to QuoteFree 2014-03-22 10:27:59 -07:00
Vidar Holen
60aafae21d Count array indexes as references, even without $ 2014-03-22 10:22:34 -07:00
Vidar Holen
902cb9c303 Fixed up README 2014-03-19 09:57:01 -07:00
Vidar Holen
4f1fd43360 Don't suggest removing $ in (( 10#$n )) 2014-03-16 15:06:18 -07:00
Vidar Holen
ca5af5c55a Don't warn about decimals in (( )) for zsh/ksh 2014-03-16 14:56:23 -07:00
Vidar Holen
503cac3bb3 Merge branch 'master' of github.com:koalaman/shellcheck 2014-03-16 14:53:18 -07:00
Vidar Holen
2a9c9ae0ad Warn about using <=/>=, and don't warn about -gt 1.2 in ksh/zsh 2014-03-16 14:51:46 -07:00
koalaman
def4551991 Merge pull request #117 from mcandre/master
readme: markdown for link rendering
2014-03-15 16:17:07 -07:00
Vidar Holen
67f4a0d6eb Accept and warn about capitalization in keywords. 2014-03-15 16:08:33 -07:00
Andrew Pennebaker
f92f934688 readme: added compiler memory note 2014-03-15 16:49:05 -04:00
Andrew Pennebaker
d4059c30b7 readme: markdown for link rendering 2014-03-14 16:37:34 -04:00
Vidar Holen
b68de7f42b Don't warn about for s in "${!var}", it could be an array 2014-03-13 20:54:10 -07:00
Vidar Holen
7dacb62d36 Fixed determining shell for shebangs with flags 2014-03-09 17:24:05 -07:00
Vidar Holen
3423cde931 Check attempts to set variables with 'set' 2014-03-01 16:52:53 -08:00
Vidar Holen
b2d1aa01f7 Don't warn about commas when quoted in a=("a,b") 2014-03-01 15:30:51 -08:00
Vidar Holen
19e1bdf11f Warn about array assignments for /bin/sh 2014-03-01 15:16:31 -08:00
Vidar Holen
75d51087c8 Warn about functions using parameters that are never passed 2014-03-01 14:42:00 -08:00
Vidar Holen
ed524fb77f Don't warn about decimals when comparing with = 2014-03-01 10:24:22 -08:00
Vidar Holen
97045c4af1 Fixed x[0] not recognized as reference of x in arithmetics 2014-03-01 10:14:17 -08:00
Vidar Holen
1b806f6c9f Merge branch 'master' of github.com:koalaman/shellcheck 2014-03-01 10:06:03 -08:00
Vidar Holen
632c1614a1 Added support for |& 2014-03-01 10:05:43 -08:00
koalaman
00d9ef12e7 Merge pull request #105 from Dridi/dynamic_link
Dynamic linking to libShellCheck
2014-03-01 09:00:47 -08:00
Vidar Holen
d07294810b Allow \n before and after ||/&& in [[ ]] 2014-02-28 18:46:10 -08:00
Dridi Boukelmoune
948b750754 Make the executable depend on the library
It needed a bump to 1.8 for the minimum `Cabal-Version'. One downside is
that the executable also build-depends on the same libraries.
Alphabetical order is kept, except for the dependency to the ShellCheck
library itself.
2014-02-28 19:55:04 +01:00
Dridi Boukelmoune
41ae95116d Reformat ShellCheck.cabal for readability
Uses one line per `build-depends' or `exposed-modules'. Also got them
sorted by name. Folded `base' dependencies into a single one.
2014-02-28 19:44:09 +01:00
Vidar Holen
bf3c942294 Warn about using 'su foo' to continue as foo 2014-02-16 18:51:30 -08:00
Vidar Holen
055b40462d Improved $(echo ..) warnings 2014-02-16 13:26:50 -08:00
Vidar Holen
b087b7efb1 Some hlint fixes.
Ironically, this is the first time the linter has been linted.
2014-02-16 12:57:34 -08:00
Vidar Holen
5d8d57cf07 Suggest useless use of echo for $(echo $var) 2014-02-12 19:20:39 -08:00
Vidar Holen
661091a9da Added better message for SC1007, for 'var= value' 2014-02-12 18:26:41 -08:00
Vidar Holen
2ec60c2627 Added double prime to list of unicode quotes. 2014-02-08 14:15:04 -08:00
Vidar Holen
8b4909b238 Improve warnings for missing quotes. 2014-02-08 14:10:45 -08:00
Vidar Holen
95a3be6546 README: Updated URL, reformatted long lines 2014-02-08 09:58:11 -08:00
Vidar Holen
968e34e002 Parse forward ticks (acute accents) just like backticks and warn. 2014-02-08 09:50:20 -08:00
Vidar Holen
197b3e3f20 Some checks for accidental rm -r 2014-02-04 19:43:16 -08:00
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
10 changed files with 1348 additions and 676 deletions

38
README
View File

@@ -1,38 +0,0 @@
ShellCheck - A shell script static analysis tool
http://www.vidarholen.net/contents/shellcheck
Copyright 2012, Vidar 'koala_man' Holen
Licensed under the GNU Affero General Public License, v3
The goals of ShellCheck are:
- To point out and clarify typical beginner's syntax issues,
that causes a shell to give cryptic error messages.
- To point out and clarify typical intermediate level semantic problems,
that causes a shell to behave strangely and counter-intuitively.
- To point out subtle caveats, corner cases and pitfalls, that may cause an
advanced user's otherwise working script to fail under future circumstances.
ShellCheck is written in Haskell, and requires GHC, Parsec3 and Text.Regex.
To build the JSON interface and run the unit tests, it also requires QuickCheck2 and JSON.
On 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
Executables can be built with cabal. Tests currently still rely on a Makefile.
Install:
cabal install
which shellcheck
~/.cabal/bin/shellcheck
Happy ShellChecking!

68
README.md Normal file
View File

@@ -0,0 +1,68 @@
# ShellCheck - A shell script static analysis tool
http://www.shellcheck.net
Copyright 2012-2014, Vidar 'koala_man' Holen
Licensed under the GNU Affero General Public License, v3
The goals of ShellCheck are:
- To point out and clarify typical beginner's syntax issues,
that causes a shell to give cryptic error messages.
- To point out and clarify typical intermediate level semantic problems,
that causes a shell to behave strangely and counter-intuitively.
- To point out subtle caveats, corner cases and pitfalls, that may cause an
advanced user's otherwise working script to fail under future circumstances.
ShellCheck requires at least 1 GB of RAM to compile. Executables can be built with cabal. Tests currently still rely on a Makefile.
## Building with Cabal
Make sure cabal is installed. On Debian based distros:
apt-get install cabal-install
On Fedora:
yum install cabal-install
On Mac OS X with homebrew (http://brew.sh/):
brew install cabal-install
On Mac OS X with MacPorts (http://www.macports.org/):
port install hs-cabal-install
With cabal installed, cd to the shellcheck source directory and:
$ cabal install
...
$ which shellcheck
~/.cabal/bin/shellcheck
## Building with Make
ShellCheck is written in Haskell, and requires GHC, Parsec3, JSON and
Text.Regex. To run the unit tests, it also requires QuickCheck2.
On Fedora, these can be installed with:
yum 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
To build and run the tests, cd to the shellcheck source directory and:
$ make
Happy ShellChecking!

View File

@@ -1,5 +1,6 @@
Name: ShellCheck
Version: 0.3.0
-- Must also be updated in ShellCheck/Data.hs :
Version: 0.3.2
Synopsis: Shell script analysis tool
License: OtherLicense
License-file: LICENSE
@@ -8,7 +9,7 @@ Author: Vidar Holen
Maintainer: vidar@vidarholen.net
Homepage: http://www.shellcheck.net/
Build-Type: Simple
Cabal-Version: >= 1.6
Cabal-Version: >= 1.8
Bug-reports: https://github.com/koalaman/shellcheck/issues
Description:
The goals of ShellCheck are:
@@ -27,8 +28,29 @@ source-repository head
location: git://github.com/koalaman/shellcheck.git
library
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
build-depends:
base >= 4 && < 5,
containers,
directory,
json,
mtl,
parsec,
regex-compat
exposed-modules:
ShellCheck.Analytics
ShellCheck.AST
ShellCheck.Data
ShellCheck.Parser
ShellCheck.Simple
executable shellcheck
build-depends:
ShellCheck,
base >= 4 && < 5,
containers,
directory,
json,
mtl,
parsec,
regex-compat
main-is: shellcheck.hs

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
@@ -100,7 +102,7 @@ data Token =
| T_NormalWord Id [Token]
| T_OR_IF Id
| T_OrIf Id (Token) (Token)
| T_Pipeline Id [Token]
| T_Pipeline Id [Token] [Token] -- [Pipe separators] [Commands]
| T_ProcSub Id String [Token]
| T_Rbrace Id
| T_Redirecting Id [Token] Token
@@ -118,6 +120,7 @@ data Token =
| T_While Id
| T_WhileExpression Id [Token] [Token]
| T_Annotation Id [Annotation] Token
| T_Pipe Id String
deriving (Show)
data Annotation = DisableComment Integer deriving (Show, Eq)
@@ -126,12 +129,12 @@ data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
-- I apologize for nothing!
lolHax s = Re.subRegex (Re.mkRegex "(Id [0-9]+)") (show s) "(Id 0)"
instance Eq Token where
(==) a b = (lolHax a) == (lolHax b)
(==) a b = lolHax a == lolHax b
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> Token) -> Token -> m Token
analyze f g i t =
round t
analyze f g i =
round
where
round t = do
f t
@@ -180,7 +183,7 @@ analyze f g i t =
b <- round cmd
return $ T_Redirecting id a b
delve (T_SimpleCommand id vars cmds) = dll vars cmds $ T_SimpleCommand id
delve (T_Pipeline id l) = dl l $ T_Pipeline id
delve (T_Pipeline id l1 l2) = dll l1 l2 $ T_Pipeline id
delve (T_Banged id l) = d1 l $ T_Banged id
delve (T_AndIf id t u) = d2 t u $ T_AndIf id
delve (T_OrIf id t u) = d2 t u $ T_OrIf id
@@ -218,7 +221,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
@@ -295,7 +298,7 @@ getId t = case t of
T_Array id _ -> id
T_Redirecting id _ _ -> id
T_SimpleCommand id _ _ -> id
T_Pipeline id _ -> id
T_Pipeline id _ _ -> id
T_Banged id _ -> id
T_AndIf id _ _ -> id
T_OrIf id _ _ -> id
@@ -308,7 +311,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
@@ -335,10 +338,18 @@ getId t = case t of
T_DollarDoubleQuoted id _ -> id
T_DollarBracket id _ -> id
T_Annotation id _ _ -> id
T_Pipe id _ -> id
blank :: Monad m => Token -> m ()
blank = const $ return ()
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
doAnalysis f = analyze f blank id
doStackAnalysis startToken endToken = analyze startToken endToken id
doTransform i = runIdentity . analyze blank blank i
isLoop t = case t of
T_WhileExpression {} -> True
T_UntilExpression {} -> True
T_ForIn {} -> True
T_ForArithmetic {} -> True
T_SelectIn {} -> True
_ -> False

File diff suppressed because it is too large Load Diff

View File

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

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
@@ -48,14 +46,18 @@ tokenDelimiter = oneOf "&|;<> \t\n\r" <|> nbsp
quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars
quotable = nbsp <|> unicodeDoubleQuote <|> oneOf quotableChars
bracedQuotable = oneOf "}\"$`'"
doubleQuotableChars = "\"$`\x201C\x201D"
doubleQuotableChars = "\"$`" ++ unicodeDoubleQuoteChars
doubleQuotable = unicodeDoubleQuote <|> oneOf doubleQuotableChars
whitespace = oneOf " \t\n" <|> carriageReturn <|> nbsp
linewhitespace = oneOf " \t" <|> nbsp
suspectCharAfterQuotes = variableChars <|> char '%'
extglobStartChars = "?*@!+"
extglobStart = oneOf extglobStartChars
unicodeDoubleQuoteChars = "\x201C\x201D\x2033\x2036"
prop_spacing = isOk spacing " \\\n # Comment"
spacing = do
x <- many (many1 linewhitespace <|> (try $ string "\\\n"))
@@ -80,7 +82,7 @@ allspacingOrFail = do
unicodeDoubleQuote = do
pos <- getPosition
char '\x201C' <|> char '\x201D'
oneOf unicodeDoubleQuoteChars
parseProblemAt pos WarningC 1015 "This is a unicode double quote. Delete and retype it."
return '"'
@@ -100,20 +102,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 +123,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 +191,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 +266,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 +282,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
@@ -312,9 +317,11 @@ readConditionContents single = do
when (endedWith "]" x) $ do
parseProblemAt pos ErrorC 1020 $
"You need a space before the " ++ (if single then "]" else "]]") ++ "."
fail "Missing space before ]"
when (single && endedWith ")" x) $ do
parseProblemAt pos ErrorC 1021 $
"You need a space before the \\)"
fail "Missing space before )"
disregard spacing
return x
where endedWith str (T_NormalWord id s@(_:_)) =
@@ -325,17 +332,16 @@ 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
skipLineFeeds
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
skipLineFeeds
return $ TC_Or id typ x
readCondNoaryOrBinary = do
@@ -414,7 +420,17 @@ readConditionContents single = do
str <- string "|"
return $ T_Literal id str
readCondTerm = readCondNot <|> readCondExpr
skipLineFeeds = do
pos <- getPosition
spacing <- allspacing
when (single && '\n' `elem` spacing) $
parseProblemAt pos ErrorC 1080 "In [ ] you need \\ before line feeds."
readCondTerm = do
term <- readCondNot <|> readCondExpr
skipLineFeeds
return term
readCondNot = do
id <- getNextId
char '!'
@@ -444,6 +460,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 +495,7 @@ readArithmeticContents =
readExpansion = do
id <- getNextId
x <- readNormalDollar
x <- readNormalDollar <|> readBackTicked
spacing
return $ TA_Expansion id x
@@ -611,6 +629,9 @@ prop_readCondition6 = isOk readCondition "[[ $c =~ ^[yY]$ ]]"
prop_readCondition7 = isOk readCondition "[[ ${line} =~ ^[[:space:]]*# ]]"
prop_readCondition8 = isOk readCondition "[[ $l =~ ogg|flac ]]"
prop_readCondition9 = isOk readCondition "[ foo -a -f bar ]"
prop_readCondition10= isOk readCondition "[[ a == b \n || c == d ]]"
prop_readCondition11= isOk readCondition "[[ a == b || \n c == d ]]"
prop_readCondition12= isWarning readCondition "[ a == b \n -o c == d ]"
readCondition = called "test expression" $ do
opos <- getPosition
id <- getNextId
@@ -739,17 +760,29 @@ readProcSub = called "process substitution" $ do
prop_readSingleQuoted = isOk readSingleQuoted "'foo bar'"
prop_readSingleQuoted2 = isWarning readSingleQuoted "'foo bar\\'"
prop_readsingleQuoted3 = isWarning readSingleQuoted "\x2018hello\x2019"
prop_readSingleQuoted4 = isWarning readNormalWord "'it's"
prop_readSingleQuoted5 = isWarning readSimpleCommand "foo='bar\ncow 'arg"
prop_readSingleQuoted6 = isOk readSimpleCommand "foo='bar cow 'arg"
readSingleQuoted = called "single quoted string" $ do
id <- getNextId
startPos <- getPosition
singleQuote
s <- readSingleQuotedPart `reluctantlyTill` singleQuote
pos <- getPosition
let string = concat s
endPos <- getPosition
singleQuote <?> "end of single quoted string"
let string = concat s
return (T_SingleQuoted id string) `attempting` do
x <- lookAhead anyChar
when (isAlpha x && not (null string) && isAlpha (last string)) $ parseProblemAt pos WarningC 1011 "This apostrophe terminated the single quoted string!"
optional $ do
c <- try . lookAhead $ suspectCharAfterQuotes <|> oneOf "'"
if (not (null string) && isAlpha c && isAlpha (last string))
then
parseProblemAt endPos WarningC 1011 $
"This apostrophe terminated the single quoted string!"
else
when ('\n' `elem` string && not ("\n" `isPrefixOf` string)) $
suggestForgotClosingQuote startPos endPos "single quoted string"
return (T_SingleQuoted id string)
readSingleQuotedLiteral = do
singleQuote
@@ -763,13 +796,24 @@ readSingleQuotedPart =
prop_readBackTicked = isOk readBackTicked "`ls *.mp3`"
prop_readBackTicked2 = isOk readBackTicked "`grep \"\\\"\"`"
prop_readBackTicked3 = isWarning readBackTicked "´grep \"\\\"\"´"
prop_readBackTicked4 = isOk readBackTicked "`echo foo\necho bar`"
prop_readBackTicked5 = isOk readSimpleCommand "echo `foo`bar"
prop_readBackTicked6 = isWarning readSimpleCommand "echo `foo\necho `bar"
readBackTicked = called "backtick expansion" $ do
id <- getNextId
pos <- getPosition
char '`'
startPos <- getPosition
backtick
subStart <- getPosition
subString <- readGenericLiteral "`"
char '`'
subString <- readGenericLiteral "`´"
endPos <- getPosition
backtick
optional $ do
c <- try . lookAhead $ suspectCharAfterQuotes
when ('\n' `elem` subString && not ("\n" `isPrefixOf` subString)) $ do
suggestForgotClosingQuote startPos endPos "backtick expansion"
-- Result positions may be off due to escapes
result <- subParse subStart readCompoundList (unEscape subString)
return $ T_Backticked id result
@@ -778,6 +822,12 @@ readBackTicked = called "backtick expansion" $ do
unEscape ('\\':x:rest) | x `elem` "$`\\" = x : unEscape rest
unEscape ('\\':'\n':rest) = unEscape rest
unEscape (c:rest) = c : unEscape rest
backtick =
disregard (char '`') <|> do
pos <- getPosition
char '´'
parseProblemAt pos ErrorC 1077 $
"For command expansion, the tick should slant left (` vs ´)."
subParse pos parser input = do
lastPosition <- getPosition
@@ -792,12 +842,31 @@ subParse pos parser input = do
prop_readDoubleQuoted = isOk readDoubleQuoted "\"Hello $FOO\""
prop_readDoubleQuoted2 = isOk readDoubleQuoted "\"$'\""
prop_readDoubleQuoted3 = isWarning readDoubleQuoted "\x201Chello\x201D"
prop_readDoubleQuoted4 = isWarning readSimpleCommand "\"foo\nbar\"foo"
prop_readDoubleQuoted5 = isOk readSimpleCommand "lol \"foo\nbar\" etc"
readDoubleQuoted = called "double quoted string" $ do
id <- getNextId
startPos <- getPosition
doubleQuote
x <- many doubleQuotedPart
endPos <- getPosition
doubleQuote <?> "end of double quoted string"
optional $ do
try . lookAhead $ suspectCharAfterQuotes <|> oneOf "$\""
when (any hasLineFeed x && not (startsWithLineFeed x)) $
suggestForgotClosingQuote startPos endPos "double quoted string"
return $ T_DoubleQuoted id x
where
startsWithLineFeed ((T_Literal _ ('\n':_)):_) = True
startsWithLineFeed _ = False
hasLineFeed (T_Literal _ str) | '\n' `elem` str = True
hasLineFeed _ = False
suggestForgotClosingQuote startPos endPos name = do
parseProblemAt startPos WarningC 1078 $
"Did you forget to close this " ++ name ++ "?"
parseProblemAt endPos InfoC 1079 $
"This is actually an end quote, but due to next char it looks suspect."
doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readBackTicked
@@ -1325,13 +1394,25 @@ transformWithSeparator i _ = id
readPipeSequence = do
id <- getNextId
list <- readCommand `sepBy1` (readPipe `thenSkip` (spacing >> readLineBreak))
(cmds, pipes) <- sepBy1WithSeparators readCommand
(readPipe `thenSkip` (spacing >> readLineBreak))
spacing
return $ T_Pipeline id list
return $ T_Pipeline id pipes cmds
where
sepBy1WithSeparators p s = do
let elems = p >>= \x -> return ([x], [])
let seps = do
separator <- s
return $ \(a,b) (c,d) -> (a++c, b ++ d ++ [separator])
elems `chainl1` seps
readPipe = do
notFollowedBy2 g_OR_IF
char '|' `thenSkip` spacing
id <- getNextId
char '|'
qualifier <- string "&" <|> return ""
spacing
return $ T_Pipe id ('|':qualifier)
readCommand = (readCompoundCommand <|> readSimpleCommand)
@@ -1348,6 +1429,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 +1472,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 +1483,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 +1567,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 +1585,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 +1663,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
@@ -1678,7 +1763,8 @@ readAssignmentWord = try $ do
if space == "" && space2 /= ""
then do
when (variable /= "IFS") $
parseNoteAt pos InfoC 1007 $ "Note that 'var= value' (with space after equals sign) is similar to 'var=\"\"; value'."
parseNoteAt pos WarningC 1007
"Remove space after = if trying to assign a value (for empty string, use var='' ... )."
value <- readEmptyLiteral
return $ T_Assignment id op variable index value
else do
@@ -1725,16 +1811,24 @@ redirToken c t = try $ do
notFollowedBy2 $ char '('
return $ t id
tryWordToken s t = tryParseWordToken (string s) t `thenSkip` spacing
tryParseWordToken parser t = try $ do
tryWordToken s t = tryParseWordToken s t `thenSkip` spacing
tryParseWordToken keyword t = try $ do
id <- getNextId
parser
str <- anycaseString keyword
optional (do
try . lookAhead $ char '['
parseProblem ErrorC 1069 "You need a space before the [.")
try $ lookAhead (keywordSeparator)
when (str /= keyword) $
parseProblem ErrorC 1081 $
"Scripts are case sensitive. Use '" ++ keyword ++ "', not '" ++ str ++ "'."
return $ t id
anycaseString str =
mapM anycaseChar str
where
anycaseChar c = char (toLower c) <|> char (toUpper c)
g_AND_IF = tryToken "&&" T_AND_IF
g_OR_IF = tryToken "||" T_OR_IF
g_DSEMI = tryToken ";;" T_DSEMI
@@ -1865,7 +1959,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 +1968,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 +1992,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,17 +64,12 @@ 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
printErr $ concat errors ++ "\n" ++ usageInfo header options
exitWith syntaxFailure
formats = Map.fromList [
@@ -82,7 +84,7 @@ forTty options files = do
return $ and output
where
clear = ansi 0
ansi n = "\x1B[" ++ (show n) ++ "m"
ansi n = "\x1B[" ++ show n ++ "m"
colorForLevel "error" = 31 -- red
colorForLevel "warning" = 33 -- yellow
@@ -92,7 +94,8 @@ forTty options files = do
colorForLevel "source" = 0 -- none
colorForLevel _ = 0 -- none
colorComment level comment = (ansi $ colorForLevel level) ++ comment ++ clear
colorComment level comment =
ansi (colorForLevel level) ++ comment ++ clear
doFile path = do
contents <- readContents path
@@ -110,15 +113,17 @@ forTty options files = do
then ""
else fileLines !! (lineNum - 1)
putStrLn ""
putStrLn $ colorFunc "message" ("In " ++ filename ++" line " ++ (show $ lineNum) ++ ":")
putStrLn $ colorFunc "message"
("In " ++ filename ++" line " ++ show lineNum ++ ":")
putStrLn (colorFunc "source" line)
mapM (\c -> putStrLn (colorFunc (scSeverity c) $ cuteIndent c)) x
mapM_ (\c -> putStrLn (colorFunc (scSeverity c) $ cuteIndent c)) x
putStrLn ""
) groups
return $ null comments
cuteIndent comment =
(replicate ((scColumn comment) - 1) ' ') ++ "^-- " ++ (code $ scCode comment) ++ ": " ++ (scMessage comment)
replicate (scColumn comment - 1) ' ' ++
"^-- " ++ code (scCode comment) ++ ": " ++ scMessage comment
code code = "SC" ++ (show code)
@@ -129,7 +134,7 @@ forTty options files = do
-- This totally ignores the filenames. Fixme?
forJson options files = do
comments <- liftM concat $ mapM (commentsFor options) files
putStrLn $ encodeStrict $ comments
putStrLn $ encodeStrict comments
return . null $ comments
-- Mimic GCC "file:line:col: (error|warning|note): message" format
@@ -176,8 +181,8 @@ forCheckstyle options files = do
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) ++ ";"
escape = concatMap escape'
escape' c = if isOk c then [c] else "&#" ++ show (ord c) ++ ";"
isOk x = any ($x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")]
formatFile name comments = concat [
@@ -200,7 +205,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 +228,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
@@ -238,8 +250,8 @@ getExclusions options =
in
map (Prelude.read . clean) elements :: [Int]
excludeCodes codes comments =
filter (not . hasCode) comments
excludeCodes codes =
filter (not . hasCode)
where
hasCode c = scCode c `elem` codes
@@ -257,7 +269,7 @@ main = do
process Nothing = return False
process (Just (options, files)) =
let format = getOption options "format" "tty" in
let format = fromMaybe "tty" $ getOption options "format" in
case Map.lookup format formats of
Nothing -> do
printErr $ "Unknown format " ++ format
@@ -268,3 +280,22 @@ 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
when (isJust shell && 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