37 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
8 changed files with 742 additions and 258 deletions

44
README
View File

@@ -1,44 +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
On Mac OS X with homebrew (http://brew.sh/), use:
brew install cabal-install
On Mac OS X with MacPorts (http://www.macports.org/), use:
port install hs-cabal-install
Executables can be built with cabal. Tests currently still rely on a Makefile.
Install:
cabal install
which shellcheck
~/.cabal/bin/shellcheck
Happy ShellChecking!

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
module ShellCheck.Data where module ShellCheck.Data where
shellcheckVersion = "0.3.1" -- Must also be updated in ShellCheck.cabal shellcheckVersion = "0.3.2" -- Must also be updated in ShellCheck.cabal
internalVariables = [ internalVariables = [
-- Generic -- Generic

View File

@@ -46,14 +46,18 @@ tokenDelimiter = oneOf "&|;<> \t\n\r" <|> nbsp
quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars
quotable = nbsp <|> unicodeDoubleQuote <|> oneOf quotableChars quotable = nbsp <|> unicodeDoubleQuote <|> oneOf quotableChars
bracedQuotable = oneOf "}\"$`'" bracedQuotable = oneOf "}\"$`'"
doubleQuotableChars = "\"$`\x201C\x201D" doubleQuotableChars = "\"$`" ++ unicodeDoubleQuoteChars
doubleQuotable = unicodeDoubleQuote <|> oneOf doubleQuotableChars doubleQuotable = unicodeDoubleQuote <|> oneOf doubleQuotableChars
whitespace = oneOf " \t\n" <|> carriageReturn <|> nbsp whitespace = oneOf " \t\n" <|> carriageReturn <|> nbsp
linewhitespace = oneOf " \t" <|> nbsp linewhitespace = oneOf " \t" <|> nbsp
suspectCharAfterQuotes = variableChars <|> char '%'
extglobStartChars = "?*@!+" extglobStartChars = "?*@!+"
extglobStart = oneOf extglobStartChars extglobStart = oneOf extglobStartChars
unicodeDoubleQuoteChars = "\x201C\x201D\x2033\x2036"
prop_spacing = isOk spacing " \\\n # Comment" prop_spacing = isOk spacing " \\\n # Comment"
spacing = do spacing = do
x <- many (many1 linewhitespace <|> (try $ string "\\\n")) x <- many (many1 linewhitespace <|> (try $ string "\\\n"))
@@ -78,7 +82,7 @@ allspacingOrFail = do
unicodeDoubleQuote = do unicodeDoubleQuote = do
pos <- getPosition pos <- getPosition
char '\x201C' <|> char '\x201D' oneOf unicodeDoubleQuoteChars
parseProblemAt pos WarningC 1015 "This is a unicode double quote. Delete and retype it." parseProblemAt pos WarningC 1015 "This is a unicode double quote. Delete and retype it."
return '"' return '"'
@@ -313,9 +317,11 @@ readConditionContents single = do
when (endedWith "]" x) $ do when (endedWith "]" x) $ do
parseProblemAt pos ErrorC 1020 $ parseProblemAt pos ErrorC 1020 $
"You need a space before the " ++ (if single then "]" else "]]") ++ "." "You need a space before the " ++ (if single then "]" else "]]") ++ "."
fail "Missing space before ]"
when (single && endedWith ")" x) $ do when (single && endedWith ")" x) $ do
parseProblemAt pos ErrorC 1021 $ parseProblemAt pos ErrorC 1021 $
"You need a space before the \\)" "You need a space before the \\)"
fail "Missing space before )"
disregard spacing disregard spacing
return x return x
where endedWith str (T_NormalWord id s@(_:_)) = where endedWith str (T_NormalWord id s@(_:_)) =
@@ -327,6 +333,7 @@ readConditionContents single = do
id <- getNextId id <- getNextId
x <- try (string "&&" <|> string "-a") x <- try (string "&&" <|> string "-a")
softCondSpacing softCondSpacing
skipLineFeeds
return $ TC_And id typ x return $ TC_And id typ x
readCondOrOp = do readCondOrOp = do
@@ -334,6 +341,7 @@ readConditionContents single = do
id <- getNextId id <- getNextId
x <- try (string "||" <|> string "-o") x <- try (string "||" <|> string "-o")
softCondSpacing softCondSpacing
skipLineFeeds
return $ TC_Or id typ x return $ TC_Or id typ x
readCondNoaryOrBinary = do readCondNoaryOrBinary = do
@@ -412,7 +420,17 @@ readConditionContents single = do
str <- string "|" str <- string "|"
return $ T_Literal id str 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 readCondNot = do
id <- getNextId id <- getNextId
char '!' char '!'
@@ -611,6 +629,9 @@ prop_readCondition6 = isOk readCondition "[[ $c =~ ^[yY]$ ]]"
prop_readCondition7 = isOk readCondition "[[ ${line} =~ ^[[:space:]]*# ]]" prop_readCondition7 = isOk readCondition "[[ ${line} =~ ^[[:space:]]*# ]]"
prop_readCondition8 = isOk readCondition "[[ $l =~ ogg|flac ]]" prop_readCondition8 = isOk readCondition "[[ $l =~ ogg|flac ]]"
prop_readCondition9 = isOk readCondition "[ foo -a -f bar ]" 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 readCondition = called "test expression" $ do
opos <- getPosition opos <- getPosition
id <- getNextId id <- getNextId
@@ -739,17 +760,29 @@ readProcSub = called "process substitution" $ do
prop_readSingleQuoted = isOk readSingleQuoted "'foo bar'" prop_readSingleQuoted = isOk readSingleQuoted "'foo bar'"
prop_readSingleQuoted2 = isWarning readSingleQuoted "'foo bar\\'" prop_readSingleQuoted2 = isWarning readSingleQuoted "'foo bar\\'"
prop_readsingleQuoted3 = isWarning readSingleQuoted "\x2018hello\x2019" 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 readSingleQuoted = called "single quoted string" $ do
id <- getNextId id <- getNextId
startPos <- getPosition
singleQuote singleQuote
s <- readSingleQuotedPart `reluctantlyTill` singleQuote s <- readSingleQuotedPart `reluctantlyTill` singleQuote
pos <- getPosition let string = concat s
endPos <- getPosition
singleQuote <?> "end of single quoted string" singleQuote <?> "end of single quoted string"
let string = concat s optional $ do
return (T_SingleQuoted id string) `attempting` do c <- try . lookAhead $ suspectCharAfterQuotes <|> oneOf "'"
x <- lookAhead anyChar if (not (null string) && isAlpha c && isAlpha (last string))
when (isAlpha x && not (null string) && isAlpha (last string)) $ parseProblemAt pos WarningC 1011 "This apostrophe terminated the single quoted 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 readSingleQuotedLiteral = do
singleQuote singleQuote
@@ -763,13 +796,24 @@ readSingleQuotedPart =
prop_readBackTicked = isOk readBackTicked "`ls *.mp3`" prop_readBackTicked = isOk readBackTicked "`ls *.mp3`"
prop_readBackTicked2 = isOk readBackTicked "`grep \"\\\"\"`" 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 readBackTicked = called "backtick expansion" $ do
id <- getNextId id <- getNextId
pos <- getPosition startPos <- getPosition
char '`' backtick
subStart <- getPosition subStart <- getPosition
subString <- readGenericLiteral "`" subString <- readGenericLiteral "`´"
char '`' 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 positions may be off due to escapes
result <- subParse subStart readCompoundList (unEscape subString) result <- subParse subStart readCompoundList (unEscape subString)
return $ T_Backticked id result return $ T_Backticked id result
@@ -778,6 +822,12 @@ readBackTicked = called "backtick expansion" $ do
unEscape ('\\':x:rest) | x `elem` "$`\\" = x : unEscape rest unEscape ('\\':x:rest) | x `elem` "$`\\" = x : unEscape rest
unEscape ('\\':'\n':rest) = unEscape rest unEscape ('\\':'\n':rest) = unEscape rest
unEscape (c:rest) = c : 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 subParse pos parser input = do
lastPosition <- getPosition lastPosition <- getPosition
@@ -792,12 +842,31 @@ subParse pos parser input = do
prop_readDoubleQuoted = isOk readDoubleQuoted "\"Hello $FOO\"" prop_readDoubleQuoted = isOk readDoubleQuoted "\"Hello $FOO\""
prop_readDoubleQuoted2 = isOk readDoubleQuoted "\"$'\"" prop_readDoubleQuoted2 = isOk readDoubleQuoted "\"$'\""
prop_readDoubleQuoted3 = isWarning readDoubleQuoted "\x201Chello\x201D" 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 readDoubleQuoted = called "double quoted string" $ do
id <- getNextId id <- getNextId
startPos <- getPosition
doubleQuote doubleQuote
x <- many doubleQuotedPart x <- many doubleQuotedPart
endPos <- getPosition
doubleQuote <?> "end of double quoted string" 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 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 doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readBackTicked
@@ -1325,13 +1394,25 @@ transformWithSeparator i _ = id
readPipeSequence = do readPipeSequence = do
id <- getNextId id <- getNextId
list <- readCommand `sepBy1` (readPipe `thenSkip` (spacing >> readLineBreak)) (cmds, pipes) <- sepBy1WithSeparators readCommand
(readPipe `thenSkip` (spacing >> readLineBreak))
spacing 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 readPipe = do
notFollowedBy2 g_OR_IF notFollowedBy2 g_OR_IF
char '|' `thenSkip` spacing id <- getNextId
char '|'
qualifier <- string "&" <|> return ""
spacing
return $ T_Pipe id ('|':qualifier)
readCommand = (readCompoundCommand <|> readSimpleCommand) readCommand = (readCompoundCommand <|> readSimpleCommand)
@@ -1682,7 +1763,8 @@ readAssignmentWord = try $ do
if space == "" && space2 /= "" if space == "" && space2 /= ""
then do then do
when (variable /= "IFS") $ 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 value <- readEmptyLiteral
return $ T_Assignment id op variable index value return $ T_Assignment id op variable index value
else do else do
@@ -1729,16 +1811,24 @@ redirToken c t = try $ do
notFollowedBy2 $ char '(' notFollowedBy2 $ char '('
return $ t id return $ t id
tryWordToken s t = tryParseWordToken (string s) t `thenSkip` spacing tryWordToken s t = tryParseWordToken s t `thenSkip` spacing
tryParseWordToken parser t = try $ do tryParseWordToken keyword t = try $ do
id <- getNextId id <- getNextId
parser str <- anycaseString keyword
optional (do optional (do
try . lookAhead $ char '[' try . lookAhead $ char '['
parseProblem ErrorC 1069 "You need a space before the [.") parseProblem ErrorC 1069 "You need a space before the [.")
try $ lookAhead (keywordSeparator) try $ lookAhead (keywordSeparator)
when (str /= keyword) $
parseProblem ErrorC 1081 $
"Scripts are case sensitive. Use '" ++ keyword ++ "', not '" ++ str ++ "'."
return $ t id 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_AND_IF = tryToken "&&" T_AND_IF
g_OR_IF = tryToken "||" T_OR_IF g_OR_IF = tryToken "||" T_OR_IF
g_DSEMI = tryToken ";;" T_DSEMI g_DSEMI = tryToken ";;" T_DSEMI

View File

@@ -69,7 +69,7 @@ parseArguments argv =
return $ Just (opts, files) return $ Just (opts, files)
(_, _, errors) -> do (_, _, errors) -> do
printErr $ (concat errors) ++ "\n" ++ usageInfo header options printErr $ concat errors ++ "\n" ++ usageInfo header options
exitWith syntaxFailure exitWith syntaxFailure
formats = Map.fromList [ formats = Map.fromList [
@@ -84,7 +84,7 @@ forTty options files = do
return $ and output return $ and output
where where
clear = ansi 0 clear = ansi 0
ansi n = "\x1B[" ++ (show n) ++ "m" ansi n = "\x1B[" ++ show n ++ "m"
colorForLevel "error" = 31 -- red colorForLevel "error" = 31 -- red
colorForLevel "warning" = 33 -- yellow colorForLevel "warning" = 33 -- yellow
@@ -94,7 +94,8 @@ forTty options files = do
colorForLevel "source" = 0 -- none colorForLevel "source" = 0 -- none
colorForLevel _ = 0 -- none colorForLevel _ = 0 -- none
colorComment level comment = (ansi $ colorForLevel level) ++ comment ++ clear colorComment level comment =
ansi (colorForLevel level) ++ comment ++ clear
doFile path = do doFile path = do
contents <- readContents path contents <- readContents path
@@ -112,15 +113,17 @@ forTty options files = do
then "" then ""
else fileLines !! (lineNum - 1) else fileLines !! (lineNum - 1)
putStrLn "" putStrLn ""
putStrLn $ colorFunc "message" ("In " ++ filename ++" line " ++ (show $ lineNum) ++ ":") putStrLn $ colorFunc "message"
("In " ++ filename ++" line " ++ show lineNum ++ ":")
putStrLn (colorFunc "source" line) putStrLn (colorFunc "source" line)
mapM (\c -> putStrLn (colorFunc (scSeverity c) $ cuteIndent c)) x mapM_ (\c -> putStrLn (colorFunc (scSeverity c) $ cuteIndent c)) x
putStrLn "" putStrLn ""
) groups ) groups
return $ null comments return $ null comments
cuteIndent comment = 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) code code = "SC" ++ (show code)
@@ -131,7 +134,7 @@ forTty options files = do
-- This totally ignores the filenames. Fixme? -- This totally ignores the filenames. Fixme?
forJson options files = do forJson options files = do
comments <- liftM concat $ mapM (commentsFor options) files comments <- liftM concat $ mapM (commentsFor options) files
putStrLn $ encodeStrict $ comments putStrLn $ encodeStrict comments
return . null $ comments return . null $ comments
-- Mimic GCC "file:line:col: (error|warning|note): message" format -- Mimic GCC "file:line:col: (error|warning|note): message" format
@@ -178,8 +181,8 @@ forCheckstyle options files = do
severity "warning" = "warning" severity "warning" = "warning"
severity _ = "info" severity _ = "info"
attr s v = concat [ s, "='", escape v, "' " ] attr s v = concat [ s, "='", escape v, "' " ]
escape msg = concatMap escape' msg escape = concatMap escape'
escape' c = if isOk c then [c] else "&#" ++ (show $ ord c) ++ ";" escape' c = if isOk c then [c] else "&#" ++ show (ord c) ++ ";"
isOk x = any ($x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")] isOk x = any ($x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")]
formatFile name comments = concat [ formatFile name comments = concat [
@@ -226,7 +229,7 @@ makeNonVirtual comments contents =
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 [] _ = Nothing getOption [] _ = Nothing
getOption ((Flag var val):_) name | name == var = return val getOption (Flag var val:_) name | name == var = return val
getOption (_:rest) flag = getOption rest flag getOption (_:rest) flag = getOption rest flag
getOptions options name = getOptions options name =
@@ -247,8 +250,8 @@ getExclusions options =
in in
map (Prelude.read . clean) elements :: [Int] map (Prelude.read . clean) elements :: [Int]
excludeCodes codes comments = excludeCodes codes =
filter (not . hasCode) comments filter (not . hasCode)
where where
hasCode c = scCode c `elem` codes hasCode c = scCode c `elem` codes
@@ -265,7 +268,7 @@ main = do
exitWith code exitWith code
process Nothing = return False process Nothing = return False
process (Just (options, files)) = do process (Just (options, files)) =
let format = fromMaybe "tty" $ getOption options "format" 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
@@ -281,11 +284,9 @@ verifyOptions opts files = do
when (isJust $ getOption opts "version") printVersionAndExit when (isJust $ getOption opts "version") printVersionAndExit
let shell = getOption opts "shell" in let shell = getOption opts "shell" in
if isNothing shell when (isJust shell && isNothing (shell >>= shellForExecutable)) $ do
then return () printErr $ "Unknown shell: " ++ (fromJust shell)
else when (isNothing $ shell >>= shellForExecutable) $ do exitWith supportFailure
printErr $ "Unknown shell: " ++ (fromJust shell)
exitWith supportFailure
when (null files) $ do when (null files) $ do
printErr "No files specified.\n" printErr "No files specified.\n"