mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-10-01 01:09:18 +08:00
Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
39423ddf81 | ||
|
875c2d2aad | ||
|
64cc7c691a | ||
|
b9784cbcc0 | ||
|
1a3f6aadaf | ||
|
35756c2cd6 | ||
|
0fd351404f | ||
|
4caa7e7900 | ||
|
c11c0196d5 | ||
|
b035331d4a | ||
|
d13253973b | ||
|
d9c622ae33 | ||
|
aac7d76047 | ||
|
fc421adb45 | ||
|
e0d3c6923a | ||
|
9772ba9de4 | ||
|
3a944de606 | ||
|
3dd592a02a | ||
|
61531cbb10 | ||
|
d53087f056 | ||
|
39756b420e | ||
|
52d4efc951 | ||
|
5dac723593 | ||
|
2364fd58b6 | ||
|
cde364c97b | ||
|
98b790f87a | ||
|
726a4e5848 | ||
|
0a9ed917e7 | ||
|
b18ee3fdef | ||
|
3fcc6c44d8 |
20
.gitignore
vendored
20
.gitignore
vendored
@@ -1,7 +1,15 @@
|
||||
*.hi
|
||||
*.o
|
||||
.tests
|
||||
jsoncheck
|
||||
shellcheck
|
||||
shellcheck.1
|
||||
# Created by http://www.gitignore.io
|
||||
|
||||
### Haskell ###
|
||||
dist
|
||||
cabal-dev
|
||||
*.o
|
||||
*.hi
|
||||
*.chi
|
||||
*.chs.h
|
||||
.virtualenv
|
||||
.hsenv
|
||||
.cabal-sandbox/
|
||||
cabal.sandbox.config
|
||||
cabal.config
|
||||
|
||||
|
30
Makefile
30
Makefile
@@ -1,30 +0,0 @@
|
||||
# TODO: Phase out Makefile in favor of Cabal
|
||||
|
||||
GHCFLAGS=-O9
|
||||
GHCFLAGS_STATIC=$(GHCFLAGS) -optl-static -optl-pthread
|
||||
|
||||
all: shellcheck .tests shellcheck.1
|
||||
: Done
|
||||
|
||||
shellcheck: regardless
|
||||
: Conditionally compiling shellcheck
|
||||
ghc $(GHCFLAGS) --make shellcheck
|
||||
|
||||
.tests: *.hs */*.hs
|
||||
: Running unit tests
|
||||
./test/runQuack && touch .tests
|
||||
|
||||
shellcheck.1: shellcheck.1.md
|
||||
: Formatting man page
|
||||
pandoc -s -t man $< -o $@
|
||||
|
||||
clean:
|
||||
rm -f .tests shellcheck shellcheck.1
|
||||
rm -f *.hi *.o ShellCheck/*.hi ShellCheck/*.o
|
||||
rm -rf dist
|
||||
|
||||
shellcheck-static: regardless
|
||||
: Conditionally compiling a statically linked shellcheck-static
|
||||
ghc $(GHCFLAGS_STATIC) --make shellcheck -o shellcheck-static
|
||||
|
||||
regardless:
|
49
README.md
49
README.md
@@ -16,12 +16,32 @@ The goals of ShellCheck are:
|
||||
- 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.
|
||||
ShellCheck is written in Haskell, and requires at least 1 GB of RAM to compile.
|
||||
|
||||
## Installing
|
||||
|
||||
On systems with Cabal:
|
||||
|
||||
cabal update
|
||||
cabal install shellcheck
|
||||
|
||||
On Arch Linux with community packages enabled:
|
||||
|
||||
pacman -S shellcheck
|
||||
|
||||
On OS X with homebrew:
|
||||
|
||||
brew install shellcheck
|
||||
|
||||
ShellCheck is also available as an online service:
|
||||
|
||||
http://www.shellcheck.net
|
||||
|
||||
## Building with Cabal
|
||||
|
||||
Make sure cabal is installed. On Debian based distros:
|
||||
This sections describes how to build ShellCheck from a source directory.
|
||||
|
||||
First, make sure cabal is installed. On Debian based distros:
|
||||
|
||||
apt-get install cabal-install
|
||||
|
||||
@@ -57,27 +77,12 @@ Verify that your PATH is set up correctly:
|
||||
$ which shellcheck
|
||||
~/.cabal/bin/shellcheck
|
||||
|
||||
## Building with Make
|
||||
## Running tests
|
||||
|
||||
ShellCheck is written in Haskell, and requires GHC, Parsec3, JSON and
|
||||
Text.Regex. To run the unit tests, it also requires QuickCheck2.
|
||||
To run the unit test suite:
|
||||
|
||||
On Fedora, these can be installed with:
|
||||
|
||||
yum install ghc ghc-parsec-devel ghc-QuickCheck-devel \
|
||||
ghc-json-devel ghc-regex-compat-devel pandoc
|
||||
|
||||
On Ubuntu and similar, use:
|
||||
|
||||
apt-get install ghc libghc-parsec3-dev libghc-json-dev \
|
||||
libghc-regex-compat-dev libghc-quickcheck2-dev pandoc
|
||||
|
||||
To build and run the tests, cd to the shellcheck source directory and:
|
||||
|
||||
$ make
|
||||
|
||||
If you want to distribute the binary and/or run it on other distros, you
|
||||
can `make shellcheck-static` to build a statically linked executable without
|
||||
library dependencies.
|
||||
cabal configure --enable-tests
|
||||
cabal build
|
||||
cabal test
|
||||
|
||||
Happy ShellChecking!
|
||||
|
@@ -1,6 +1,5 @@
|
||||
Name: ShellCheck
|
||||
-- Must also be updated in ShellCheck/Data.hs :
|
||||
Version: 0.3.3
|
||||
Version: 0.3.4
|
||||
Synopsis: Shell script analysis tool
|
||||
License: OtherLicense
|
||||
License-file: LICENSE
|
||||
@@ -23,6 +22,13 @@ Description:
|
||||
* To point out subtle caveats, corner cases and pitfalls, that may cause an
|
||||
advanced user's otherwise working script to fail under future circumstances.
|
||||
|
||||
Extra-Source-Files:
|
||||
-- documentation
|
||||
README.md
|
||||
shellcheck.1.md
|
||||
-- tests
|
||||
test/shellcheck.hs
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: git://github.com/koalaman/shellcheck.git
|
||||
@@ -35,13 +41,16 @@ library
|
||||
json,
|
||||
mtl,
|
||||
parsec,
|
||||
regex-compat
|
||||
regex-compat,
|
||||
QuickCheck >= 2.2
|
||||
exposed-modules:
|
||||
ShellCheck.Analytics
|
||||
ShellCheck.AST
|
||||
ShellCheck.Data
|
||||
ShellCheck.Parser
|
||||
ShellCheck.Simple
|
||||
other-modules:
|
||||
Paths_ShellCheck
|
||||
|
||||
executable shellcheck
|
||||
build-depends:
|
||||
@@ -52,5 +61,21 @@ executable shellcheck
|
||||
json,
|
||||
mtl,
|
||||
parsec,
|
||||
regex-compat
|
||||
regex-compat,
|
||||
QuickCheck >= 2.2
|
||||
main-is: shellcheck.hs
|
||||
|
||||
test-suite test-shellcheck
|
||||
type: exitcode-stdio-1.0
|
||||
build-depends:
|
||||
ShellCheck,
|
||||
base >= 4 && < 5,
|
||||
containers,
|
||||
directory,
|
||||
json,
|
||||
mtl,
|
||||
parsec,
|
||||
regex-compat,
|
||||
QuickCheck >= 2.2
|
||||
main-is: test/shellcheck.hs
|
||||
|
||||
|
@@ -29,16 +29,14 @@ data AssignmentMode = Assign | Append deriving (Show, Eq)
|
||||
data FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
|
||||
data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
|
||||
data ForInType = NormalForIn | ShortForIn deriving (Show, Eq)
|
||||
data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
|
||||
|
||||
data Token =
|
||||
TA_Base Id String Token
|
||||
| TA_Binary Id String Token Token
|
||||
| TA_Expansion Id Token
|
||||
| TA_Literal Id String
|
||||
TA_Binary Id String Token Token
|
||||
| TA_Expansion Id [Token]
|
||||
| TA_Sequence Id [Token]
|
||||
| TA_Trinary Id Token Token Token
|
||||
| TA_Unary Id String Token
|
||||
| TA_Variable Id String
|
||||
| TC_And Id ConditionType String Token Token
|
||||
| TC_Binary Id ConditionType String Token Token
|
||||
| TC_Group Id ConditionType Token
|
||||
@@ -49,6 +47,8 @@ data Token =
|
||||
| T_AndIf Id (Token) (Token)
|
||||
| T_Arithmetic Id Token
|
||||
| T_Array Id [Token]
|
||||
| T_IndexedElement Id Token Token
|
||||
| T_ Id [Token]
|
||||
| T_Assignment Id AssignmentMode String (Maybe Token) Token
|
||||
| T_Backgrounded Id Token
|
||||
| T_Backticked Id [Token]
|
||||
@@ -58,7 +58,7 @@ data Token =
|
||||
| T_BraceGroup Id [Token]
|
||||
| T_CLOBBER Id
|
||||
| T_Case Id
|
||||
| T_CaseExpression Id Token [([Token],[Token])]
|
||||
| T_CaseExpression Id Token [(CaseType, [Token], [Token])]
|
||||
| T_Condition Id ConditionType Token
|
||||
| T_DGREAT Id
|
||||
| T_DLESS Id
|
||||
@@ -179,6 +179,7 @@ analyze f g i =
|
||||
b <- round value
|
||||
return $ T_Assignment id mode var a b
|
||||
delve (T_Array id t) = dl t $ T_Array id
|
||||
delve (T_IndexedElement id t1 t2) = d2 t1 t2 $ T_IndexedElement id
|
||||
delve (T_Redirecting id redirs cmd) = do
|
||||
a <- roundAll redirs
|
||||
b <- round cmd
|
||||
@@ -207,10 +208,10 @@ analyze f g i =
|
||||
delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v
|
||||
delve (T_CaseExpression id word cases) = do
|
||||
newWord <- round word
|
||||
newCases <- mapM (\(c, t) -> do
|
||||
newCases <- mapM (\(o, c, t) -> do
|
||||
x <- mapM round c
|
||||
y <- mapM round t
|
||||
return (x,y)
|
||||
return (o, x,y)
|
||||
) cases
|
||||
return $ T_CaseExpression id newWord newCases
|
||||
|
||||
@@ -243,8 +244,7 @@ analyze f g i =
|
||||
b <- round t2
|
||||
c <- round t3
|
||||
return $ TA_Trinary id a b c
|
||||
delve (TA_Expansion id t) = d1 t $ TA_Expansion id
|
||||
delve (TA_Base id b t) = d1 t $ TA_Base id b
|
||||
delve (TA_Expansion id t) = dl t $ TA_Expansion id
|
||||
delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns
|
||||
delve t = return t
|
||||
|
||||
@@ -297,6 +297,7 @@ getId t = case t of
|
||||
T_FdRedirect id _ _ -> id
|
||||
T_Assignment id _ _ _ _ -> id
|
||||
T_Array id _ -> id
|
||||
T_IndexedElement id _ _ -> id
|
||||
T_Redirecting id _ _ -> id
|
||||
T_SimpleCommand id _ _ -> id
|
||||
T_Pipeline id _ _ -> id
|
||||
@@ -327,11 +328,8 @@ getId t = case t of
|
||||
TA_Binary id _ _ _ -> id
|
||||
TA_Unary id _ _ -> id
|
||||
TA_Sequence id _ -> id
|
||||
TA_Variable id _ -> id
|
||||
TA_Trinary id _ _ _ -> id
|
||||
TA_Expansion id _ -> id
|
||||
TA_Literal id _ -> id
|
||||
TA_Base id _ _ -> id
|
||||
T_ProcSub id _ _ -> id
|
||||
T_Glob id _ -> id
|
||||
T_ForArithmetic id _ _ _ _ -> id
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,9 @@
|
||||
module ShellCheck.Data where
|
||||
|
||||
shellcheckVersion = "0.3.3" -- Must also be updated in ShellCheck.cabal
|
||||
import Data.Version (showVersion)
|
||||
import Paths_ShellCheck (version)
|
||||
|
||||
shellcheckVersion = showVersion version
|
||||
|
||||
internalVariables = [
|
||||
-- Generic
|
||||
|
@@ -15,15 +15,15 @@
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE NoMonomorphismRestriction #-}
|
||||
|
||||
module ShellCheck.Parser (Note(..), Severity(..), parseShell, ParseResult(..), ParseNote(..), sortNotes, noteToParseNote) where
|
||||
{-# LANGUAGE NoMonomorphismRestriction, TemplateHaskell #-}
|
||||
module ShellCheck.Parser (Note(..), Severity(..), parseShell, ParseResult(..), ParseNote(..), sortNotes, noteToParseNote, runTests) where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.Data
|
||||
import Text.Parsec
|
||||
import Debug.Trace
|
||||
import Control.Monad
|
||||
import Control.Arrow (first)
|
||||
import Data.Char
|
||||
import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub)
|
||||
import qualified Data.Map as Map
|
||||
@@ -33,9 +33,10 @@ import Prelude hiding (readList)
|
||||
import System.IO
|
||||
import Text.Parsec.Error
|
||||
import GHC.Exts (sortWith)
|
||||
import Test.QuickCheck.All (quickCheckAll)
|
||||
|
||||
backslash = char '\\'
|
||||
linefeed = (optional carriageReturn) >> char '\n'
|
||||
linefeed = optional carriageReturn >> char '\n'
|
||||
singleQuote = char '\'' <|> unicodeSingleQuote
|
||||
doubleQuote = char '"' <|> unicodeDoubleQuote
|
||||
variableStart = upper <|> lower <|> oneOf "_"
|
||||
@@ -60,7 +61,7 @@ unicodeDoubleQuoteChars = "\x201C\x201D\x2033\x2036"
|
||||
|
||||
prop_spacing = isOk spacing " \\\n # Comment"
|
||||
spacing = do
|
||||
x <- many (many1 linewhitespace <|> (try $ string "\\\n"))
|
||||
x <- many (many1 linewhitespace <|> try (string "\\\n"))
|
||||
optional readComment
|
||||
return $ concat x
|
||||
|
||||
@@ -110,7 +111,7 @@ nbsp = do
|
||||
data Note = Note Id Severity Code String deriving (Show, Eq)
|
||||
data ParseNote = ParseNote SourcePos Severity Code String deriving (Show, Eq)
|
||||
data Severity = ErrorC | WarningC | InfoC | StyleC deriving (Show, Eq, Ord)
|
||||
data Context = ContextName SourcePos String | ContextAnnotation [Annotation]
|
||||
data Context = ContextName SourcePos String | ContextAnnotation [Annotation] deriving (Show)
|
||||
type Code = Integer
|
||||
|
||||
codeForParseNote (ParseNote _ _ code _) = code
|
||||
@@ -131,7 +132,7 @@ getNextIdAt sourcepos = do
|
||||
let newMap = Map.insert newId sourcepos map
|
||||
putState (newId, newMap, notes)
|
||||
return newId
|
||||
where incId (Id n) = (Id $ n+1)
|
||||
where incId (Id n) = Id $ n+1
|
||||
|
||||
getNextId = do
|
||||
pos <- getPosition
|
||||
@@ -151,7 +152,7 @@ getParseNotes = do
|
||||
|
||||
addParseNote n = do
|
||||
irrelevant <- shouldIgnoreCode (codeForParseNote n)
|
||||
when (not irrelevant) $ do
|
||||
unless irrelevant $ do
|
||||
(a, b, notes) <- getState
|
||||
putState (a, b, n:notes)
|
||||
|
||||
@@ -169,7 +170,7 @@ parseProblem level code msg = do
|
||||
pos <- getPosition
|
||||
parseProblemAt pos level code msg
|
||||
|
||||
setCurrentContexts c = do
|
||||
setCurrentContexts c =
|
||||
Ms.modify (\(list, _) -> (list, c))
|
||||
|
||||
getCurrentContexts = do
|
||||
@@ -192,8 +193,8 @@ pushContext c = do
|
||||
|
||||
parseProblemAt pos level code msg = do
|
||||
irrelevant <- shouldIgnoreCode code
|
||||
when (not irrelevant) $
|
||||
Ms.modify (\(list, current) -> ((ParseNote pos level code msg):list, current))
|
||||
unless irrelevant $
|
||||
Ms.modify (first ((:) (ParseNote pos level code msg)))
|
||||
|
||||
-- Store non-parse problems inside
|
||||
|
||||
@@ -209,15 +210,15 @@ thenSkip main follow = do
|
||||
optional follow
|
||||
return r
|
||||
|
||||
unexpecting s p = try $ do
|
||||
unexpecting s p = try $
|
||||
(try p >> unexpected s) <|> return ()
|
||||
|
||||
notFollowedBy2 = unexpecting "keyword/token"
|
||||
|
||||
disregard x = x >> return ()
|
||||
disregard = void
|
||||
|
||||
reluctantlyTill p end = do
|
||||
(lookAhead ((disregard $ try end) <|> eof) >> return []) <|> do
|
||||
reluctantlyTill p end =
|
||||
(lookAhead (disregard (try end) <|> eof) >> return []) <|> do
|
||||
x <- p
|
||||
more <- reluctantlyTill p end
|
||||
return $ x:more
|
||||
@@ -229,15 +230,15 @@ reluctantlyTill1 p end = do
|
||||
more <- reluctantlyTill p end
|
||||
return $ x:more
|
||||
|
||||
attempting rest branch = do
|
||||
((try branch) >> rest) <|> rest
|
||||
attempting rest branch =
|
||||
(try branch >> rest) <|> rest
|
||||
|
||||
orFail parser stuff = do
|
||||
orFail parser stuff =
|
||||
try (disregard parser) <|> (disregard stuff >> fail "nope")
|
||||
|
||||
wasIncluded p = option False (p >> return True)
|
||||
|
||||
acceptButWarn parser level code note = do
|
||||
acceptButWarn parser level code note =
|
||||
optional $ try (do
|
||||
pos <- getPosition
|
||||
parser
|
||||
@@ -252,17 +253,17 @@ withContext entry p = do
|
||||
return v
|
||||
<|> do -- p failed without consuming input, abort context
|
||||
popContext
|
||||
fail $ ""
|
||||
fail ""
|
||||
|
||||
called s p = do
|
||||
pos <- getPosition
|
||||
withContext (ContextName pos s) p
|
||||
|
||||
withAnnotations anns p =
|
||||
withContext (ContextAnnotation anns) p
|
||||
withAnnotations anns =
|
||||
withContext (ContextAnnotation anns)
|
||||
|
||||
readConditionContents single = do
|
||||
readCondContents `attempting` (lookAhead $ do
|
||||
readConditionContents single =
|
||||
readCondContents `attempting` lookAhead (do
|
||||
pos <- getPosition
|
||||
s <- many1 letter
|
||||
when (s `elem` commonCommands) $
|
||||
@@ -273,7 +274,7 @@ readConditionContents single = do
|
||||
readCondBinaryOp = try $ do
|
||||
optional guardArithmetic
|
||||
id <- getNextId
|
||||
op <- (choice $ (map tryOp ["==", "!=", "<=", ">=", "=~", ">", "<", "=", "\\<=", "\\>=", "\\<", "\\>"])) <|> otherOp
|
||||
op <- choice (map tryOp ["==", "!=", "<=", ">=", "=~", ">", "<", "=", "\\<=", "\\>=", "\\<", "\\>"]) <|> otherOp
|
||||
hardCondSpacing
|
||||
return op
|
||||
where
|
||||
@@ -301,7 +302,7 @@ readConditionContents single = do
|
||||
arg <- readCondWord
|
||||
return $ op arg)
|
||||
<|> (do
|
||||
parseProblemAt pos ErrorC 1019 $ "Expected this to be an argument to the unary condition."
|
||||
parseProblemAt pos ErrorC 1019 "Expected this to be an argument to the unary condition."
|
||||
fail "oops")
|
||||
|
||||
readCondUnaryOp = try $ do
|
||||
@@ -316,7 +317,7 @@ readConditionContents single = do
|
||||
return ('-':s)
|
||||
|
||||
readCondWord = do
|
||||
notFollowedBy2 (try (spacing >> (string "]")))
|
||||
notFollowedBy2 (try (spacing >> string "]"))
|
||||
x <- readNormalWord
|
||||
pos <- getPosition
|
||||
when (endedWith "]" x) $ do
|
||||
@@ -324,13 +325,13 @@ readConditionContents single = do
|
||||
"You need a space before the " ++ (if single then "]" else "]]") ++ "."
|
||||
fail "Missing space before ]"
|
||||
when (single && endedWith ")" x) $ do
|
||||
parseProblemAt pos ErrorC 1021 $
|
||||
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@(_:_)) =
|
||||
case (last s) of T_Literal id s -> str `isSuffixOf` s
|
||||
case last s of T_Literal id s -> str `isSuffixOf` s
|
||||
_ -> False
|
||||
endedWith _ _ = False
|
||||
|
||||
@@ -364,9 +365,9 @@ readConditionContents single = do
|
||||
op <- readCondBinaryOp
|
||||
y <- if isRegex
|
||||
then readRegex
|
||||
else readCondWord <|> ( (parseProblemAt pos ErrorC 1027 $ "Expected another argument for this operator.") >> mzero)
|
||||
else readCondWord <|> (parseProblemAt pos ErrorC 1027 "Expected another argument for this operator." >> mzero)
|
||||
return (x `op` y)
|
||||
) <|> (return $ TC_Noary id typ x)
|
||||
) <|> return (TC_Noary id typ x)
|
||||
|
||||
readCondGroup = do
|
||||
id <- getNextId
|
||||
@@ -389,7 +390,7 @@ readConditionContents single = do
|
||||
xor x y = x && not y || not x && y
|
||||
|
||||
-- Currently a bit of a hack since parsing rules are obscure
|
||||
regexOperatorAhead = (lookAhead $ do
|
||||
regexOperatorAhead = lookAhead (do
|
||||
try (string "=~") <|> try (string "~=")
|
||||
return True)
|
||||
<|> return False
|
||||
@@ -467,6 +468,8 @@ 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"
|
||||
prop_a10= isOk readArithmeticContents "$foo$bar"
|
||||
prop_a11= isOk readArithmeticContents "i<(0+(1+1))"
|
||||
readArithmeticContents =
|
||||
readSequence
|
||||
where
|
||||
@@ -484,25 +487,34 @@ readArithmeticContents =
|
||||
spacing
|
||||
return $ token id op
|
||||
|
||||
readVar = do
|
||||
id <- getNextId
|
||||
x <- readVariableName
|
||||
y <- readArrayIndex <|> return ""
|
||||
optional spacing
|
||||
return $ TA_Variable id (x ++ y)
|
||||
|
||||
-- Doesn't help with foo[foo]
|
||||
readArrayIndex = do
|
||||
char '['
|
||||
x <- many1 $ noneOf "]"
|
||||
char ']'
|
||||
return $ "[" ++ x ++ "]"
|
||||
id <- getNextId
|
||||
start <- literal "["
|
||||
middle <- readArithmeticContents
|
||||
end <- literal "]"
|
||||
return $ T_NormalWord id [start, middle, end]
|
||||
|
||||
literal s = do
|
||||
id <- getNextId
|
||||
string s
|
||||
return $ T_Literal id s
|
||||
|
||||
readArithmeticLiteral =
|
||||
readArrayIndex <|> literal "#"
|
||||
|
||||
readExpansion = do
|
||||
id <- getNextId
|
||||
x <- readNormalDollar <|> readBackTicked
|
||||
pieces <- many1 $ choice [
|
||||
readArithmeticLiteral,
|
||||
readSingleQuoted,
|
||||
readDoubleQuoted,
|
||||
readNormalDollar,
|
||||
readBraced,
|
||||
readBackTicked,
|
||||
readNormalLiteral "+-*/=%^,]"
|
||||
]
|
||||
spacing
|
||||
return $ TA_Expansion id x
|
||||
return $ TA_Expansion id pieces
|
||||
|
||||
readGroup = do
|
||||
char '('
|
||||
@@ -511,40 +523,7 @@ readArithmeticContents =
|
||||
spacing
|
||||
return s
|
||||
|
||||
readNumber = do
|
||||
id <- getNextId
|
||||
num <- many1 $ oneOf "0123456789."
|
||||
return $ TA_Literal id (num)
|
||||
|
||||
readBased = getArbitrary <|> getHex <|> getOct
|
||||
where
|
||||
getThing prefix litchars = try $ do
|
||||
id <- getNextId
|
||||
x <- prefix
|
||||
t <- readExpansion <|> (do
|
||||
i <- getNextId
|
||||
stuff <- many1 litchars
|
||||
return $ TA_Literal i stuff)
|
||||
return $ TA_Base id x t
|
||||
|
||||
getArbitrary = getThing arbitrary variableChars
|
||||
getHex = getThing hex hexDigit
|
||||
getOct = getThing oct digit
|
||||
|
||||
arbitrary = try $ do
|
||||
b <- many1 digit
|
||||
s <- char '#'
|
||||
return (b ++ [s])
|
||||
hex = try $ do
|
||||
z <- char '0'
|
||||
x <- oneOf "xX"
|
||||
return (z:x:[])
|
||||
oct = string "0"
|
||||
|
||||
readArithTerm = readBased <|> readArithTermUnit
|
||||
readArithTermUnit = readGroup <|> readExpansion <|> readQuoted <|> readNumber <|> readVar
|
||||
|
||||
readQuoted = readDoubleQuoted <|> readSingleQuoted
|
||||
readArithTerm = readGroup <|> readExpansion
|
||||
|
||||
readSequence = do
|
||||
spacing
|
||||
@@ -641,7 +620,7 @@ prop_readCondition13= isOk readCondition "[[ foo =~ ^fo{1,3}$ ]]"
|
||||
readCondition = called "test expression" $ do
|
||||
opos <- getPosition
|
||||
id <- getNextId
|
||||
open <- (try $ string "[[") <|> (string "[")
|
||||
open <- try (string "[[") <|> string "["
|
||||
let single = open == "["
|
||||
condSpacingMsg False $ if single
|
||||
then "You need spaces after the opening [ and before the closing ]."
|
||||
@@ -649,7 +628,7 @@ readCondition = called "test expression" $ do
|
||||
condition <- readConditionContents single
|
||||
|
||||
cpos <- getPosition
|
||||
close <- (try $ string "]]") <|> (string "]")
|
||||
close <- try (string "]]") <|> string "]"
|
||||
when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Did you mean ]] ?"
|
||||
when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?"
|
||||
spacing
|
||||
@@ -674,12 +653,12 @@ prop_readAnnotation2 = isOk readAnnotation "# shellcheck disable=SC1234 disable=
|
||||
readAnnotation = called "shellcheck annotation" $ do
|
||||
try readAnnotationPrefix
|
||||
many1 linewhitespace
|
||||
values <- many1 (readDisable)
|
||||
values <- many1 readDisable
|
||||
linefeed
|
||||
many linewhitespace
|
||||
return $ concat values
|
||||
where
|
||||
readDisable = forKey "disable" $ do
|
||||
readDisable = forKey "disable" $
|
||||
readCode `sepBy` char ','
|
||||
where
|
||||
readCode = do
|
||||
@@ -718,12 +697,12 @@ readNormalishWord end = do
|
||||
return $ T_NormalWord id x
|
||||
|
||||
checkPossibleTermination pos [T_Literal _ x] =
|
||||
if x `elem` ["do", "done", "then", "fi", "esac"]
|
||||
then parseProblemAt pos WarningC 1010 $ "Use semicolon or linefeed before '" ++ x ++ "' (or quote to make it literal)."
|
||||
else return ()
|
||||
when (x `elem` ["do", "done", "then", "fi", "esac"]) $
|
||||
parseProblemAt pos WarningC 1010 $ "Use semicolon or linefeed before '" ++ x ++ "' (or quote to make it literal)."
|
||||
checkPossibleTermination _ _ = return ()
|
||||
|
||||
readNormalWordPart end = do
|
||||
notFollowedBy2 $ oneOf end
|
||||
checkForParenthesis
|
||||
choice [
|
||||
readSingleQuoted,
|
||||
@@ -737,7 +716,7 @@ readNormalWordPart end = do
|
||||
readLiteralCurlyBraces
|
||||
]
|
||||
where
|
||||
checkForParenthesis = do
|
||||
checkForParenthesis =
|
||||
return () `attempting` do
|
||||
pos <- getPosition
|
||||
lookAhead $ char '('
|
||||
@@ -806,9 +785,9 @@ readSingleQuoted = called "single quoted string" $ do
|
||||
|
||||
optional $ do
|
||||
c <- try . lookAhead $ suspectCharAfterQuotes <|> oneOf "'"
|
||||
if (not (null string) && isAlpha c && isAlpha (last string))
|
||||
if not (null string) && isAlpha c && isAlpha (last string)
|
||||
then
|
||||
parseProblemAt endPos WarningC 1011 $
|
||||
parseProblemAt endPos WarningC 1011
|
||||
"This apostrophe terminated the single quoted string!"
|
||||
else
|
||||
when ('\n' `elem` string && not ("\n" `isPrefixOf` string)) $
|
||||
@@ -824,7 +803,7 @@ readSingleQuotedLiteral = do
|
||||
|
||||
readSingleQuotedPart =
|
||||
readSingleEscaped
|
||||
<|> (many1 $ noneOf "'\\\x2018\x2019")
|
||||
<|> many1 (noneOf "'\\\x2018\x2019")
|
||||
|
||||
prop_readBackTicked = isOk readBackTicked "`ls *.mp3`"
|
||||
prop_readBackTicked2 = isOk readBackTicked "`grep \"\\\"\"`"
|
||||
@@ -843,7 +822,7 @@ readBackTicked = called "backtick expansion" $ do
|
||||
|
||||
optional $ do
|
||||
c <- try . lookAhead $ suspectCharAfterQuotes
|
||||
when ('\n' `elem` subString && not ("\n" `isPrefixOf` subString)) $ do
|
||||
when ('\n' `elem` subString && not ("\n" `isPrefixOf` subString)) $
|
||||
suggestForgotClosingQuote startPos endPos "backtick expansion"
|
||||
|
||||
-- Result positions may be off due to escapes
|
||||
@@ -858,7 +837,7 @@ readBackTicked = called "backtick expansion" $ do
|
||||
disregard (char '`') <|> do
|
||||
pos <- getPosition
|
||||
char '´'
|
||||
parseProblemAt pos ErrorC 1077 $
|
||||
parseProblemAt pos ErrorC 1077
|
||||
"For command expansion, the tick should slant left (` vs ´)."
|
||||
|
||||
subParse pos parser input = do
|
||||
@@ -889,7 +868,7 @@ readDoubleQuoted = called "double quoted string" $ do
|
||||
suggestForgotClosingQuote startPos endPos "double quoted string"
|
||||
return $ T_DoubleQuoted id x
|
||||
where
|
||||
startsWithLineFeed ((T_Literal _ ('\n':_)):_) = True
|
||||
startsWithLineFeed (T_Literal _ ('\n':_):_) = True
|
||||
startsWithLineFeed _ = False
|
||||
hasLineFeed (T_Literal _ str) | '\n' `elem` str = True
|
||||
hasLineFeed _ = False
|
||||
@@ -897,7 +876,7 @@ readDoubleQuoted = called "double quoted string" $ do
|
||||
suggestForgotClosingQuote startPos endPos name = do
|
||||
parseProblemAt startPos WarningC 1078 $
|
||||
"Did you forget to close this " ++ name ++ "?"
|
||||
parseProblemAt endPos InfoC 1079 $
|
||||
parseProblemAt endPos InfoC 1079
|
||||
"This is actually an end quote, but due to next char it looks suspect."
|
||||
|
||||
doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readBackTicked
|
||||
@@ -914,7 +893,7 @@ readDoubleLiteral = do
|
||||
return $ T_Literal id (concat s)
|
||||
|
||||
readDoubleLiteralPart = do
|
||||
x <- many1 $ (readDoubleEscaped <|> (many1 $ noneOf ('\\':doubleQuotableChars)))
|
||||
x <- many1 (readDoubleEscaped <|> many1 (noneOf ('\\':doubleQuotableChars)))
|
||||
return $ concat x
|
||||
|
||||
readNormalLiteral end = do
|
||||
@@ -937,9 +916,9 @@ readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
|
||||
readClass = try $ do
|
||||
id <- getNextId
|
||||
char '['
|
||||
s <- many1 (predefined <|> (liftM return $ letter <|> digit <|> oneOf globchars))
|
||||
s <- many1 (predefined <|> liftM return (letter <|> digit <|> oneOf globchars))
|
||||
char ']'
|
||||
return $ T_Glob id $ "[" ++ (concat s) ++ "]"
|
||||
return $ T_Glob id $ "[" ++ concat s ++ "]"
|
||||
where
|
||||
globchars = "^-_:?*.,!~@#$%=+{}/~"
|
||||
predefined = do
|
||||
@@ -953,20 +932,20 @@ readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
|
||||
c <- extglobStart <|> char '['
|
||||
return $ T_Literal id [c]
|
||||
|
||||
readNormalLiteralPart end = do
|
||||
readNormalEscaped <|> (many1 $ noneOf (end ++ quotableChars ++ extglobStartChars ++ "[{}"))
|
||||
readNormalLiteralPart end =
|
||||
readNormalEscaped <|> many1 (noneOf (end ++ quotableChars ++ extglobStartChars ++ "[{}"))
|
||||
|
||||
readNormalEscaped = called "escaped char" $ do
|
||||
pos <- getPosition
|
||||
backslash
|
||||
do
|
||||
next <- (quotable <|> oneOf "?*@!+[]{}.,")
|
||||
next <- quotable <|> oneOf "?*@!+[]{}.,"
|
||||
return $ if next == '\n' then "" else [next]
|
||||
<|>
|
||||
do
|
||||
next <- anyChar
|
||||
case escapedChar next of
|
||||
Just name -> parseNoteAt pos WarningC 1012 $ "\\" ++ [next] ++ " is just literal '" ++ [next] ++ "' here. For " ++ name ++ ", use " ++ (alternative next) ++ " instead."
|
||||
Just name -> parseNoteAt pos WarningC 1012 $ "\\" ++ [next] ++ " is just literal '" ++ [next] ++ "' here. For " ++ name ++ ", use " ++ alternative next ++ " instead."
|
||||
Nothing -> parseNoteAt pos InfoC 1001 $ "This \\" ++ [next] ++ " will be a regular '" ++ [next] ++ "' in this context."
|
||||
return [next]
|
||||
where
|
||||
@@ -991,7 +970,7 @@ readExtglob = called "extglob" $ do
|
||||
f <- extglobStart
|
||||
char '('
|
||||
return f
|
||||
contents <- readExtglobPart `sepBy` (char '|')
|
||||
contents <- readExtglobPart `sepBy` char '|'
|
||||
char ')'
|
||||
return $ T_Extglob id [c] contents
|
||||
|
||||
@@ -1003,7 +982,7 @@ readExtglobPart = do
|
||||
readExtglobGroup = do
|
||||
id <- getNextId
|
||||
char '('
|
||||
contents <- readExtglobPart `sepBy` (char '|')
|
||||
contents <- readExtglobPart `sepBy` char '|'
|
||||
char ')'
|
||||
return $ T_Extglob id "" contents
|
||||
readExtglobLiteral = do
|
||||
@@ -1030,18 +1009,18 @@ readSingleEscaped = do
|
||||
readDoubleEscaped = do
|
||||
bs <- backslash
|
||||
(linefeed >> return "")
|
||||
<|> (doubleQuotable >>= return . return)
|
||||
<|> (anyChar >>= (return . \x -> [bs, x]))
|
||||
<|> liftM return doubleQuotable
|
||||
<|> liftM (\ x -> [bs, x]) anyChar
|
||||
|
||||
readBraceEscaped = do
|
||||
bs <- backslash
|
||||
(linefeed >> return "")
|
||||
<|> (bracedQuotable >>= return . return)
|
||||
<|> (anyChar >>= (return . \x -> [bs, x]))
|
||||
<|> liftM return bracedQuotable
|
||||
<|> liftM (\ x -> [bs, x]) anyChar
|
||||
|
||||
|
||||
readGenericLiteral endChars = do
|
||||
strings <- many (readGenericEscaped <|> (many1 $ noneOf ('\\':endChars)))
|
||||
strings <- many (readGenericEscaped <|> many1 (noneOf ('\\':endChars)))
|
||||
return $ concat strings
|
||||
|
||||
readGenericLiteral1 endExp = do
|
||||
@@ -1059,12 +1038,12 @@ readBraced = try $ do
|
||||
let strip (T_Literal _ s) = return ("\"" ++ s ++ "\"")
|
||||
id <- getNextId
|
||||
char '{'
|
||||
str <- many1 ((readDoubleQuotedLiteral >>= (strip)) <|> readGenericLiteral1 (oneOf "}\"" <|> whitespace))
|
||||
str <- many1 ((readDoubleQuotedLiteral >>= strip) <|> readGenericLiteral1 (oneOf "}\"" <|> whitespace))
|
||||
char '}'
|
||||
let result = concat str
|
||||
unless (',' `elem` result || ".." `isInfixOf` result) $
|
||||
fail "Not a brace expression"
|
||||
return $ T_BraceExpansion id $ result
|
||||
return $ T_BraceExpansion id result
|
||||
|
||||
readNormalDollar = readDollarExpression <|> readDollarDoubleQuote <|> readDollarSingleQuote <|> readDollarLonely
|
||||
readDoubleQuotedDollar = readDollarExpression <|> readDollarLonely
|
||||
@@ -1129,7 +1108,7 @@ readDollarExpansion = called "command expansion" $ do
|
||||
try (string "$(")
|
||||
cmds <- readCompoundList
|
||||
char ')' <?> "end of $(..) expression"
|
||||
return $ (T_DollarExpansion id cmds)
|
||||
return $ T_DollarExpansion id cmds
|
||||
|
||||
prop_readDollarVariable = isOk readDollarVariable "$@"
|
||||
readDollarVariable = do
|
||||
@@ -1176,6 +1155,7 @@ prop_readHereDoc3 = isOk readHereDoc "<< foo\n$\"\nfoo"
|
||||
prop_readHereDoc4 = isOk readHereDoc "<< foo\n`\nfoo"
|
||||
prop_readHereDoc5 = isOk readHereDoc "<<- !foo\nbar\n!foo"
|
||||
prop_readHereDoc6 = isOk readHereDoc "<< foo\\ bar\ncow\nfoo bar"
|
||||
prop_readHereDoc7 = isOk readHereDoc "<< foo\n\\$(f ())\nfoo"
|
||||
readHereDoc = called "here document" $ do
|
||||
fid <- getNextId
|
||||
pos <- getPosition
|
||||
@@ -1189,8 +1169,8 @@ readHereDoc = called "here document" $ do
|
||||
parseProblemAt pos ErrorC 1038 message
|
||||
hid <- getNextId
|
||||
(quoted, endToken) <-
|
||||
(readDoubleQuotedLiteral >>= return . (\x -> (Quoted, stripLiteral x)))
|
||||
<|> (readSingleQuotedLiteral >>= return . (\x -> (Quoted, x)))
|
||||
liftM (\ x -> (Quoted, stripLiteral x)) readDoubleQuotedLiteral
|
||||
<|> liftM (\ x -> (Quoted, x)) readSingleQuotedLiteral
|
||||
<|> (readToken >>= (\x -> return (Unquoted, x)))
|
||||
spacing
|
||||
|
||||
@@ -1214,7 +1194,7 @@ readHereDoc = called "here document" $ do
|
||||
stripLiteral (T_Literal _ x) = x
|
||||
stripLiteral (T_SingleQuoted _ x) = x
|
||||
|
||||
readToken = do
|
||||
readToken =
|
||||
liftM concat $ many1 (escaped <|> quoted <|> normal)
|
||||
where
|
||||
quoted = liftM stripLiteral readDoubleQuotedLiteral <|> readSingleQuotedLiteral
|
||||
@@ -1226,16 +1206,16 @@ readHereDoc = called "here document" $ do
|
||||
|
||||
parseHereData Quoted startPos hereData = do
|
||||
id <- getNextIdAt startPos
|
||||
return $ [T_Literal id hereData]
|
||||
return [T_Literal id hereData]
|
||||
|
||||
parseHereData Unquoted startPos hereData = do
|
||||
parseHereData Unquoted startPos hereData =
|
||||
subParse startPos readHereData hereData
|
||||
|
||||
readHereData = many $ try readNormalDollar <|> try readBackTicked <|> readHereLiteral
|
||||
readHereData = many $ try doubleQuotedPart <|> readHereLiteral
|
||||
|
||||
readHereLiteral = do
|
||||
id <- getNextId
|
||||
chars <- many1 $ noneOf "`$"
|
||||
chars <- many1 $ noneOf "`$\\"
|
||||
return $ T_Literal id chars
|
||||
|
||||
verifyHereDoc dashed quoted spacing hereInfo = do
|
||||
@@ -1245,17 +1225,17 @@ readHereDoc = called "here document" $ do
|
||||
parseNote ErrorC 1040 "When using <<-, you can only indent with tabs."
|
||||
return ()
|
||||
|
||||
debugHereDoc pos endToken doc =
|
||||
if endToken `isInfixOf` doc
|
||||
then
|
||||
debugHereDoc pos endToken doc
|
||||
| endToken `isInfixOf` doc =
|
||||
let lookAt line = when (endToken `isInfixOf` line) $
|
||||
parseProblemAt pos ErrorC 1041 ("Close matches include '" ++ line ++ "' (!= '" ++ endToken ++ "').")
|
||||
in do
|
||||
parseProblemAt pos ErrorC 1042 ("Found '" ++ endToken ++ "' further down, but not entirely by itself.")
|
||||
mapM_ lookAt (lines doc)
|
||||
else if (map toLower endToken) `isInfixOf` (map toLower doc)
|
||||
then parseProblemAt pos ErrorC 1043 ("Found " ++ endToken ++ " further down, but with wrong casing.")
|
||||
else parseProblemAt pos ErrorC 1044 ("Couldn't find end token `" ++ endToken ++ "' in the here document.")
|
||||
| map toLower endToken `isInfixOf` map toLower doc =
|
||||
parseProblemAt pos ErrorC 1043 ("Found " ++ endToken ++ " further down, but with wrong casing.")
|
||||
| otherwise =
|
||||
parseProblemAt pos ErrorC 1044 ("Couldn't find end token `" ++ endToken ++ "' in the here document.")
|
||||
|
||||
|
||||
readFilename = readNormalWord
|
||||
@@ -1305,9 +1285,9 @@ readLineBreak = optional readNewlineList
|
||||
prop_readSeparator1 = isWarning readScript "a &; b"
|
||||
prop_readSeparator2 = isOk readScript "a & b"
|
||||
readSeparatorOp = do
|
||||
notFollowedBy2 (g_AND_IF <|> g_DSEMI)
|
||||
notFollowedBy2 (void g_AND_IF <|> void readCaseSeparator)
|
||||
notFollowedBy2 (string "&>")
|
||||
f <- (try $ do
|
||||
f <- try (do
|
||||
char '&'
|
||||
spacing
|
||||
pos <- getPosition
|
||||
@@ -1320,7 +1300,7 @@ readSeparatorOp = do
|
||||
spacing
|
||||
return f
|
||||
|
||||
readSequentialSep = (disregard $ g_Semi >> readLineBreak) <|> (disregard readNewlineList)
|
||||
readSequentialSep = disregard (g_Semi >> readLineBreak) <|> disregard readNewlineList
|
||||
readSeparator =
|
||||
do
|
||||
separator <- readSeparatorOp
|
||||
@@ -1343,9 +1323,9 @@ makeSimpleCommand id1 id2 prefix cmd suffix =
|
||||
in
|
||||
T_Redirecting id1 redirs $ T_SimpleCommand id2 assigns args
|
||||
where
|
||||
assignment (T_Assignment _ _ _ _ _) = True
|
||||
assignment (T_Assignment {}) = True
|
||||
assignment _ = False
|
||||
redirection (T_FdRedirect _ _ _) = True
|
||||
redirection (T_FdRedirect {}) = True
|
||||
redirection _ = False
|
||||
|
||||
|
||||
@@ -1365,20 +1345,20 @@ readSimpleCommand = called "simple command" $ do
|
||||
case cmd of
|
||||
Nothing -> return $ makeSimpleCommand id1 id2 prefix [] []
|
||||
Just cmd -> do
|
||||
suffix <- option [] $
|
||||
if isModifierCommand cmd
|
||||
then readModifierSuffix
|
||||
else if isTimeCommand cmd
|
||||
then readTimeSuffix
|
||||
else readCmdSuffix
|
||||
suffix <- option [] $ getParser readCmdSuffix cmd [
|
||||
(["declare", "export", "local", "readonly", "typeset"], readModifierSuffix),
|
||||
(["time"], readTimeSuffix),
|
||||
(["let"], readLetSuffix)
|
||||
]
|
||||
return $ makeSimpleCommand id1 id2 prefix [cmd] suffix
|
||||
where
|
||||
isModifierCommand (T_NormalWord _ [T_Literal _ s]) =
|
||||
s `elem` ["declare", "export", "local", "readonly", "typeset"]
|
||||
isModifierCommand _ = False
|
||||
-- Might not belong in T_SimpleCommand. Fixme?
|
||||
isTimeCommand (T_NormalWord _ [T_Literal _ "time"]) = True
|
||||
isTimeCommand _ = False
|
||||
isCommand strings (T_NormalWord _ [T_Literal _ s]) = s `elem` strings
|
||||
isCommand _ _ = False
|
||||
getParser def cmd [] = def
|
||||
getParser def cmd ((list, action):rest) =
|
||||
if isCommand list cmd
|
||||
then action
|
||||
else getParser def cmd rest
|
||||
|
||||
prop_readPipeline = isOk readPipeline "! cat /etc/issue | grep -i ubuntu"
|
||||
prop_readPipeline2 = isWarning readPipeline "!cat /etc/issue | grep -i ubuntu"
|
||||
@@ -1389,7 +1369,7 @@ readPipeline = do
|
||||
(T_Bang id) <- g_Bang
|
||||
pipe <- readPipeSequence
|
||||
return $ T_Banged id pipe
|
||||
<|> do
|
||||
<|>
|
||||
readPipeSequence
|
||||
|
||||
prop_readAndOr = isOk readAndOr "grep -i lol foo || exit 1"
|
||||
@@ -1399,7 +1379,7 @@ readAndOr = do
|
||||
aid <- getNextId
|
||||
annotations <- readAnnotations
|
||||
|
||||
andOr <- withAnnotations annotations $ do
|
||||
andOr <- withAnnotations annotations $
|
||||
chainr1 readPipeline $ do
|
||||
op <- g_AND_IF <|> g_OR_IF
|
||||
readLineBreak
|
||||
@@ -1419,11 +1399,11 @@ readTerm' current =
|
||||
do
|
||||
id <- getNextId
|
||||
sep <- readSeparator
|
||||
more <- (option (T_EOF id) readAndOr)
|
||||
more <- option (T_EOF id) readAndOr
|
||||
case more of (T_EOF _) -> return [transformWithSeparator id sep current]
|
||||
_ -> do
|
||||
list <- readTerm' more
|
||||
return $ (transformWithSeparator id sep current : list)
|
||||
return (transformWithSeparator id sep current : list)
|
||||
<|>
|
||||
return [current]
|
||||
|
||||
@@ -1453,7 +1433,7 @@ readPipe = do
|
||||
spacing
|
||||
return $ T_Pipe id ('|':qualifier)
|
||||
|
||||
readCommand = (readCompoundCommand <|> readSimpleCommand)
|
||||
readCommand = readCompoundCommand <|> readSimpleCommand
|
||||
|
||||
readCmdName = do
|
||||
f <- readNormalWord
|
||||
@@ -1512,7 +1492,7 @@ readIfPart = do
|
||||
readElifPart = called "elif clause" $ do
|
||||
pos <- getPosition
|
||||
correctElif <- elif
|
||||
when (not correctElif) $
|
||||
unless correctElif $
|
||||
parseProblemAt pos ErrorC 1075 "Use 'elif' instead of 'else if'."
|
||||
allspacing
|
||||
condition <- readTerm
|
||||
@@ -1524,7 +1504,7 @@ readElifPart = called "elif clause" $ do
|
||||
return (condition, action)
|
||||
where
|
||||
elif = (g_Elif >> return True) <|>
|
||||
(try $ g_Else >> g_If >> return False)
|
||||
try (g_Else >> g_If >> return False)
|
||||
|
||||
readElsePart = called "else clause" $ do
|
||||
pos <- getPosition
|
||||
@@ -1671,14 +1651,14 @@ readSelectClause = called "select loop" $ do
|
||||
|
||||
readInClause = do
|
||||
g_In
|
||||
things <- (readCmdWord) `reluctantlyTill`
|
||||
(disregard (g_Semi) <|> disregard linefeed <|> disregard g_Do)
|
||||
things <- readCmdWord `reluctantlyTill`
|
||||
(disregard g_Semi <|> disregard linefeed <|> disregard g_Do)
|
||||
|
||||
do {
|
||||
lookAhead (g_Do);
|
||||
lookAhead g_Do;
|
||||
parseNote ErrorC 1063 "You need a line feed or semicolon before the 'do'.";
|
||||
} <|> do {
|
||||
optional $ g_Semi;
|
||||
optional g_Semi;
|
||||
disregard allspacing;
|
||||
}
|
||||
|
||||
@@ -1687,6 +1667,8 @@ readInClause = do
|
||||
prop_readCaseClause = isOk readCaseClause "case foo in a ) lol; cow;; b|d) fooo; esac"
|
||||
prop_readCaseClause2 = isOk readCaseClause "case foo\n in * ) echo bar;; esac"
|
||||
prop_readCaseClause3 = isOk readCaseClause "case foo\n in * ) echo bar & ;; esac"
|
||||
prop_readCaseClause4 = isOk readCaseClause "case foo\n in *) echo bar ;& bar) foo; esac"
|
||||
prop_readCaseClause5 = isOk readCaseClause "case foo\n in *) echo bar;;& foo) baz;; esac"
|
||||
readCaseClause = called "case expression" $ do
|
||||
id <- getNextId
|
||||
g_Case
|
||||
@@ -1707,14 +1689,21 @@ readCaseItem = called "case item" $ do
|
||||
pattern <- readPattern
|
||||
g_Rparen
|
||||
readLineBreak
|
||||
list <- ((lookAhead g_DSEMI >> return []) <|> readCompoundList)
|
||||
(g_DSEMI <|> lookAhead (readLineBreak >> g_Esac)) `attempting` do
|
||||
list <- (lookAhead readCaseSeparator >> return []) <|> readCompoundList
|
||||
separator <- readCaseSeparator `attempting` do
|
||||
pos <- getPosition
|
||||
lookAhead g_Rparen
|
||||
parseProblemAt pos ErrorC 1074
|
||||
"Did you forget the ;; after the previous case item?"
|
||||
readLineBreak
|
||||
return (pattern, list)
|
||||
return (separator, pattern, list)
|
||||
|
||||
readCaseSeparator = choice [
|
||||
tryToken ";;&" (const ()) >> return CaseContinue,
|
||||
tryToken ";&" (const ()) >> return CaseFallThrough,
|
||||
g_DSEMI >> return CaseBreak,
|
||||
lookAhead (readLineBreak >> g_Esac) >> return CaseBreak
|
||||
]
|
||||
|
||||
prop_readFunctionDefinition = isOk readFunctionDefinition "foo() { command foo --lol \"$@\"; }"
|
||||
prop_readFunctionDefinition1 = isOk readFunctionDefinition "foo (){ command foo --lol \"$@\"; }"
|
||||
@@ -1726,11 +1715,11 @@ prop_readFunctionDefinition8 = isOk readFunctionDefinition "foo() (ls)"
|
||||
readFunctionDefinition = called "function" $ do
|
||||
functionSignature <- try readFunctionSignature
|
||||
allspacing
|
||||
(disregard (lookAhead $ oneOf "{(") <|> parseProblem ErrorC 1064 "Expected a { to open the function definition.")
|
||||
disregard (lookAhead $ oneOf "{(") <|> parseProblem ErrorC 1064 "Expected a { to open the function definition."
|
||||
group <- readBraceGroup <|> readSubshell
|
||||
return $ functionSignature group
|
||||
where
|
||||
readFunctionSignature = do
|
||||
readFunctionSignature =
|
||||
readWithFunction <|> readWithoutFunction
|
||||
where
|
||||
readWithFunction = do
|
||||
@@ -1770,10 +1759,10 @@ readCompoundCommand = do
|
||||
cmd <- choice [ readBraceGroup, readArithmeticExpression, readSubshell, readCondition, readWhileClause, readUntilClause, readIfClause, readForClause, readSelectClause, readCaseClause, readFunctionDefinition]
|
||||
optional spacing
|
||||
redirs <- many readIoRedirect
|
||||
when (not . null $ redirs) $ optional $ do
|
||||
unless (null redirs) $ optional $ do
|
||||
lookAhead $ try (spacing >> needsSeparator)
|
||||
parseProblem WarningC 1013 "Bash requires ; or \\n here, after redirecting nested compound commands."
|
||||
return $ T_Redirecting id redirs $ cmd
|
||||
return $ T_Redirecting id redirs cmd
|
||||
where
|
||||
needsSeparator = choice [ g_Then, g_Else, g_Elif, g_Fi, g_Do, g_Done, g_Esac, g_Rbrace ]
|
||||
|
||||
@@ -1793,6 +1782,22 @@ readTimeSuffix = do
|
||||
lookAhead $ char '-'
|
||||
readCmdWord
|
||||
|
||||
-- Fixme: this is a hack that doesn't handle let '++c' or let a\>b
|
||||
readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord)
|
||||
where
|
||||
readLetExpression = do
|
||||
startPos <- getPosition
|
||||
expression <- readStringForParser readCmdWord
|
||||
subParse startPos readArithmeticContents expression
|
||||
|
||||
-- Get whatever a parser would parse as a string
|
||||
readStringForParser parser = do
|
||||
pos <- lookAhead (parser >> getPosition)
|
||||
s <- readUntil pos
|
||||
return s
|
||||
where
|
||||
readUntil endPos = anyChar `reluctantlyTill` (getPosition >>= guard . (== endPos))
|
||||
|
||||
prop_readAssignmentWord = isOk readAssignmentWord "a=42"
|
||||
prop_readAssignmentWord2 = isOk readAssignmentWord "b=(1 2 3)"
|
||||
prop_readAssignmentWord3 = isWarning readAssignmentWord "$b = 13"
|
||||
@@ -1802,7 +1807,8 @@ prop_readAssignmentWord6 = isWarning readAssignmentWord "b += (1 2 3)"
|
||||
prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42"
|
||||
prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42"
|
||||
prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "
|
||||
prop_readAssignmentWord0 = isWarning readAssignmentWord "foo$n=42"
|
||||
prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42"
|
||||
prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
|
||||
readAssignmentWord = try $ do
|
||||
id <- getNextId
|
||||
pos <- getPosition
|
||||
@@ -1853,9 +1859,24 @@ readArray = called "array assignment" $ do
|
||||
id <- getNextId
|
||||
char '('
|
||||
allspacing
|
||||
words <- (readNormalWord `thenSkip` allspacing) `reluctantlyTill` (char ')')
|
||||
words <- readElement `reluctantlyTill` char ')'
|
||||
char ')'
|
||||
return $ T_Array id words
|
||||
where
|
||||
readElement = (readIndexed <|> readRegular) `thenSkip` allspacing
|
||||
readIndexed = do
|
||||
id <- getNextId
|
||||
index <- try $ do
|
||||
x <- readArrayIndex
|
||||
char '='
|
||||
return x
|
||||
value <- readNormalWord <|> nothing
|
||||
return $ T_IndexedElement id index value
|
||||
readRegular = readNormalWord
|
||||
|
||||
nothing = do
|
||||
id <- getNextId
|
||||
return $ T_Literal id ""
|
||||
|
||||
tryToken s t = try $ do
|
||||
id <- getNextId
|
||||
@@ -1876,14 +1897,14 @@ tryParseWordToken keyword t = try $ do
|
||||
optional (do
|
||||
try . lookAhead $ char '['
|
||||
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
|
||||
|
||||
anycaseString str =
|
||||
mapM anycaseChar str
|
||||
anycaseString =
|
||||
mapM anycaseChar
|
||||
where
|
||||
anycaseChar c = char (toLower c) <|> char (toUpper c)
|
||||
|
||||
@@ -1930,11 +1951,11 @@ g_Semi = do
|
||||
tryToken ";" T_Semi
|
||||
|
||||
keywordSeparator =
|
||||
eof <|> disregard whitespace <|> (disregard $ oneOf ";()[<>&|")
|
||||
eof <|> disregard whitespace <|> disregard (oneOf ";()[<>&|")
|
||||
|
||||
readKeyword = choice [ g_Then, g_Else, g_Elif, g_Fi, g_Do, g_Done, g_Esac, g_Rbrace, g_Rparen, g_DSEMI ]
|
||||
|
||||
ifParse p t f = do
|
||||
ifParse p t f =
|
||||
(lookAhead (try p) >> t) <|> f
|
||||
|
||||
readShebang = do
|
||||
@@ -1953,24 +1974,24 @@ readScript = do
|
||||
pos <- getPosition
|
||||
optional $ do
|
||||
readUtf8Bom
|
||||
parseProblem ErrorC 1082 $
|
||||
parseProblem ErrorC 1082
|
||||
"This file has a UTF-8 BOM. Remove it with: LC_CTYPE=C sed '1s/^...//' < yourscript ."
|
||||
sb <- option "" readShebang
|
||||
verifyShell pos (getShell sb)
|
||||
if (isValidShell $ getShell sb) /= Just False
|
||||
if isValidShell (getShell sb) /= Just False
|
||||
then
|
||||
do {
|
||||
allspacing;
|
||||
commands <- readTerm;
|
||||
eof <|> (parseProblem ErrorC 1070 "Parsing stopped here because of parsing errors.");
|
||||
eof <|> parseProblem ErrorC 1070 "Parsing stopped here because of parsing errors.";
|
||||
return $ T_Script id sb commands;
|
||||
} <|> do {
|
||||
parseProblem WarningC 1014 "Couldn't read any commands.";
|
||||
return $ T_Script id sb $ [T_EOF id];
|
||||
return $ T_Script id sb [T_EOF id];
|
||||
}
|
||||
else do
|
||||
many anyChar
|
||||
return $ T_Script id sb $ [T_EOF id];
|
||||
return $ T_Script id sb [T_EOF id];
|
||||
|
||||
where
|
||||
basename s = reverse . takeWhile (/= '/') . reverse $ s
|
||||
@@ -2018,8 +2039,8 @@ readScript = do
|
||||
|
||||
rp p filename contents = Ms.runState (runParserT p initialState filename contents) ([], [])
|
||||
|
||||
isWarning p s = (fst cs) && (not . null . snd $ cs) where cs = checkString p s
|
||||
isOk p s = (fst cs) && (null . snd $ cs) where cs = checkString p s
|
||||
isWarning p s = fst cs && (not . null . snd $ cs) where cs = checkString p s
|
||||
isOk p s = fst cs && (null . snd $ cs) where cs = checkString p s
|
||||
|
||||
checkString parser string =
|
||||
case rp (parser >> eof >> getState) "-" string of
|
||||
@@ -2043,7 +2064,7 @@ makeErrorFor parsecError =
|
||||
|
||||
getStringFromParsec errors =
|
||||
case map snd $ sortWith fst $ map f errors of
|
||||
r -> (intercalate " " $ take 1 $ nub r) ++ " Fix any mentioned problems and try again."
|
||||
r -> unwords (take 1 $ nub r) ++ " Fix any mentioned problems and try again."
|
||||
where f err =
|
||||
case err of
|
||||
UnExpect s -> (1, unexpected s)
|
||||
@@ -2052,15 +2073,15 @@ getStringFromParsec errors =
|
||||
Message s -> (4, s ++ ".")
|
||||
wut "" = "eof"
|
||||
wut x = x
|
||||
unexpected s = "Unexpected " ++ (wut s) ++ "."
|
||||
unexpected s = "Unexpected " ++ wut s ++ "."
|
||||
|
||||
parseShell filename contents = do
|
||||
parseShell filename contents =
|
||||
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]))
|
||||
(nub $ sortNotes $ p ++ notesForContext context ++ [makeErrorFor err])
|
||||
where
|
||||
isName (ContextName _ _) = True
|
||||
isName _ = False
|
||||
@@ -2071,4 +2092,8 @@ parseShell filename contents = do
|
||||
"The mentioned parser error was in this " ++ str ++ "."
|
||||
|
||||
lt x = trace (show x) x
|
||||
ltt t x = trace (show t) x
|
||||
ltt t = trace (show t)
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
||||
|
||||
|
@@ -15,35 +15,20 @@
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Simple (shellCheck, ShellCheckComment, scLine, scColumn, scSeverity, scCode, scMessage) where
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
module ShellCheck.Simple (shellCheck, ShellCheckComment, scLine, scColumn, scSeverity, scCode, scMessage, runTests) where
|
||||
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Analytics
|
||||
import ShellCheck.Parser hiding (runTests)
|
||||
import ShellCheck.Analytics hiding (runTests)
|
||||
import Data.Maybe
|
||||
import Text.Parsec.Pos
|
||||
import Data.List
|
||||
|
||||
|
||||
prop_findsParseIssue =
|
||||
let comments = shellCheck "echo \"$12\"" [] in
|
||||
(length comments) == 1 && (scCode $ head comments) == 1037
|
||||
prop_commentDisablesParseIssue1 =
|
||||
null $ shellCheck "#shellcheck disable=SC1037\necho \"$12\"" []
|
||||
prop_commentDisablesParseIssue2 =
|
||||
null $ shellCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\"" []
|
||||
|
||||
prop_findsAnalysisIssue =
|
||||
let comments = shellCheck "echo $1" [] in
|
||||
(length comments) == 1 && (scCode $ head comments) == 2086
|
||||
prop_commentDisablesAnalysisIssue1 =
|
||||
null $ shellCheck "#shellcheck disable=SC2086\necho $1" []
|
||||
prop_commentDisablesAnalysisIssue2 =
|
||||
null $ shellCheck "#shellcheck disable=SC2086\n#lol\necho $1" []
|
||||
import Test.QuickCheck.All (quickCheckAll)
|
||||
|
||||
shellCheck :: String -> [AnalysisOption] -> [ShellCheckComment]
|
||||
shellCheck script options =
|
||||
let (ParseResult result notes) = parseShell "-" script in
|
||||
let allNotes = notes ++ (concat $ maybeToList $ do
|
||||
let allNotes = notes ++ concat (maybeToList $ do
|
||||
(tree, posMap) <- result
|
||||
let list = runAnalytics options tree
|
||||
return $ map (noteToParseNote posMap) $ filterByAnnotation tree list
|
||||
@@ -65,3 +50,23 @@ severityToString s =
|
||||
|
||||
formatNote (ParseNote pos severity code text) =
|
||||
ShellCheckComment (sourceLine pos) (sourceColumn pos) (severityToString severity) (fromIntegral code) text
|
||||
|
||||
prop_findsParseIssue =
|
||||
let comments = shellCheck "echo \"$12\"" [] in
|
||||
length comments == 1 && scCode (head comments) == 1037
|
||||
prop_commentDisablesParseIssue1 =
|
||||
null $ shellCheck "#shellcheck disable=SC1037\necho \"$12\"" []
|
||||
prop_commentDisablesParseIssue2 =
|
||||
null $ shellCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\"" []
|
||||
|
||||
prop_findsAnalysisIssue =
|
||||
let comments = shellCheck "echo $1" [] in
|
||||
length comments == 1 && scCode (head comments) == 2086
|
||||
prop_commentDisablesAnalysisIssue1 =
|
||||
null $ shellCheck "#shellcheck disable=SC2086\necho $1" []
|
||||
prop_commentDisablesAnalysisIssue2 =
|
||||
null $ shellCheck "#shellcheck disable=SC2086\n#lol\necho $1" []
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
||||
|
||||
|
@@ -1,65 +0,0 @@
|
||||
#!/usr/bin/env runhaskell
|
||||
-- #!/usr/bin/env runhugs
|
||||
-- $Id: quickcheck,v 1.4 2003/01/08 15:09:22 shae Exp $
|
||||
-- This file defines a command
|
||||
-- quickCheck <options> <files>
|
||||
-- which invokes quickCheck on all properties defined in the files given as
|
||||
-- arguments, by generating an input script for hugs and then invoking it.
|
||||
-- quickCheck recognises the options
|
||||
-- +names print the name of each property before checking it
|
||||
-- -names do not print property names (the default)
|
||||
-- +verbose displays each test case before running
|
||||
-- -verbose do not displays each test case before running (the default)
|
||||
-- Other options (beginning with + or -) are passed unchanged to hugs.
|
||||
--
|
||||
-- Change the first line of this file to the location of runhaskell or runhugs
|
||||
-- on your system.
|
||||
-- Make the file executable.
|
||||
--
|
||||
-- TODO:
|
||||
-- someone on #haskell asked about supporting QC tests inside LaTeX, ex. \{begin} \{end}, how?
|
||||
|
||||
import System.Cmd
|
||||
import System.Directory (findExecutable)
|
||||
import System.Environment
|
||||
import Data.List
|
||||
import Data.Maybe (fromJust)
|
||||
|
||||
main :: IO ()
|
||||
main = do as<-getArgs
|
||||
sequence_ (map (process (filter isOption as))
|
||||
(filter (not.isOption) as))
|
||||
|
||||
-- ugly hack for .lhs files, is there a better way?
|
||||
unlit [] = []
|
||||
unlit x = if (head x) == '>' then (tail x) else x
|
||||
|
||||
process opts file =
|
||||
let (namesOpt,opts') = getOption "names" "-names" opts
|
||||
(verboseOpt,opts'') = getOption "verbose" "-verbose" opts' in
|
||||
do xs<-readFile file
|
||||
let names = nub$ filter (\x -> (("> prop_" `isPrefixOf` x) || ("prop_" `isPrefixOf` x)))
|
||||
(map (fst.head.lex.unlit) (lines xs))
|
||||
if null names then
|
||||
putStr (file++": no properties to check\n")
|
||||
else do writeFile "hugsin"$
|
||||
unlines ((":load "++file):
|
||||
":m +Test.QuickCheck":
|
||||
"let quackCheck p = quickCheckWith (stdArgs { maxSuccess = 1 }) p ":
|
||||
[(if namesOpt=="+names" then
|
||||
"putStr \""++p++": \" >> "
|
||||
else "") ++
|
||||
("quackCheck ")
|
||||
++ p | p<-names])
|
||||
-- To use ghci
|
||||
ghci <- findExecutable "ghci"
|
||||
system (fromJust ghci ++options opts''++" <hugsin")
|
||||
return ()
|
||||
|
||||
isOption xs = head xs `elem` "-+"
|
||||
|
||||
options opts = unwords ["\""++opt++"\"" | opt<-opts]
|
||||
|
||||
getOption name def opts =
|
||||
let opt = head [opt | opt<-opts++[def], isPrefixOf name (drop 1 opt)] in
|
||||
(opt, filter (/=opt) opts)
|
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Todo: Find a way to make this not suck.
|
||||
|
||||
ulimit -t 60 # Sometimes GHC ends in a spin loop, and this is easier than debugging
|
||||
|
||||
[[ -e test/quackCheck.hs ]] || { echo "Are you running me from the wrong directory?"; exit 1; }
|
||||
[[ $1 == -v ]] && pattern="" || pattern="FAIL"
|
||||
|
||||
find . -name '*.hs' -exec bash -c '
|
||||
grep -v "^module " "$1" > quack.tmp.hs
|
||||
./test/quackCheck.hs +names quack.tmp.hs
|
||||
' -- {} \; 2>&1 | grep -i "$pattern"
|
||||
result=$?
|
||||
rm -f quack.tmp.hs hugsin
|
||||
|
||||
if [[ $result == 0 ]]
|
||||
then
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
|
16
test/shellcheck.hs
Normal file
16
test/shellcheck.hs
Normal file
@@ -0,0 +1,16 @@
|
||||
module Main where
|
||||
|
||||
import Control.Monad
|
||||
import System.Exit
|
||||
import qualified ShellCheck.Simple
|
||||
import qualified ShellCheck.Analytics
|
||||
import qualified ShellCheck.Parser
|
||||
|
||||
main = do
|
||||
putStrLn "Running ShellCheck tests..."
|
||||
results <- sequence [ShellCheck.Simple.runTests,
|
||||
ShellCheck.Analytics.runTests,
|
||||
ShellCheck.Parser.runTests]
|
||||
if and results then exitSuccess
|
||||
else exitFailure
|
||||
|
Reference in New Issue
Block a user