blob: 98bf17e2a7d496f0bfff4e46730e13eca2b8af74 [file] [log] [blame]
{-
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiWayIf #-}
module ShellCheck.Parser (parseScript) where
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.Data
import ShellCheck.Interface
import Control.Applicative ((<*), (*>))
import Control.Monad
import Control.Monad.Identity
import Control.Monad.Trans
import Data.Char
import Data.Functor
import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub)
import Data.Maybe
import Data.Monoid
import Debug.Trace
import GHC.Exts (sortWith)
import Prelude hiding (readList)
import System.IO
import Text.Parsec hiding (runParser, (<?>))
import Text.Parsec.Error
import Text.Parsec.Pos
import qualified Control.Monad.Reader as Mr
import qualified Control.Monad.State as Ms
import qualified Data.Map as Map
prop :: Bool -> IO ()
prop False = putStrLn "FAIL"
prop True = return ()
type SCBase m = Mr.ReaderT (Environment m) (Ms.StateT SystemState m)
type SCParser m v = ParsecT String UserState (SCBase m) v
backslash :: Monad m => SCParser m Char
backslash = char '\\'
linefeed :: Monad m => SCParser m Char
linefeed = do
optional carriageReturn
c <- char '\n'
readPendingHereDocs
return c
singleQuote = char '\''
doubleQuote = char '"'
variableStart = upper <|> lower <|> oneOf "_"
variableChars = upper <|> lower <|> digit <|> oneOf "_"
-- Chars to allow in function names
functionChars = variableChars <|> oneOf ":+?-./^@"
-- Chars to allow in functions using the 'function' keyword
extendedFunctionChars = functionChars <|> oneOf "[]*=!"
specialVariable = oneOf "@*#?-$!"
paramSubSpecialChars = oneOf "/:+-=%"
quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars
quotable = almostSpace <|> oneOf quotableChars
bracedQuotable = oneOf "}\"$`'"
doubleQuotableChars = "\\\"$`"
doubleQuotable = oneOf doubleQuotableChars
whitespace = oneOf " \t" <|> carriageReturn <|> almostSpace <|> linefeed
linewhitespace = oneOf " \t" <|> almostSpace
suspectCharAfterQuotes = variableChars <|> char '%'
extglobStartChars = "?*@!+"
extglobStart = oneOf extglobStartChars
unicodeDoubleQuotes = "\x201C\x201D\x2033\x2036"
unicodeSingleQuotes = "\x2018\x2019"
-- |
-- >>> prop $ isOk spacing " \\\n # Comment"
spacing = do
x <- many (many1 linewhitespace <|> try (string "\\\n" >> return ""))
optional readComment
return $ concat x
spacing1 = do
spacing <- spacing
when (null spacing) $ fail "Expected whitespace"
return spacing
-- |
-- >>> prop $ isOk allspacing "#foo"
-- >>> prop $ isOk allspacing " #foo\n # bar\n#baz\n"
-- >>> prop $ isOk allspacing "#foo\n#bar\n#baz\n"
allspacing = do
s <- spacing
more <- option False (linefeed >> return True)
if more then do
rest <- allspacing
return $ s ++ "\n" ++ rest
else
return s
allspacingOrFail = do
s <- allspacing
when (null s) $ fail "Expected whitespace"
readUnicodeQuote = do
start <- startSpan
c <- oneOf (unicodeSingleQuotes ++ unicodeDoubleQuotes)
id <- endSpan start
parseProblemAtId id WarningC 1110 "This is a unicode quote. Delete and retype it (or quote to make literal)."
return $ T_Literal id [c]
carriageReturn = do
parseNote ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ."
char '\r'
almostSpace =
choice [
check '\xA0' "unicode non-breaking space",
check '\x200B' "unicode zerowidth space"
]
where
check c name = do
parseNote ErrorC 1018 $ "This is a " ++ name ++ ". Delete and retype it."
char c
return ' '
--------- Message/position annotation on top of user state
data Note = Note Id Severity Code String deriving (Show, Eq)
data ParseNote = ParseNote SourcePos SourcePos Severity Code String deriving (Show, Eq)
data Context =
ContextName SourcePos String
| ContextAnnotation [Annotation]
| ContextSource String
deriving (Show)
data HereDocContext =
HereDocPending Token [Context] -- on linefeed, read this T_HereDoc
deriving (Show)
data UserState = UserState {
lastId :: Id,
positionMap :: Map.Map Id (SourcePos, SourcePos),
parseNotes :: [ParseNote],
hereDocMap :: Map.Map Id [Token],
pendingHereDocs :: [HereDocContext]
}
initialUserState = UserState {
lastId = Id $ -1,
positionMap = Map.empty,
parseNotes = [],
hereDocMap = Map.empty,
pendingHereDocs = []
}
codeForParseNote (ParseNote _ _ _ code _) = code
noteToParseNote map (Note id severity code message) =
ParseNote pos pos severity code message
where
pos = fromJust $ Map.lookup id map
getLastId = lastId <$> getState
getNextIdBetween startPos endPos = do
state <- getState
let newId = incId (lastId state)
let newMap = Map.insert newId (startPos, endPos) (positionMap state)
putState $ state {
lastId = newId,
positionMap = newMap
}
return newId
where incId (Id n) = Id $ n+1
getNextIdSpanningTokens startTok endTok = do
(start, _) <- getSpanForId (getId startTok)
(_, end) <- getSpanForId (getId endTok)
getNextIdBetween start end
-- Get an ID starting from the first token of the list, and ending after the last
getNextIdSpanningTokenList list =
if null list
then do
pos <- getPosition
getNextIdBetween pos pos
else
getNextIdSpanningTokens (head list) (last list)
-- Get the span covered by an id
getSpanForId :: Monad m => Id -> SCParser m (SourcePos, SourcePos)
getSpanForId id =
Map.findWithDefault (error "Internal error: no position for id. Please report!") id <$>
getMap
-- Create a new id with the same span as an existing one
getNewIdFor :: Monad m => Id -> SCParser m Id
getNewIdFor id = getSpanForId id >>= uncurry getNextIdBetween
data IncompleteInterval = IncompleteInterval SourcePos
startSpan = IncompleteInterval <$> getPosition
endSpan (IncompleteInterval start) = do
endPos <- getPosition
id <- getNextIdBetween start endPos
return id
addToHereDocMap id list = do
state <- getState
let map = hereDocMap state
putState $ state {
hereDocMap = Map.insert id list map
}
addPendingHereDoc t = do
state <- getState
context <- getCurrentContexts
let docs = pendingHereDocs state
putState $ state {
pendingHereDocs = HereDocPending t context : docs
}
popPendingHereDocs = do
state <- getState
let pending = pendingHereDocs state
putState $ state {
pendingHereDocs = []
}
return . reverse $ pendingHereDocs state
getMap = positionMap <$> getState
getParseNotes = parseNotes <$> getState
addParseNote n = do
irrelevant <- shouldIgnoreCode (codeForParseNote n)
unless irrelevant $ do
state <- getState
putState $ state {
parseNotes = n : parseNotes state
}
shouldIgnoreCode code = do
context <- getCurrentContexts
checkSourced <- Mr.asks checkSourced
return $ any (disabling checkSourced) context
where
disabling checkSourced item =
case item of
ContextAnnotation list -> any disabling' list
ContextSource _ -> not $ checkSourced
_ -> False
disabling' (DisableComment n) = code == n
disabling' _ = False
shouldFollow file = do
context <- getCurrentContexts
if any isThisFile context
then return False
else
if length (filter isSource context) >= 100
then do
parseProblem ErrorC 1092 "Stopping at 100 'source' frames :O"
return False
else
return True
where
isSource (ContextSource _) = True
isSource _ = False
isThisFile (ContextSource name) | name == file = True
isThisFile _= False
getSourceOverride = do
context <- getCurrentContexts
return . msum . map findFile $ takeWhile isSameFile context
where
isSameFile (ContextSource _) = False
isSameFile _ = True
findFile (ContextAnnotation list) = msum $ map getFile list
findFile _ = Nothing
getFile (SourceOverride str) = Just str
getFile _ = Nothing
-- Store potential parse problems outside of parsec
data SystemState = SystemState {
contextStack :: [Context],
parseProblems :: [ParseNote]
}
initialSystemState = SystemState {
contextStack = [],
parseProblems = []
}
data Environment m = Environment {
systemInterface :: SystemInterface m,
checkSourced :: Bool,
shellTypeOverride :: Maybe Shell
}
parseProblem level code msg = do
pos <- getPosition
parseProblemAt pos level code msg
setCurrentContexts c = Ms.modify (\state -> state { contextStack = c })
getCurrentContexts = contextStack <$> Ms.get
popContext = do
v <- getCurrentContexts
if not $ null v
then do
let (a:r) = v
setCurrentContexts r
return $ Just a
else
return Nothing
pushContext c = do
v <- getCurrentContexts
setCurrentContexts (c:v)
parseProblemAtWithEnd start end level code msg = do
irrelevant <- shouldIgnoreCode code
unless irrelevant $
addParseProblem note
where
note = ParseNote start end level code msg
addParseProblem note =
Ms.modify (\state -> state {
parseProblems = note:parseProblems state
})
parseProblemAt pos = parseProblemAtWithEnd pos pos
parseProblemAtId :: Monad m => Id -> Severity -> Integer -> String -> SCParser m ()
parseProblemAtId id level code msg = do
(start, end) <- getSpanForId id
parseProblemAtWithEnd start end level code msg
-- Store non-parse problems inside
parseNote c l a = do
pos <- getPosition
parseNoteAt pos c l a
parseNoteAt pos c l a = addParseNote $ ParseNote pos pos c l a
parseNoteAtId id c l a = do
(start, end) <- getSpanForId id
addParseNote $ ParseNote start end c l a
parseNoteAtWithEnd start end c l a = addParseNote $ ParseNote start end c l a
--------- Convenient combinators
thenSkip main follow = do
r <- main
optional follow
return r
unexpecting s p = try $
(try p >> fail ("Unexpected " ++ s)) <|> return ()
notFollowedBy2 = unexpecting ""
reluctantlyTill p end =
(lookAhead (void (try end) <|> eof) >> return []) <|> do
x <- p
more <- reluctantlyTill p end
return $ x:more
<|> return []
reluctantlyTill1 p end = do
notFollowedBy2 end
x <- p
more <- reluctantlyTill p end
return $ x:more
attempting rest branch =
(try branch >> rest) <|> rest
orFail parser errorAction =
try parser <|> (errorAction >>= fail)
-- Construct a node with a parser, e.g. T_Literal `withParser` (readGenericLiteral ",")
withParser node parser = do
start <- startSpan
contents <- parser
id <- endSpan start
return $ node id contents
wasIncluded p = option False (p >> return True)
acceptButWarn parser level code note =
optional $ try (do
pos <- getPosition
parser
parseProblemAt pos level code note
)
parsecBracket before after op = do
val <- before
(op val <* optional (after val)) <|> (after val *> fail "")
swapContext contexts p =
parsecBracket (getCurrentContexts <* setCurrentContexts contexts)
setCurrentContexts
(const p)
withContext entry p = parsecBracket (pushContext entry) (const popContext) (const p)
called s p = do
pos <- getPosition
withContext (ContextName pos s) p
withAnnotations anns =
withContext (ContextAnnotation anns)
readConditionContents single =
readCondContents `attempting` lookAhead (do
pos <- getPosition
s <- readVariableName
when (s `elem` commonCommands) $
parseProblemAt pos WarningC 1014 "Use 'if cmd; then ..' to check exit code, or 'if [[ $(cmd) == .. ]]' to check output.")
where
spacingOrLf = condSpacing True
condSpacing required = do
pos <- getPosition
space <- allspacing
when (required && null space) $
parseProblemAt pos ErrorC 1035 "You are missing a required space here."
when (single && '\n' `elem` space) $
parseProblemAt pos ErrorC 1080 "When breaking lines in [ ], you need \\ before the linefeed."
return space
typ = if single then SingleBracket else DoubleBracket
readCondBinaryOp = try $ do
optional guardArithmetic
op <- getOp
spacingOrLf
return op
where
flaglessOps = [ "==", "!=", "<=", ">=", "=~", ">", "<", "=" ]
getOp = do
start <- startSpan
op <- readRegularOrEscaped anyOp
id <- endSpan start
return $ TC_Binary id typ op
anyOp = flagOp <|> flaglessOp <|> fail
"Expected comparison operator (don't wrap commands in []/[[]])"
flagOp = try $ do
s <- readOp
when (s == "-a" || s == "-o") $ fail "Unexpected operator"
return s
flaglessOp =
choice $ map (try . string) flaglessOps
-- hacks to read quoted operators without having to read a shell word
readEscaped p = try $ withEscape <|> withQuotes
where
withEscape = do
char '\\'
escaped <$> p
withQuotes = do
c <- oneOf "'\""
s <- p
char c
return $ escaped s
escaped s = if any (`elem` s) "<>()" then '\\':s else s
readRegularOrEscaped p = readEscaped p <|> p
guardArithmetic = do
try . lookAhead $ void (oneOf "+*/%") <|> void (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
liftM op readCondWord `orFail` do
parseProblemAt pos ErrorC 1019 "Expected this to be an argument to the unary condition."
return "Expected an argument for the unary operator"
readCondUnaryOp = try $ do
start <- startSpan
s <- readOp
id <- endSpan start
spacingOrLf
return $ TC_Unary id typ s
readOp = try $ do
char '-' <|> weirdDash
s <- many1 letter <|> fail "Expected a test operator"
return ('-':s)
weirdDash = do
pos <- getPosition
oneOf "\x058A\x05BE\x2010\x2011\x2012\x2013\x2014\x2015\xFE63\xFF0D"
parseProblemAt pos ErrorC 1100
"This is a unicode dash. Delete and retype as ASCII minus."
return '-'
readCondWord = do
notFollowedBy2 (try (spacing >> string "]"))
x <- readNormalWord
pos <- getPosition
when (endedWith "]" x && notArrayIndex 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 )"
void spacing
return x
where endedWith str (T_NormalWord id s@(_:_)) =
case last s of T_Literal id s -> str `isSuffixOf` s
_ -> False
endedWith _ _ = False
notArrayIndex (T_NormalWord id s@(_:T_Literal _ t:_)) = t /= "["
notArrayIndex _ = True
readCondAndOp = readAndOrOp TC_And "&&" False <|> readAndOrOp TC_And "-a" True
readCondOrOp = do
optional guardArithmetic
readAndOrOp TC_Or "||" False <|> readAndOrOp TC_Or "-o" True
readAndOrOp node op requiresSpacing = do
optional $ lookAhead weirdDash
start <- startSpan
x <- try $ string op
id <- endSpan start
condSpacing requiresSpacing
return $ node id typ x
readCondNullaryOrBinary = do
start <- startSpan
x <- readCondWord `attempting` (do
pos <- getPosition
lookAhead (char '[')
parseProblemAt pos ErrorC 1026 $ if single
then "If grouping expressions inside [..], use \\( ..\\)."
else "If grouping expressions inside [[..]], use ( .. )."
)
id <- endSpan start
(do
pos <- getPosition
isRegex <- regexOperatorAhead
op <- readCondBinaryOp
y <- if isRegex
then readRegex
else readCondWord <|> (parseProblemAt pos ErrorC 1027 "Expected another argument for this operator." >> mzero)
return (x `op` y)
) <|> ( do
checkTrailingOp x
return $ TC_Nullary id typ x
)
checkTrailingOp x = fromMaybe (return ()) $ do
(T_Literal id str) <- getTrailingUnquotedLiteral x
trailingOp <- listToMaybe (filter (`isSuffixOf` str) binaryTestOps)
return $ parseProblemAtId id ErrorC 1108 $
"You need a space before and after the " ++ trailingOp ++ " ."
readCondGroup = do
start <- startSpan
pos <- getPosition
lparen <- try $ readRegularOrEscaped (string "(")
when (single && lparen == "(") $
singleWarning pos
when (not single && lparen == "\\(") $
doubleWarning pos
condSpacing single
x <- readCondContents
cpos <- getPosition
rparen <- readRegularOrEscaped (string ")")
id <- endSpan start
condSpacing single
when (single && rparen == ")") $
singleWarning cpos
when (not single && rparen == "\\)") $
doubleWarning cpos
return $ TC_Group id typ x
where
singleWarning pos =
parseProblemAt pos ErrorC 1028 "In [..] you have to escape \\( \\) or preferably combine [..] expressions."
doubleWarning pos =
parseProblemAt pos ErrorC 1029 "In [[..]] you shouldn't escape ( or )."
-- Currently a bit of a hack since parsing rules are obscure
regexOperatorAhead = lookAhead (do
try (string "=~") <|> try (string "~=")
return True)
<|> return False
readRegex = called "regex" $ do
start <- startSpan
parts <- many1 readPart
id <- endSpan start
void spacing
return $ T_NormalWord id parts
where
readPart = choice [
readGroup,
readSingleQuoted,
readDoubleQuoted,
readDollarExpression,
readLiteralForParser $ readNormalLiteral "( ",
readLiteralString "|",
readGlobLiteral
]
readGlobLiteral = do
start <- startSpan
s <- extglobStart <|> oneOf "{}[]$"
id <- endSpan start
return $ T_Literal id [s]
readGroup = called "regex grouping" $ do
start <- startSpan
p1 <- readLiteralString "("
parts <- many (readPart <|> readRegexLiteral)
p2 <- readLiteralString ")"
id <- endSpan start
return $ T_NormalWord id (p1:(parts ++ [p2]))
readRegexLiteral = do
start <- startSpan
str <- readGenericLiteral1 (singleQuote <|> doubleQuotable <|> oneOf "()")
id <- endSpan start
return $ T_Literal id str
readLiteralString s = do
start <- startSpan
str <- string s
id <- endSpan start
return $ T_Literal id str
readCondTerm = do
term <- readCondNot <|> readCondExpr
condSpacing False
return term
readCondNot = do
start <- startSpan
char '!'
id <- endSpan start
spacingOrLf
expr <- readCondExpr
return $ TC_Unary id typ "!" expr
readCondExpr =
readCondGroup <|> readCondUnaryExp <|> readCondNullaryOrBinary
readCondOr = chainl1 readCondAnd readCondAndOp
readCondAnd = chainl1 readCondTerm readCondOrOp
readCondContents = readCondOr
-- |
-- >>> prop $ isOk readArithmeticContents " n++ + ++c"
-- >>> prop $ isOk readArithmeticContents "$N*4-(3,2)"
-- >>> prop $ isOk readArithmeticContents "n|=2<<1"
-- >>> prop $ isOk readArithmeticContents "n &= 2 **3"
-- >>> prop $ isOk readArithmeticContents "1 |= 4 && n >>= 4"
-- >>> prop $ isOk readArithmeticContents " 1 | 2 ||3|4"
-- >>> prop $ isOk readArithmeticContents "3*2**10"
-- >>> prop $ isOk readArithmeticContents "3"
-- >>> prop $ isOk readArithmeticContents "a^!-b"
-- >>> prop $ isOk readArithmeticContents "! $?"
-- >>> prop $ isOk readArithmeticContents "10#08 * 16#f"
-- >>> prop $ isOk readArithmeticContents "\"$((3+2))\" + '37'"
-- >>> prop $ isOk readArithmeticContents "foo[9*y+x]++"
-- >>> prop $ isOk readArithmeticContents "1+`echo 2`"
-- >>> prop $ isOk readArithmeticContents "foo[`echo foo | sed s/foo/4/g` * 3] + 4"
-- >>> prop $ isOk readArithmeticContents "$foo$bar"
-- >>> prop $ isOk readArithmeticContents "i<(0+(1+1))"
-- >>> prop $ isOk readArithmeticContents "a?b:c"
-- >>> prop $ isOk readArithmeticContents "\\\n3 +\\\n 2"
-- >>> prop $ isOk readArithmeticContents "a ? b ? c : d : e"
-- >>> prop $ isOk readArithmeticContents "a ? b : c ? d : e"
-- >>> prop $ isOk readArithmeticContents "!!a"
-- >>> prop $ isOk readArithmeticContents "~0"
readArithmeticContents :: Monad m => SCParser m Token
readArithmeticContents =
readSequence
where
spacing =
let lf = try (string "\\\n") >> return '\n'
in many (whitespace <|> lf)
splitBy x ops = chainl1 x (readBinary ops)
readBinary ops = readComboOp ops TA_Binary
readComboOp op token = do
start <- startSpan
op <- choice (map (\x -> try $ do
s <- string x
failIfIncompleteOp
return s
) op)
id <- endSpan start
spacing
return $ token id op
failIfIncompleteOp = notFollowedBy2 $ oneOf "&|<>="
-- Read binary minus, but also check for -lt, -gt and friends:
readMinusOp = do
start <- startSpan
pos <- getPosition
try $ do
char '-'
failIfIncompleteOp
optional $ do
(str, alt) <- lookAhead . choice $ map tryOp [
("lt", "<"),
("gt", ">"),
("le", "<="),
("ge", ">="),
("eq", "=="),
("ne", "!=")
]
parseProblemAt pos ErrorC 1106 $ "In arithmetic contexts, use " ++ alt ++ " instead of -" ++ str
id <- endSpan start
spacing
return $ TA_Binary id "-"
where
tryOp (str, alt) = try $ do
string str
spacing1
return (str, alt)
readArrayIndex = do
start <- startSpan
char '['
pos <- getPosition
middle <- readStringForParser readArithmeticContents
char ']'
id <- endSpan start
return $ T_UnparsedIndex id pos middle
literal s = do
start <- startSpan
string s
id <- endSpan start
return $ T_Literal id s
readVariable = do
start <- startSpan
name <- readVariableName
indices <- many readArrayIndex
id <- endSpan start
spacing
return $ TA_Variable id name indices
readExpansion = do
start <- startSpan
pieces <- many1 $ choice [
readSingleQuoted,
readDoubleQuoted,
readNormalDollar,
readBraced,
readUnquotedBackTicked,
literal "#",
readNormalLiteral "+-*/=%^,]?:"
]
id <- endSpan start
spacing
return $ TA_Expansion id pieces
readGroup = do
char '('
s <- readSequence
char ')'
spacing
return s
readArithTerm = readGroup <|> readVariable <|> readExpansion
readSequence = do
spacing
start <- startSpan
l <- readAssignment `sepBy` (char ',' >> spacing)
id <- endSpan start
return $ TA_Sequence id l
readAssignment = chainr1 readTrinary readAssignmentOp
readAssignmentOp = readComboOp ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="] TA_Assignment
readTrinary = do
x <- readLogicalOr
do
start <- startSpan
string "?"
spacing
y <- readTrinary
string ":"
spacing
z <- readTrinary
id <- endSpan start
return $ TA_Trinary id x y z
<|>
return x
readLogicalOr = readLogicalAnd `splitBy` ["||"]
readLogicalAnd = readBitOr `splitBy` ["&&"]
readBitOr = readBitXor `splitBy` ["|"]
readBitXor = readBitAnd `splitBy` ["^"]
readBitAnd = readEquated `splitBy` ["&"]
readEquated = readCompared `splitBy` ["==", "!="]
readCompared = readShift `splitBy` ["<=", ">=", "<", ">"]
readShift = readAddition `splitBy` ["<<", ">>"]
readAddition = chainl1 readMultiplication (readBinary ["+"] <|> readMinusOp)
readMultiplication = readExponential `splitBy` ["*", "/", "%"]
readExponential = readAnyNegated `splitBy` ["**"]
readAnyNegated = readNegated <|> readAnySigned
readNegated = do
start <- startSpan
op <- oneOf "!~"
id <- endSpan start
spacing
x <- readAnyNegated
return $ TA_Unary id [op] x
readAnySigned = readSigned <|> readAnycremented
readSigned = do
start <- startSpan
op <- choice (map readSignOp "+-")
id <- endSpan start
spacing
x <- readAnycremented
return $ TA_Unary id [op] x
where
readSignOp c = try $ do
char c
notFollowedBy2 $ char c
spacing
return c
readAnycremented = readNormalOrPostfixIncremented <|> readPrefixIncremented
readPrefixIncremented = do
start <- startSpan
op <- try $ string "++" <|> string "--"
id <- endSpan start
spacing
x <- readArithTerm
return $ TA_Unary id (op ++ "|") x
readNormalOrPostfixIncremented = do
x <- readArithTerm
spacing
do
start <- startSpan
op <- try $ string "++" <|> string "--"
id <- endSpan start
spacing
return $ TA_Unary id ('|':op) x
<|>
return x
-- |
-- >>> prop $ isOk readCondition "[ \\( a = b \\) -a \\( c = d \\) ]"
-- >>> prop $ isOk readCondition "[[ (a = b) || (c = d) ]]"
-- >>> prop $ isOk readCondition "[[ $c = [[:alpha:].~-] ]]"
-- >>> prop $ isOk readCondition "[[ $c =~ *foo* ]]"
-- >>> prop $ isOk readCondition "[[ $c =~ f( ]] )* ]]"
-- >>> prop $ isOk readCondition "[[ $c =~ a(b) ]]"
-- >>> prop $ isOk readCondition "[[ $c =~ f( ($var ]]) )* ]]"
-- >>> prop $ isOk readCondition "[[ $c =~ ^[yY]$ ]]"
-- >>> prop $ isOk readCondition "[[ ${line} =~ ^[[:space:]]*# ]]"
-- >>> prop $ isOk readCondition "[[ $l =~ ogg|flac ]]"
-- >>> prop $ isOk readCondition "[ foo -a -f bar ]"
-- >>> prop $ isOk readCondition "[[\na == b\n||\nc == d ]]"
-- >>> prop $ isOk readCondition "[[\na == b ||\nc == d ]]"
-- >>> prop $ isOk readCondition "[[ a == b\n||\nc == d ]]"
-- >>> prop $ isOk readCondition "[[ a == b ||\n c == d ]]"
-- >>> prop $ isWarning readCondition "[ a == b \n -o c == d ]"
-- >>> prop $ isOk readCondition "[[ foo =~ ^fo{1,3}$ ]]"
-- >>> prop $ isOk readCondition "[ foo '>' bar ]"
-- >>> prop $ isOk readCondition "[ foo \">=\" bar ]"
-- >>> prop $ isOk readCondition "[ foo \\< bar ]"
-- >>> prop $ isOk readCondition "[[ ${file::1} = [-.\\|/\\\\] ]]"
-- >>> prop $ isOk readCondition "[ ]"
-- >>> prop $ isOk readCondition "[ '(' x \")\" ]"
-- >>> prop $ isOk readCondition "[[ echo_rc -eq 0 ]]"
-- >>> prop $ isOk readCondition "[[ $1 =~ ^(a\\ b)$ ]]"
-- >>> prop $ isOk readCondition "[[ $1 =~ \\.a\\.(\\.b\\.)\\.c\\. ]]"
-- >>> prop $ isOk readCondition "[[ -v arr[$var] ]]"
readCondition = called "test expression" $ do
opos <- getPosition
start <- startSpan
open <- try (string "[[") <|> string "["
let single = open == "["
let typ = if single then SingleBracket else DoubleBracket
pos <- getPosition
space <- allspacing
when (null space) $
parseProblemAtWithEnd opos pos ErrorC 1035 $ "You need a space after the " ++
if single
then "[ and before the ]."
else "[[ and before the ]]."
when (single && '\n' `elem` space) $
parseProblemAt pos ErrorC 1080 "You need \\ before line feeds to break lines in [ ]."
condition <- readConditionContents single <|> do
guard . not . null $ space
lookAhead $ string "]"
id <- endSpan start
return $ TC_Empty id typ
cpos <- getPosition
close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here (don't wrap commands in []/[[]])"
id <- endSpan start
when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Did you mean ]] ?"
when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?"
spacing
many readCmdWord -- Read and throw away remainders to get then/do warnings. Fixme?
return $ T_Condition id typ condition
readAnnotationPrefix = do
char '#'
many linewhitespace
string "shellcheck"
-- |
-- >>> prop $ isOk readAnnotation "# shellcheck disable=1234,5678\n"
-- >>> prop $ isOk readAnnotation "# shellcheck disable=SC1234 disable=SC5678\n"
-- >>> prop $ isOk readAnnotation "# shellcheck disable=SC1234 source=/dev/null disable=SC5678\n"
-- >>> prop $ isWarning readAnnotation "# shellcheck cats=dogs disable=SC1234\n"
-- >>> prop $ isOk readAnnotation "# shellcheck disable=SC2002 # All cats are precious\n"
-- >>> prop $ isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\n"
readAnnotation = called "shellcheck directive" $ do
try readAnnotationPrefix
many1 linewhitespace
values <- many1 readKey
optional readAnyComment
void linefeed <|> do
parseNote ErrorC 1125 "Invalid key=value pair? Ignoring the rest of this directive starting here."
many (noneOf "\n")
void linefeed <|> eof
many linewhitespace
return $ concat values
where
readKey = do
keyPos <- getPosition
key <- many1 letter
char '=' <|> fail "Expected '=' after directive key"
annotations <- case key of
"disable" -> readCode `sepBy` char ','
where
readCode = do
optional $ string "SC"
int <- many1 digit
return $ DisableComment (read int)
"source" -> do
filename <- many1 $ noneOf " \n"
return [SourceOverride filename]
"shell" -> do
pos <- getPosition
shell <- many1 $ noneOf " \n"
when (isNothing $ shellForExecutable shell) $
parseNoteAt pos ErrorC 1103
"This shell type is unknown. Use e.g. sh or bash."
return [ShellOverride shell]
_ -> do
parseNoteAt keyPos WarningC 1107 "This directive is unknown. It will be ignored."
anyChar `reluctantlyTill` whitespace
return []
many linewhitespace
return annotations
readAnnotations = do
annotations <- many (readAnnotation `thenSkip` allspacing)
return $ concat annotations
readComment = do
unexpecting "shellcheck annotation" readAnnotationPrefix
readAnyComment
readAnyComment = do
char '#'
many $ noneOf "\r\n"
-- |
-- >>> prop $ isOk readNormalWord "'foo'\"bar\"{1..3}baz$(lol)"
-- >>> prop $ isOk readNormalWord "foo**(foo)!!!(@@(bar))"
-- >>> prop $ isOk readNormalWord "foo#"
-- >>> prop $ isOk readNormalWord "$\"foo\"$'foo\nbar'"
-- >>> prop $ isWarning readNormalWord "${foo}}"
-- >>> prop $ isOk readNormalWord "foo/{}"
-- >>> prop $ isOk readNormalWord "foo\\\nbar"
-- >>> prop $ isWarning readSubshell "(foo\\ \nbar)"
-- >>> prop $ isOk readSubshell "(foo\\ ;\nbar)"
-- >>> prop $ isWarning readNormalWord "\x201Chello\x201D"
-- >>> prop $ isWarning readNormalWord "\x2018hello\x2019"
-- >>> prop $ isWarning readNormalWord "hello\x2018"
readNormalWord = readNormalishWord ""
readNormalishWord end = do
start <- startSpan
pos <- getPosition
x <- many1 (readNormalWordPart end)
id <- endSpan start
checkPossibleTermination pos x
return $ T_NormalWord id x
readIndexSpan = do
start <- startSpan
x <- many (readNormalWordPart "]" <|> someSpace <|> otherLiteral)
id <- endSpan start
return $ T_NormalWord id x
where
someSpace = do
start <- startSpan
str <- spacing1
id <- endSpan start
return $ T_Literal id str
otherLiteral = do
start <- startSpan
str <- many1 $ oneOf quotableChars
id <- endSpan start
return $ T_Literal id str
checkPossibleTermination pos [T_Literal _ x] =
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,
readDoubleQuoted,
readGlob,
readNormalDollar,
readBraced,
readUnquotedBackTicked,
readProcSub,
readUnicodeQuote,
readNormalLiteral end,
readLiteralCurlyBraces
]
where
checkForParenthesis =
return () `attempting` do
pos <- getPosition
lookAhead $ char '('
parseProblemAt pos ErrorC 1036 "'(' is invalid here. Did you forget to escape it?"
readLiteralCurlyBraces = do
start <- startSpan
str <- findParam <|> literalBraces
id <- endSpan start
return $ T_Literal id str
findParam = try $ string "{}"
literalBraces = do
pos <- getPosition
c <- oneOf "{}"
parseProblemAt pos WarningC 1083 $
"This " ++ [c] ++ " is literal. Check expression (missing ;/\\n?) or quote it."
return [c]
readSpacePart = do
start <- startSpan
x <- many1 whitespace
id <- endSpan start
return $ T_Literal id x
readDollarBracedWord = do
start <- startSpan
list <- many readDollarBracedPart
id <- endSpan start
return $ T_NormalWord id list
readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|>
readParamSubSpecialChar <|> readExtglob <|> readNormalDollar <|>
readUnquotedBackTicked <|> readDollarBracedLiteral
readDollarBracedLiteral = do
start <- startSpan
vars <- (readBraceEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` bracedQuotable
id <- endSpan start
return $ T_Literal id $ concat vars
readParamSubSpecialChar = do
start <- startSpan
x <- many1 paramSubSpecialChars
id <- endSpan start
return $ T_ParamSubSpecialChar id x
-- |
-- >>> prop $ isOk readProcSub "<(echo test | wc -l)"
-- >>> prop $ isOk readProcSub "<( if true; then true; fi )"
-- >>> prop $ isOk readProcSub "<( # nothing here \n)"
readProcSub = called "process substitution" $ do
start <- startSpan
dir <- try $ do
x <- oneOf "<>"
char '('
return [x]
list <- readCompoundListOrEmpty
allspacing
char ')'
id <- endSpan start
return $ T_ProcSub id dir list
-- |
-- >>> prop $ isOk readSingleQuoted "'foo bar'"
-- >>> prop $ isWarning readSingleQuoted "'foo bar\\'"
-- >>> prop $ isWarning readNormalWord "'it's"
-- >>> prop $ isWarning readSimpleCommand "foo='bar\ncow 'arg"
-- >>> prop $ isOk readSimpleCommand "foo='bar cow 'arg"
-- >>> prop $ isOk readSingleQuoted "'foo\x201C\&bar'"
-- >>> prop $ isWarning readSingleQuoted "'foo\x2018\&bar'"
readSingleQuoted = called "single quoted string" $ do
start <- startSpan
startPos <- getPosition
singleQuote
s <- many readSingleQuotedPart
let string = concat s
endPos <- getPosition
singleQuote <|> fail "Expected end of 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"
id <- endSpan start
return (T_SingleQuoted id string)
readSingleQuotedLiteral = do
singleQuote
strs <- many1 readSingleQuotedPart
singleQuote
return $ concat strs
readSingleQuotedPart =
readSingleEscaped
<|> many1 (noneOf $ "'\\" ++ unicodeSingleQuotes)
<|> readUnicodeQuote
where
readUnicodeQuote = do
pos <- getPosition
x <- oneOf unicodeSingleQuotes
parseProblemAt pos WarningC 1112
"This is a unicode quote. Delete and retype it (or ignore/doublequote for literal)."
return [x]
-- |
-- >>> prop $ isOk (readBackTicked False) "`ls *.mp3`"
-- >>> prop $ isOk (readBackTicked False) "`grep \"\\\"\"`"
-- >>> prop $ isWarning (readBackTicked False) "´grep \"\\\"\"´"
-- >>> prop $ isOk readSimpleCommand "`echo foo\necho bar`"
-- >>> prop $ isOk readSimpleCommand "echo `foo`bar"
-- >>> prop $ isWarning readSimpleCommand "echo `foo\necho `bar"
-- >>> prop $ isOk readSimpleCommand "`#inline comment`"
-- >>> prop $ isOk readSimpleCommand "echo `#comment` \\\nbar baz"
readQuotedBackTicked = readBackTicked True
readUnquotedBackTicked = readBackTicked False
readBackTicked quoted = called "backtick expansion" $ do
start <- startSpan
startPos <- getPosition
backtick
subStart <- getPosition
subString <- readGenericLiteral "`´"
endPos <- getPosition
backtick
id <- endSpan start
optional $ do
c <- try . lookAhead $ suspectCharAfterQuotes
when ('\n' `elem` subString && not ("\n" `isPrefixOf` subString)) $
suggestForgotClosingQuote startPos endPos "backtick expansion"
-- Result positions may be off due to escapes
result <- subParse subStart subParser (unEscape subString)
return $ T_Backticked id result
where
unEscape [] = []
unEscape ('\\':'"':rest) | quoted = '"' : unEscape rest
unEscape ('\\':x:rest) | x `elem` "$`\\" = x : unEscape rest
unEscape ('\\':'\n':rest) = unEscape rest
unEscape (c:rest) = c : unEscape rest
subParser = do
cmds <- readCompoundListOrEmpty
verifyEof
return cmds
backtick =
void (char '`') <|> do
pos <- getPosition
char '´'
parseProblemAt pos ErrorC 1077
"For command expansion, the tick should slant left (` vs ´). Use $(..) instead."
-- Run a parser on a new input, such as for `..` or here documents.
subParse pos parser input = do
lastPosition <- getPosition
lastInput <- getInput
setPosition pos
setInput input
result <- parser
setInput lastInput
setPosition lastPosition
return result
-- Parse something, but forget all parseProblems
inSeparateContext = parseForgettingContext True
-- Parse something, but forget all parseProblems on failure
forgetOnFailure = parseForgettingContext False
parseForgettingContext alsoOnSuccess parser = do
context <- Ms.get
success context <|> failure context
where
success c = do
res <- try parser
when alsoOnSuccess $ Ms.put c
return res
failure c = do
Ms.put c
fail ""
-- |
-- >>> prop $ isOk readDoubleQuoted "\"Hello $FOO\""
-- >>> prop $ isOk readDoubleQuoted "\"$'\""
-- >>> prop $ isOk readDoubleQuoted "\"\x2018hello\x2019\""
-- >>> prop $ isWarning readSimpleCommand "\"foo\nbar\"foo"
-- >>> prop $ isOk readSimpleCommand "lol \"foo\nbar\" etc"
-- >>> prop $ isOk readSimpleCommand "echo \"${ ls; }\""
-- >>> prop $ isOk readSimpleCommand "echo \"${ ls;}bar\""
-- >>> prop $ isWarning readDoubleQuoted "\"\x201Chello\x201D\""
-- >>> prop $ isOk readDoubleQuoted "\"foo\\\\n\""
readDoubleQuoted = called "double quoted string" $ do
start <- startSpan
startPos <- getPosition
doubleQuote
x <- many doubleQuotedPart
endPos <- getPosition
doubleQuote <|> fail "Expected end of double quoted string"
id <- endSpan start
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 <|> readQuotedBackTicked <|> readUnicodeQuote
where
readUnicodeQuote = do
pos <- getPosition
start <- startSpan
c <- oneOf unicodeDoubleQuotes
id <- endSpan start
parseProblemAt pos WarningC 1111
"This is a unicode quote. Delete and retype it (or ignore/singlequote for literal)."
return $ T_Literal id [c]
readDoubleLiteral = do
start <- startSpan
s <- many1 readDoubleLiteralPart
id <- endSpan start
return $ T_Literal id (concat s)
readDoubleLiteralPart = do
x <- many1 (readDoubleEscaped <|> many1 (noneOf (doubleQuotableChars ++ unicodeDoubleQuotes)))
return $ concat x
readNormalLiteral end = do
start <- startSpan
s <- many1 (readNormalLiteralPart end)
id <- endSpan start
return $ T_Literal id (concat s)
-- |
-- >>> prop $ isOk readGlob "*"
-- >>> prop $ isOk readGlob "[^0-9]"
-- >>> prop $ isOk readGlob "[a[:alpha:]]"
-- >>> prop $ isOk readGlob "[[:alnum:]]"
-- >>> prop $ isOk readGlob "[^[:alpha:]1-9]"
-- >>> prop $ isOk readGlob "[\\|]"
-- >>> prop $ isOk readGlob "[^[]"
-- >>> prop $ isOk readGlob "[*?]"
readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
where
readSimple = do
start <- startSpan
c <- oneOf "*?"
id <- endSpan start
return $ T_Glob id [c]
-- Doesn't handle weird things like [^]a] and [$foo]. fixme?
readClass = try $ do
start <- startSpan
char '['
s <- many1 (predefined <|> readNormalLiteralPart "]" <|> globchars)
char ']'
id <- endSpan start
return $ T_Glob id $ "[" ++ concat s ++ "]"
where
globchars = fmap return . oneOf $ "!$[" ++ extglobStartChars
predefined = do
try $ string "[:"
s <- many1 letter
string ":]"
return $ "[:" ++ s ++ ":]"
readGlobbyLiteral = do
start <- startSpan
c <- extglobStart <|> char '['
id <- endSpan start
return $ T_Literal id [c]
readNormalLiteralPart customEnd =
readNormalEscaped <|>
many1 (noneOf (customEnd ++ standardEnd))
where
standardEnd = "[{}"
++ quotableChars
++ extglobStartChars
++ unicodeDoubleQuotes
++ unicodeSingleQuotes
readNormalEscaped = called "escaped char" $ do
pos <- getPosition
backslash
do
next <- quotable <|> oneOf "?*@!+[]{}.,~#"
when (next == ' ') $ checkTrailingSpaces pos <|> return ()
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."
Nothing -> parseNoteAt pos InfoC 1001 $ "This \\" ++ [next] ++ " will be a regular '" ++ [next] ++ "' in this context."
return [next]
where
alternative 'n' = "a quoted, literal line feed"
alternative t = "\"$(printf '\\" ++ [t] ++ "')\""
escapedChar 'n' = Just "line feed"
escapedChar 't' = Just "tab"
escapedChar 'r' = Just "carriage return"
escapedChar _ = Nothing
checkTrailingSpaces pos = lookAhead . try $ do
many linewhitespace
void linefeed <|> eof
parseProblemAt pos ErrorC 1101 "Delete trailing spaces after \\ to break line (or use quotes for literal space)."
-- |
-- >>> prop $ isOk readExtglob "!(*.mp3)"
-- >>> prop $ isOk readExtglob "!(*.mp3|*.wmv)"
-- >>> prop $ isOk readExtglob "+(foo \\) bar)"
-- >>> prop $ isOk readExtglob "+(!(foo *(bar)))"
-- >>> prop $ isOk readExtglob "*(((||))|())"
-- >>> prop $ isOk readExtglob "*(<>)"
-- >>> prop $ isOk readExtglob "@(|*())"
readExtglob = called "extglob" $ do
start <- startSpan
c <- try $ do
f <- extglobStart
char '('
return f
contents <- readExtglobPart `sepBy` char '|'
char ')'
id <- endSpan start
return $ T_Extglob id [c] contents
readExtglobPart = do
start <- startSpan
x <- many (readExtglobGroup <|> readNormalWordPart "" <|> readSpacePart <|> readExtglobLiteral)
id <- endSpan start
return $ T_NormalWord id x
where
readExtglobGroup = do
char '('
start <- startSpan
contents <- readExtglobPart `sepBy` char '|'
id <- endSpan start
char ')'
return $ T_Extglob id "" contents
readExtglobLiteral = do
start <- startSpan
str <- many1 (oneOf "<>#;&")
id <- endSpan start
return $ T_Literal id str
readSingleEscaped = do
pos <- getPosition
s <- backslash
x <- lookAhead anyChar
case x of
'\'' -> parseProblemAt pos InfoC 1003 "Want to escape a single quote? echo 'This is how it'\\''s done'.";
'\n' -> parseProblemAt pos InfoC 1004 "This backslash+linefeed is literal. Break outside single quotes if you just want to break the line."
_ -> return ()
return [s]
readDoubleEscaped = do
pos <- getPosition
bs <- backslash
(linefeed >> return "")
<|> fmap return doubleQuotable
<|> do
c <- anyChar
-- This is an invalid escape sequence where the \ is literal.
-- Previously this caused a SC1117, which may be re-enabled as
-- as a pedantic warning.
return [bs, c]
readBraceEscaped = do
bs <- backslash
(linefeed >> return "")
<|> fmap return bracedQuotable
<|> fmap (\ x -> [bs, x]) anyChar
readGenericLiteral endChars = do
strings <- many (readGenericEscaped <|> many1 (noneOf ('\\':endChars)))
return $ concat strings
readGenericLiteral1 endExp = do
strings <- (readGenericEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` endExp
return $ concat strings
readGenericEscaped = do
backslash
x <- anyChar
return $ if x == '\n' then [] else ['\\', x]
-- |
-- >>> prop $ isOk readBraced "{1..4}"
-- >>> prop $ isOk readBraced "{foo,bar,\"baz lol\"}"
-- >>> prop $ isOk readBraced "{1,\\},2}"
-- >>> prop $ isOk readBraced "{1,{2,3}}"
-- >>> prop $ isOk readBraced "{JP{,E}G,jp{,e}g}"
-- >>> prop $ isOk readBraced "{foo,bar,$((${var}))}"
-- >>> prop $ isNotOk readBraced "{}"
-- >>> prop $ isNotOk readBraced "{foo}"
readBraced = try braceExpansion
where
braceExpansion =
T_BraceExpansion `withParser` do
char '{'
elements <- bracedElement `sepBy1` char ','
guard $
case elements of
(_:_:_) -> True
[t] -> ".." `isInfixOf` onlyLiteralString t
[] -> False
char '}'
return elements
bracedElement =
T_NormalWord `withParser` do
many $ choice [
braceExpansion,
readDollarExpression,
readSingleQuoted,
readDoubleQuoted,
braceLiteral
]
braceLiteral =
T_Literal `withParser` readGenericLiteral1 (oneOf "{}\"$'," <|> whitespace)
ensureDollar =
-- The grammar should have been designed along the lines of readDollarExpr = char '$' >> stuff, but
-- instead, each subunit parses its own $. This results in ~7 1-3 char lookaheads instead of one 1-char.
-- Instead of optimizing the grammar, here's a green cut that decreases shellcheck runtime by 10%:
lookAhead $ char '$'
readNormalDollar = do
ensureDollar
readDollarExp <|> readDollarDoubleQuote <|> readDollarSingleQuote <|> readDollarLonely
readDoubleQuotedDollar = do
ensureDollar
readDollarExp <|> readDollarLonely
-- |
-- >>> prop $ isOk readDollarExpression "$(((1) && 3))"
-- >>> prop $ isWarning readDollarExpression "$(((1)) && 3)"
-- >>> prop $ isWarning readDollarExpression "$((\"$@\" &); foo;)"
readDollarExpression :: Monad m => SCParser m Token
readDollarExpression = do
ensureDollar
readDollarExp
readDollarExp = arithmetic <|> readDollarExpansion <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarVariable
where
arithmetic = readAmbiguous "$((" readDollarArithmetic readDollarExpansion (\pos ->
parseNoteAt pos WarningC 1102 "Shells disambiguate $(( differently or not at all. For $(command substition), add space after $( . For $((arithmetics)), fix parsing errors.")
-- |
-- >>> prop $ isOk readDollarSingleQuote "$'foo\\\'lol'"
readDollarSingleQuote = called "$'..' expression" $ do
start <- startSpan
try $ string "$'"
str <- readGenericLiteral "'"
char '\''
id <- endSpan start
return $ T_DollarSingleQuoted id str
-- |
-- >>> prop $ isOk readDollarDoubleQuote "$\"hello\""
readDollarDoubleQuote = do
lookAhead . try $ string "$\""
start <- startSpan
char '$'
doubleQuote
x <- many doubleQuotedPart
doubleQuote <|> fail "Expected end of translated double quoted string"
id <- endSpan start
return $ T_DollarDoubleQuoted id x
-- |
-- >>> prop $ isOk readDollarArithmetic "$(( 3 * 4 +5))"
-- >>> prop $ isOk readDollarArithmetic "$(((3*4)+(1*2+(3-1))))"
readDollarArithmetic = called "$((..)) expression" $ do
start <- startSpan
try (string "$((")
c <- readArithmeticContents
pos <- getPosition
char ')'
char ')' <|> fail "Expected a double )) to end the $((..))"
id <- endSpan start
return (T_DollarArithmetic id c)
readDollarBracket = called "$[..] expression" $ do
start <- startSpan
try (string "$[")
c <- readArithmeticContents
string "]"
id <- endSpan start
return (T_DollarBracket id c)
-- |
-- >>> prop $ isOk readArithmeticExpression "((a?b:c))"
readArithmeticExpression = called "((..)) command" $ do
start <- startSpan
try (string "((")
c <- readArithmeticContents
string "))"
id <- endSpan start
return (T_Arithmetic id c)
-- If the next characters match prefix, try two different parsers and warn if the alternate parser had to be used
readAmbiguous :: Monad m => String -> SCParser m p -> SCParser m p -> (SourcePos -> SCParser m ()) -> SCParser m p
readAmbiguous prefix expected alternative warner = do
pos <- getPosition
try . lookAhead $ string prefix
-- If the expected parser fails, try the alt.
-- If the alt fails, run the expected one again for the errors.
try expected <|> try (withAlt pos) <|> expected
where
withAlt pos = do
t <- forgetOnFailure alternative
warner pos
return t
-- |
-- >>> prop $ isOk readDollarBraceCommandExpansion "${ ls; }"
-- >>> prop $ isOk readDollarBraceCommandExpansion "${\nls\n}"
readDollarBraceCommandExpansion = called "ksh ${ ..; } command expansion" $ do
start <- startSpan
try $ do
string "${"
whitespace
allspacing
term <- readTerm
char '}' <|> fail "Expected } to end the ksh ${ ..; } command expansion"
id <- endSpan start
return $ T_DollarBraceCommandExpansion id term
-- |
-- >>> prop $ isOk readDollarBraced "${foo//bar/baz}"
-- >>> prop $ isOk readDollarBraced "${foo/'{cow}'}"
-- >>> prop $ isOk readDollarBraced "${foo%%$(echo cow\\})}"
-- >>> prop $ isOk readDollarBraced "${foo#\\}}"
readDollarBraced = called "parameter expansion" $ do
start <- startSpan
try (string "${")
word <- readDollarBracedWord
char '}'
id <- endSpan start
return $ T_DollarBraced id word
-- |
-- >>> prop $ isOk readDollarExpansion "$(echo foo; ls\n)"
-- >>> prop $ isOk readDollarExpansion "$( )"
-- >>> prop $ isOk readDollarExpansion "$( command \n#comment \n)"
readDollarExpansion = called "command expansion" $ do
start <- startSpan
try (string "$(")
cmds <- readCompoundListOrEmpty
char ')' <|> fail "Expected end of $(..) expression"
id <- endSpan start
return $ T_DollarExpansion id cmds
-- |
-- >>> prop $ isOk readDollarVariable "$@"
-- >>> prop $ isOk (readDollarVariable >> anyChar) "$?!"
-- >>> prop $ isWarning (readDollarVariable >> anyChar) "$10"
-- >>> prop $ isWarning (readDollarVariable >> string "[@]") "$arr[@]"
-- >>> prop $ isWarning (readDollarVariable >> string "[f") "$arr[f"
readDollarVariable :: Monad m => SCParser m Token
readDollarVariable = do
start <- startSpan
pos <- getPosition
let singleCharred p = do
value <- wrapString ((:[]) <$> p)
id <- endSpan start
return $ (T_DollarBraced id value)
let positional = do
value <- singleCharred digit
return value `attempting` do
lookAhead digit
parseNoteAt pos ErrorC 1037 "Braces are required for positionals over 9, e.g. ${10}."
let special = singleCharred specialVariable
let regular = do
value <- wrapString readVariableName
id <- endSpan start
return (T_DollarBraced id value) `attempting` do
lookAhead $ char '['
parseNoteAt pos ErrorC 1087 "Use braces when expanding arrays, e.g. ${array[idx]} (or ${var}[.. to quiet)."
try $ char '$' >> (positional <|> special <|> regular)
where
wrapString p = do
start <- getPosition
s <- p
end <- getPosition
id1 <- getNextIdBetween start end
id2 <- getNextIdBetween start end
return $ T_NormalWord id1 [T_Literal id2 s]
readVariableName = do
f <- variableStart
rest <- many variableChars
return (f:rest)
readDollarLonely = do
start <- startSpan
char '$'
id <- endSpan start
n <- lookAhead (anyChar <|> (eof >> return '_'))
return $ T_Literal id "$"
-- |
-- >>> prop $ isOk readScript "cat << foo\nlol\ncow\nfoo"
-- >>> prop $ isNotOk readScript "cat <<- EOF\n cow\n EOF"
-- >>> prop $ isOk readScript "cat << foo\n$\"\nfoo"
-- >>> prop $ isNotOk readScript "cat << foo\n`\nfoo"
-- >>> prop $ isOk readScript "cat <<- !foo\nbar\n!foo"
-- >>> prop $ isOk readScript "cat << foo\\ bar\ncow\nfoo bar"
-- >>> prop $ isOk readScript "cat << foo\n\\$(f ())\nfoo"
-- >>> prop $ isOk readScript "cat <<foo>>bar\netc\nfoo"
-- >>> prop $ isOk readScript "if true; then cat << foo; fi\nbar\nfoo\n"
-- >>> prop $ isOk readScript "if true; then cat << foo << bar; fi\nfoo\nbar\n"
-- >>> prop $ isOk readScript "cat << foo $(\nfoo\n)lol\nfoo\n"
-- >>> prop $ isOk readScript "cat << foo|cat\nbar\nfoo"
-- >>> prop $ isOk readScript "cat <<'#!'\nHello World\n#!\necho Done"
-- >>> prop $ isWarning readScript "cat << foo\nbar\nfoo \n"
-- >>> prop $ isWarning readScript "cat <<foo\nbar\nfoo bar\nfoo"
-- >>> prop $ isOk readScript "cat <<- ' foo'\nbar\n foo\n"
-- >>> prop $ isWarning readScript "cat <<- ' foo'\nbar\n foo\n foo\n"
-- >>> prop $ isWarning readScript "cat << foo\n foo\n()\nfoo\n"
-- >>> prop $ isOk readScript "# shellcheck disable=SC1039\ncat << foo\n foo\n()\nfoo\n"
readHereDoc = called "here document" $ do
pos <- getPosition
try $ string "<<"
dashed <- (char '-' >> return Dashed) <|> return Undashed
sp <- spacing
optional $ do
try . lookAhead $ char '('
let message = "Shells are space sensitive. Use '< <(cmd)', not '<<" ++ sp ++ "(cmd)'."
parseProblemAt pos ErrorC 1038 message
start <- startSpan
(quoted, endToken) <- readToken
hid <- endSpan start
-- add empty tokens for now, read the rest in readPendingHereDocs
let doc = T_HereDoc hid dashed quoted endToken []
addPendingHereDoc doc
return doc
where
quotes = "\"'\\"
-- Fun fact: bash considers << foo"" quoted, but not << <("foo").
-- Instead of replicating this, just read a token and strip quotes.
readToken = do
str <- readStringForParser readNormalWord
return (if any (`elem` quotes) str then Quoted else Unquoted,
filter (not . (`elem` quotes)) str)
readPendingHereDocs = do
docs <- popPendingHereDocs
mapM_ readDoc docs
where
readDoc (HereDocPending (T_HereDoc id dashed quoted endToken _) ctx) =
swapContext ctx $
do
docStartPos <- getPosition
(terminated, wasWarned, lines) <- readDocLines dashed endToken
docEndPos <- getPosition
let hereData = unlines lines
unless terminated $ do
unless wasWarned $
debugHereDoc id endToken hereData
fail "Here document was not correctly terminated"
list <- parseHereData quoted (docStartPos, docEndPos) hereData
addToHereDocMap id list
-- Read the lines making up the here doc. Returns (IsTerminated, Lines)
readDocLines :: Monad m => Dashed -> String -> SCParser m (Bool, Bool, [String])
readDocLines dashed endToken = do
pos <- getPosition
str <- rawLine
isEof <- option False (eof >> return True)
(isEnd, wasWarned) <- subParse pos checkEnd str
if
| isEnd -> return (True, wasWarned, [])
| isEof -> return (False, wasWarned, [str])
| True -> do
(ok, previousWarning, rest) <- readDocLines dashed endToken
return (ok, wasWarned || previousWarning, str:rest)
where
-- Check if this is the actual end, or a plausible false end
checkEnd = option (False, False) $ try $ do
-- Match what's basically '^( *)token( *)(.*)$'
leadingSpacePos <- getPosition
leadingSpace <- linewhitespace `reluctantlyTill` string endToken
string endToken
trailingSpacePos <- getPosition
trailingSpace <- many linewhitespace
trailerPos <- getPosition
trailer <- many anyChar
let leadingSpacesAreTabs = all (== '\t') leadingSpace
let thereIsNoTrailer = null trailingSpace && null trailer
let leaderIsOk = null leadingSpace
|| dashed == Dashed && leadingSpacesAreTabs
let trailerStart = if null trailer then '\0' else head trailer
let hasTrailingSpace = not $ null trailingSpace
let hasTrailer = not $ null trailer
let ppt = parseProblemAt trailerPos ErrorC
if leaderIsOk && thereIsNoTrailer
then return (True, False)
else do
let foundCause = return (False, True)
let skipLine = return (False, False)
-- This may be intended as an end token. Debug why it isn't.
if
| trailerStart == ')' -> do
ppt 1119 $ "Add a linefeed between end token and terminating ')'."
foundCause
| trailerStart == '#' -> do
ppt 1120 "No comments allowed after here-doc token. Comment the next line instead."
foundCause
| trailerStart `elem` ";>|&" -> do
ppt 1121 "Add ;/& terminators (and other syntax) on the line with the <<, not here."
foundCause
| hasTrailingSpace && hasTrailer -> do
ppt 1122 "Nothing allowed after end token. To continue a command, put it on the line with the <<."
foundCause
| leaderIsOk && hasTrailingSpace && not hasTrailer -> do
parseProblemAt trailingSpacePos ErrorC 1118 "Delete whitespace after the here-doc end token."
-- Parse as if it's the actual end token. Will koala_man regret this once again?
return (True, True)
| not hasTrailingSpace && hasTrailer ->
-- The end token is just a prefix
skipLine
| hasTrailer ->
error "ShellCheck bug, please report (here doc trailer)."
-- The following cases assume no trailing text:
| dashed == Undashed && (not $ null leadingSpace) -> do
parseProblemAt leadingSpacePos ErrorC 1039 "Remove indentation before end token (or use <<- and indent with tabs)."
foundCause
| dashed == Dashed && not leadingSpacesAreTabs -> do
parseProblemAt leadingSpacePos ErrorC 1040 "When using <<-, you can only indent with tabs."
foundCause
| True -> skipLine
rawLine = do
c <- many $ noneOf "\n"
void (char '\n') <|> eof
return c
parseHereData Quoted (start,end) hereData = do
id <- getNextIdBetween start end
return [T_Literal id hereData]
parseHereData Unquoted (startPos, _) hereData =
subParse startPos readHereData hereData
readHereData = many $ doubleQuotedPart <|> readHereLiteral
readHereLiteral = do
start <- startSpan
chars <- many1 $ noneOf "`$\\"
id <- endSpan start
return $ T_Literal id chars
debugHereDoc tokenId endToken doc
| endToken `isInfixOf` doc =
let lookAt line = when (endToken `isInfixOf` line) $
parseProblemAtId tokenId ErrorC 1042 ("Close matches include '" ++ line ++ "' (!= '" ++ endToken ++ "').")
in do
parseProblemAtId tokenId ErrorC 1041 ("Found '" ++ endToken ++ "' further down, but not on a separate line.")
mapM_ lookAt (lines doc)
| map toLower endToken `isInfixOf` map toLower doc =
parseProblemAtId tokenId ErrorC 1043 ("Found " ++ endToken ++ " further down, but with wrong casing.")
| otherwise =
parseProblemAtId tokenId ErrorC 1044 ("Couldn't find end token `" ++ endToken ++ "' in the here document.")
readFilename = readNormalWord
readIoFileOp = choice [g_DGREAT, g_LESSGREAT, g_GREATAND, g_LESSAND, g_CLOBBER, redirToken '<' T_Less, redirToken '>' T_Greater ]
readIoDuplicate = try $ do
start <- startSpan
op <- g_GREATAND <|> g_LESSAND
target <- readIoVariable <|> digitsAndOrDash
id <- endSpan start
return $ T_IoDuplicate id op target
where
-- either digits with optional dash, or a required dash
digitsAndOrDash = do
str <- many digit
dash <- (if null str then id else option "") $ string "-"
return $ str ++ dash
-- |
-- >>> prop $ isOk readIoFile ">> \"$(date +%YYmmDD)\""
readIoFile = called "redirection" $ do
start <- startSpan
op <- readIoFileOp
spacing
file <- readFilename
id <- endSpan start
return $ T_IoFile id op file
readIoVariable = try $ do
char '{'
x <- readVariableName
char '}'
return $ "{" ++ x ++ "}"
readIoSource = try $ do
x <- string "&" <|> readIoVariable <|> many digit
lookAhead $ void readIoFileOp <|> void (string "<<")
return x
-- |
-- >>> prop $ isOk readIoRedirect "3>&2"
-- >>> prop $ isOk readIoRedirect "2> lol"
-- >>> prop $ isOk readIoRedirect "4>&-"
-- >>> prop $ isOk readIoRedirect "&> lol"
-- >>> prop $ isOk readIoRedirect "{foo}>&2"
-- >>> prop $ isOk readIoRedirect "{foo}<&-"
-- >>> prop $ isOk readIoRedirect "{foo}>&1-"
readIoRedirect = do
start <- startSpan
n <- readIoSource
redir <- readHereString <|> readHereDoc <|> readIoDuplicate <|> readIoFile
id <- endSpan start
skipAnnotationAndWarn
spacing
return $ T_FdRedirect id n redir
readRedirectList = many1 readIoRedirect
-- |
-- >>> prop $ isOk readHereString "<<< \"Hello $world\""
readHereString = called "here string" $ do
start <- startSpan
try $ string "<<<"
id <- endSpan start
spacing
word <- readNormalWord
return $ T_HereString id word
readNewlineList =
many1 ((linefeed <|> carriageReturn) `thenSkip` spacing) <* checkBadBreak
where
checkBadBreak = optional $ do
pos <- getPosition
try $ lookAhead (oneOf "|&") -- See if the next thing could be |, || or &&
parseProblemAt pos ErrorC 1133
"Unexpected start of line. If breaking lines, |/||/&& should be at the end of the previous one."
readLineBreak = optional readNewlineList
-- |
-- >>> prop $ isWarning readScript "a &; b"
-- >>> prop $ isOk readScript "a & b"
-- >>> prop $ isWarning readScript "a &amp; b"
-- >>> prop $ isWarning readScript "a &gt; file; b"
-- >>> prop $ isWarning readScript "curl https://example.com/?foo=moo&bar=cow"
readSeparatorOp = do
notFollowedBy2 (void g_AND_IF <|> void readCaseSeparator)
notFollowedBy2 (string "&>")
start <- getPosition
f <- try (do
pos <- getPosition
char '&'
optional $ choice [
do
s <- lookAhead . choice . map (try . string) $
["amp;", "gt;", "lt;"]
parseProblemAt pos ErrorC 1109 "This is an unquoted HTML entity. Replace with corresponding character.",
do
try . lookAhead $ variableStart
parseProblemAt pos WarningC 1132 "This & terminates the command. Escape it or add space after & to silence."
]
spacing
pos <- getPosition
char ';'
-- In case statements we might have foo & ;;
notFollowedBy2 $ char ';'
parseProblemAt pos ErrorC 1045 "It's not 'foo &; bar', just 'foo & bar'."
return '&'
) <|> char ';' <|> char '&'
end <- getPosition
spacing
return (f, (start, end))
readSequentialSep = void (g_Semi >> readLineBreak) <|> void readNewlineList
readSeparator =
do
separator <- readSeparatorOp
readLineBreak
return separator
<|>
do
start <- getPosition
readNewlineList
end <- getPosition
return ('\n', (start, end))
-- |
-- >>> prop $ isOk readSimpleCommand "echo test > file"
-- >>> prop $ isOk readSimpleCommand "cmd &> file"
-- >>> prop $ isOk readSimpleCommand "export foo=(bar baz)"
-- >>> prop $ isOk readSimpleCommand "typeset -a foo=(lol)"
-- >>> prop $ isOk readSimpleCommand "time if true; then echo foo; fi"
-- >>> prop $ isOk readSimpleCommand "time -p ( ls -l; )"
-- >>> prop $ isOk readSimpleCommand "\\ls"
-- >>> prop $ isWarning readSimpleCommand "// Lol"
-- >>> prop $ isWarning readSimpleCommand "/* Lolbert */"
-- >>> prop $ isWarning readSimpleCommand "/**** Lolbert */"
-- >>> prop $ isOk readSimpleCommand "/\\* foo"
-- >>> prop $ isWarning readSimpleCommand "elsif foo"
-- >>> prop $ isWarning readSimpleCommand "ElseIf foo"
-- >>> prop $ isWarning readSimpleCommand "elseif[$i==2]"
readSimpleCommand = called "simple command" $ do
prefix <- option [] readCmdPrefix
skipAnnotationAndWarn
cmd <- option Nothing $ Just <$> readCmdName
when (null prefix && isNothing cmd) $ fail "Expected a command"
case cmd of
Nothing -> do
id1 <- getNextIdSpanningTokenList prefix
id2 <- getNewIdFor id1
return $ makeSimpleCommand id1 id2 prefix [] []
Just cmd -> do
validateCommand cmd
suffix <- option [] $ getParser readCmdSuffix cmd [
(["declare", "export", "local", "readonly", "typeset"], readModifierSuffix),
(["time"], readTimeSuffix),
(["let"], readLetSuffix),
(["eval"], readEvalSuffix)
]
id1 <- getNextIdSpanningTokenList (prefix ++ (cmd:suffix))
id2 <- getNewIdFor id1
let result = makeSimpleCommand id1 id2 prefix [cmd] suffix
if isCommand ["source", "."] cmd
then readSource result
else return result
where
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
cStyleComment cmd =
case cmd of
_ -> False
validateCommand cmd =
case cmd of
(T_NormalWord _ [T_Literal _ "//"]) -> commentWarning (getId cmd)
(T_NormalWord _ (T_Literal _ "/" : T_Glob _ "*" :_)) -> commentWarning (getId cmd)
(T_NormalWord _ (T_Literal _ str:_)) -> do
let cmdString = map toLower $ takeWhile isAlpha str
when (cmdString `elem` ["elsif", "elseif"]) $
parseProblemAtId (getId cmd) ErrorC 1131 "Use 'elif' to start another branch."
_ -> return ()
commentWarning id =
parseProblemAtId id ErrorC 1127 "Was this intended as a comment? Use # in sh."
makeSimpleCommand id1 id2 prefix cmd suffix =
let
(preAssigned, preRest) = partition assignment prefix
(preRedirected, preRest2) = partition redirection preRest
(postRedirected, postRest) = partition redirection suffix
redirs = preRedirected ++ postRedirected
assigns = preAssigned
args = cmd ++ preRest2 ++ postRest
in
T_Redirecting id1 redirs $ T_SimpleCommand id2 assigns args
where
assignment (T_Assignment {}) = True
assignment _ = False
redirection (T_FdRedirect {}) = True
redirection _ = False
readSource :: Monad m => Token -> SCParser m Token
readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file:_))) = do
override <- getSourceOverride
let literalFile = do
name <- override `mplus` getLiteralString file
-- Hack to avoid 'source ~/foo' trying to read from literal tilde
guard . not $ "~/" `isPrefixOf` name
return name
case literalFile of
Nothing -> do
parseNoteAtId (getId file) WarningC 1090
"Can't follow non-constant source. Use a directive to specify location."
return t
Just filename -> do
proceed <- shouldFollow filename
if not proceed
then do
parseNoteAtId (getId file) InfoC 1093
"This file appears to be recursively sourced. Ignoring."
return t
else do
sys <- Mr.asks systemInterface
input <-
if filename == "/dev/null" -- always allow /dev/null
then return (Right "")
else system $ siReadFile sys filename
case input of
Left err -> do
parseNoteAtId (getId file) InfoC 1091 $
"Not following: " ++ err
return t
Right script -> do
id1 <- getNewIdFor cmdId
id2 <- getNewIdFor cmdId
let included = do
src <- subRead filename script
return $ T_SourceCommand id1 t (T_Include id2 src)
let failed = do
parseNoteAtId (getId file) WarningC 1094
"Parsing of sourced file failed. Ignoring it."
return t
included <|> failed
where
subRead name script =
withContext (ContextSource name) $
inSeparateContext $
subParse (initialPos name) readScript script
readSource t = return t
-- |
-- >>> prop $ isOk readPipeline "! cat /etc/issue | grep -i ubuntu"
-- >>> prop $ isWarning readPipeline "!cat /etc/issue | grep -i ubuntu"
-- >>> prop $ isOk readPipeline "for f; do :; done|cat"
readPipeline = do
unexpecting "keyword/token" readKeyword
do
(T_Bang id) <- g_Bang
pipe <- readPipeSequence
return $ T_Banged id pipe
<|>
readPipeSequence
-- |
-- >>> prop $ isOk readAndOr "grep -i lol foo || exit 1"
-- >>> prop $ isOk readAndOr "# shellcheck disable=1\nfoo"
-- >>> prop $ isOk readAndOr "# shellcheck disable=1\n# lol\n# shellcheck disable=3\nfoo"
readAndOr = do
start <- startSpan
apos <- getPosition
annotations <- readAnnotations
aid <- endSpan start
unless (null annotations) $ optional $ do
try . lookAhead $ readKeyword
parseProblemAt apos ErrorC 1123 "ShellCheck directives are only valid in front of complete compound commands, like 'if', not e.g. individual 'elif' branches."
andOr <- withAnnotations annotations $
chainr1 readPipeline $ do
op <- g_AND_IF <|> g_OR_IF
readLineBreak
return $ case op of T_AND_IF id -> T_AndIf id
T_OR_IF id -> T_OrIf id
return $ if null annotations
then andOr
else T_Annotation aid annotations andOr
readTermOrNone = do
allspacing
readTerm <|> do
eof
return []
-- |
-- >>> prop $ isOk readTerm "time ( foo; bar; )"
readTerm = do
allspacing
m <- readAndOr
readTerm' m
where
readTerm' current =
do
(sep, (start, end)) <- readSeparator
id <- getNextIdBetween start end
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 [current]
where
transformWithSeparator i '&' = T_Backgrounded i
transformWithSeparator i _ = id
readPipeSequence = do
start <- startSpan
(cmds, pipes) <- sepBy1WithSeparators readCommand
(readPipe `thenSkip` (spacing >> readLineBreak))
id <- endSpan start
spacing
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
start <- startSpan
char '|'
qualifier <- string "&" <|> return ""
id <- endSpan start
spacing
return $ T_Pipe id ('|':qualifier)
readCommand = choice [
readCompoundCommand,
readCoProc,
readSimpleCommand
]
readCmdName = do
-- Ignore alias suppression
optional . try $ do
char '\\'
lookAhead $ variableChars
readCmdWord
readCmdWord = do
skipAnnotationAndWarn
readNormalWord <* spacing
-- Due to poor planning, annotations after commands isn't handled well.
-- At the time this function is used, it's usually too late to skip
-- comments, so you end up with a parse failure instead.
skipAnnotationAndWarn = optional $ do
try . lookAhead $ readAnnotationPrefix
parseProblem ErrorC 1126 "Place shellcheck directives before commands, not after."
readAnyComment
-- |
-- >>> prop $ isOk readIfClause "if false; then foo; elif true; then stuff; more stuff; else cows; fi"
-- >>> prop $ isWarning readIfClause "if false; then; echo oo; fi"
-- >>> prop $ isWarning readIfClause "if false; then true; else; echo lol; fi"
-- >>> prop $ isWarning readIfClause "if false; then true; else if true; then echo lol; fi; fi"
-- >>> prop $ isOk readIfClause "if false; then true; else\nif true; then echo lol; fi; fi"
readIfClause = called "if expression" $ do
start <- startSpan
pos <- getPosition
(condition, action) <- readIfPart
elifs <- many readElifPart
elses <- option [] readElsePart
g_Fi `orFail` do
parseProblemAt pos ErrorC 1046 "Couldn't find 'fi' for this 'if'."
parseProblem ErrorC 1047 "Expected 'fi' matching previously mentioned 'if'."
return "Expected 'fi'"
id <- endSpan start
return $ T_IfExpression id ((condition, action):elifs) elses
verifyNotEmptyIf s =
optional (do
emptyPos <- getPosition
try . lookAhead $ (g_Fi <|> g_Elif <|> g_Else)
parseProblemAt emptyPos ErrorC 1048 $ "Can't have empty " ++ s ++ " clauses (use 'true' as a no-op).")
readIfPart = do
pos <- getPosition
g_If
allspacing
condition <- readTerm
ifNextToken (g_Fi <|> g_Elif <|> g_Else) $
parseProblemAt pos ErrorC 1049 "Did you forget the 'then' for this 'if'?"
called "then clause" $ do
g_Then `orFail` do
parseProblem ErrorC 1050 "Expected 'then'."
return "Expected 'then'"
acceptButWarn g_Semi ErrorC 1051 "Semicolons directly after 'then' are not allowed. Just remove it."
allspacing
verifyNotEmptyIf "then"
action <- readTerm
return (condition, action)
readElifPart = called "elif clause" $ do
pos <- getPosition
g_Elif
allspacing
condition <- readTerm
ifNextToken (g_Fi <|> g_Elif <|> g_Else) $
parseProblemAt pos ErrorC 1049 "Did you forget the 'then' for this 'elif'?"
g_Then
acceptButWarn g_Semi ErrorC 1052 "Semicolons directly after 'then' are not allowed. Just remove it."
allspacing
verifyNotEmptyIf "then"
action <- readTerm
return (condition, action)
readElsePart = called "else clause" $ do
pos <- getPosition
g_Else
optional $ do
try . lookAhead $ g_If
parseProblemAt pos ErrorC 1075 "Use 'elif' instead of 'else if' (or put 'if' on new line if nesting)."
acceptButWarn g_Semi ErrorC 1053 "Semicolons directly after 'else' are not allowed. Just remove it."
allspacing
verifyNotEmptyIf "else"
readTerm
ifNextToken parser action =
optional $ do
try . lookAhead $ parser
action
-- |
-- >>> prop $ isOk readSubshell "( cd /foo; tar cf stuff.tar * )"
readSubshell = called "explicit subshell" $ do
start <- startSpan
char '('
allspacing
list <- readCompoundList
allspacing
char ')' <|> fail "Expected ) closing the subshell"
id <- endSpan start
return $ T_Subshell id list
-- |
-- >>> prop $ isOk readBraceGroup "{ a; b | c | d; e; }"
-- >>> prop $ isWarning readBraceGroup "{foo;}"
-- >>> prop $ isOk readBraceGroup "{(foo)}"
readBraceGroup = called "brace group" $ do
start <- startSpan
char '{'
void allspacingOrFail <|> optional (do
lookAhead $ noneOf "(" -- {( is legal
parseProblem ErrorC 1054 "You need a space after the '{'.")
optional $ do
pos <- getPosition
lookAhead $ char '}'
parseProblemAt pos ErrorC 1055 "You need at least one command here. Use 'true;' as a no-op."
list <- readTerm
char '}' <|> do
parseProblem ErrorC 1056 "Expected a '}'. If you have one, try a ; or \\n in front of it."
fail "Missing '}'"
id <- endSpan start
return $ T_BraceGroup id list
-- |
-- >>> prop $ isOk readWhileClause "while [[ -e foo ]]; do sleep 1; done"
readWhileClause = called "while loop" $ do
start <- startSpan
kwId <- getId <$> g_While
condition <- readTerm
statements <- readDoGroup kwId
id <- endSpan start
return $ T_WhileExpression id condition statements
-- |
-- >>> prop $ isOk readUntilClause "until kill -0 $PID; do sleep 1; done"
readUntilClause = called "until loop" $ do
start <- startSpan
kwId <- getId <$> g_Until
condition <- readTerm
statements <- readDoGroup kwId
id <- endSpan start
return $ T_UntilExpression id condition statements
readDoGroup kwId = do
optional (do
try . lookAhead $ g_Done
parseProblemAtId kwId ErrorC 1057 "Did you forget the 'do' for this loop?")
doKw <- g_Do `orFail` do
parseProblem ErrorC 1058 "Expected 'do'."
return "Expected 'do'"
acceptButWarn g_Semi ErrorC 1059 "No semicolons directly after 'do'."
allspacing
optional (do
try . lookAhead $ g_Done
parseProblemAtId (getId doKw) ErrorC 1060 "Can't have empty do clauses (use 'true' as a no-op).")
commands <- readCompoundList
g_Done `orFail` do
parseProblemAtId (getId doKw) ErrorC 1061 "Couldn't find 'done' for this 'do'."
parseProblem ErrorC 1062 "Expected 'done' matching previously mentioned 'do'."
return "Expected 'done'"
return commands
-- |
-- >>> prop $ isOk readForClause "for f in *; do rm \"$f\"; done"
-- >>> prop $ isOk readForClause "for f; do foo; done"
-- >>> prop $ isOk readForClause "for((i=0; i<10; i++)); do echo $i; done"
-- >>> prop $ isOk readForClause "for ((i=0;i<10 && n>x;i++,--n))\ndo \necho $i\ndone"
-- >>> prop $ isOk readForClause "for ((;;))\ndo echo $i\ndone"
-- >>> prop $ isOk readForClause "for ((;;)) do echo $i\ndone"
-- >>> prop $ isOk readForClause "for ((;;)) ; do echo $i\ndone"
-- >>> prop $ isOk readForClause "for i do true; done"
-- >>> prop $ isOk readForClause "for ((;;)) { true; }"
-- >>> prop $ isWarning readForClause "for $a in *; do echo \"$a\"; done"
-- >>> prop $ isOk readForClause "for foo\nin\\\n bar\\\n baz\ndo true; done"
readForClause = called "for loop" $ do
pos <- getPosition
(T_For id) <- g_For
spacing
readArithmetic id <|> readRegular id
where
readArithmetic id = called "arithmetic for condition" $ do
try $ string "(("
x <- readArithmeticContents
char ';' >> spacing
y <- readArithmeticContents
char ';' >> spacing
z <- readArithmeticContents
spacing
string "))"
spacing
optional $ readSequentialSep >> spacing
group <- readBraced <|> readDoGroup id
return $ T_ForArithmetic id x y z group
readBraced = do
(T_BraceGroup _ list) <- readBraceGroup
return list
readRegular id = do
acceptButWarn (char '$') ErrorC 1086
"Don't use $ on the iterator name in for loops."
name <- readVariableName `thenSkip` allspacing
values <- readInClause <|> (optional readSequentialSep >> return [])
group <- readDoGroup id
return $ T_ForIn id name values group
-- |
-- >>> prop $ isOk readSelectClause "select foo in *; do echo $foo; done"
-- >>> prop $ isOk readSelectClause "select foo; do echo $foo; done"
readSelectClause = called "select loop" $ do
(T_Select id) <- g_Select
spacing
typ <- readRegular
group <- readDoGroup id
typ id group
where
readRegular = do
name <- readVariableName
spacing
values <- readInClause <|> (readSequentialSep >> return [])
return $ \id group -> (return $ T_SelectIn id name values group)
readInClause = do
g_In
things <- readCmdWord `reluctantlyTill`
(void g_Semi <|> void linefeed <|> void g_Do)
do {
lookAhead g_Do;
parseNote ErrorC 1063 "You need a line feed or semicolon before the 'do'.";
} <|> do {
optional g_Semi;
void allspacing;
}
return things
-- |
-- >>> prop $ isOk readCaseClause "case foo in a ) lol; cow;; b|d) fooo; esac"
-- >>> prop $ isOk readCaseClause "case foo\n in * ) echo bar;; esac"
-- >>> prop $ isOk readCaseClause "case foo\n in * ) echo bar & ;; esac"
-- >>> prop $ isOk readCaseClause "case foo\n in *) echo bar ;& bar) foo; esac"
-- >>> prop $ isOk readCaseClause "case foo\n in *) echo bar;;& foo) baz;; esac"
readCaseClause = called "case expression" $ do
start <- startSpan
g_Case
word <- readNormalWord
allspacing
g_In <|> fail "Expected 'in'"
readLineBreak
list <- readCaseList
g_Esac <|> fail "Expected 'esac' to close the case statement"
id <- endSpan start
return $ T_CaseExpression id word list
readCaseList = many readCaseItem
readCaseItem = called "case item" $ do
notFollowedBy2 g_Esac
optional $ do
try . lookAhead $ readAnnotationPrefix
parseProblem ErrorC 1124 "ShellCheck directives are only valid in front of complete commands like 'case' statements, not individual case branches."
optional g_Lparen
spacing
pattern' <- readPattern
void g_Rparen <|> do
parseProblem ErrorC 1085
"Did you forget to move the ;; after extending this case item?"
fail "Expected ) to open a new case item"
readLineBreak
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 (separator, pattern', list)
readCaseSeparator = choice [
tryToken ";;&" (const ()) >> return CaseContinue,
tryToken ";&" (const ()) >> return CaseFallThrough,
g_DSEMI >> return CaseBreak,
lookAhead (readLineBreak >> g_Esac) >> return CaseBreak
]
-- |
-- >>> prop $ isOk readFunctionDefinition "foo() { command foo --lol \"$@\"; }"
-- >>> prop $ isOk readFunctionDefinition "foo (){ command foo --lol \"$@\"; }"
-- >>> prop $ isWarning readFunctionDefinition "foo(a, b) { true; }"
-- >>> prop $ isOk readFunctionDefinition ":(){ :|:;}"
-- >>> prop $ isOk readFunctionDefinition "?(){ foo; }"
-- >>> prop $ isOk readFunctionDefinition "..(){ cd ..; }"
-- >>> prop $ isOk readFunctionDefinition "foo() (ls)"
-- >>> prop $ isOk readFunctionDefinition "function foo { true; }"
-- >>> prop $ isOk readFunctionDefinition "function foo () { true; }"
-- >>> prop $ isWarning readFunctionDefinition "function foo{\ntrue\n}"
-- >>> prop $ isOk readFunctionDefinition "function []!() { true; }"
-- >>> prop $ isOk readFunctionDefinition "@require(){ true; }"
readFunctionDefinition = called "function" $ do
start <- startSpan
functionSignature <- try readFunctionSignature
allspacing
void (lookAhead $ oneOf "{(") <|> parseProblem ErrorC 1064 "Expected a { to open the function definition."
group <- readBraceGroup <|> readSubshell
id <- endSpan start
return $ functionSignature id group
where
readFunctionSignature =
readWithFunction <|> readWithoutFunction
where
readWithFunction = do
try $ do
string "function"
whitespace
spacing
name <- many1 extendedFunctionChars
spaces <- spacing
hasParens <- wasIncluded readParens
when (not hasParens && null spaces) $
acceptButWarn (lookAhead (oneOf "{("))
ErrorC 1095 "You need a space or linefeed between the function name and body."
return $ \id -> T_Function id (FunctionKeyword True) (FunctionParentheses hasParens) name
readWithoutFunction = try $ do
name <- many1 functionChars
guard $ name /= "time" -- Interfers with time ( foo )
spacing
readParens
return $ \id -> T_Function id (FunctionKeyword False) (FunctionParentheses True) name
readParens = do
g_Lparen
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 ()
-- |
-- >>> prop $ isOk readCoProc "coproc foo { echo bar; }"
-- >>> prop $ isOk readCoProc "coproc { echo bar; }"
-- >>> prop $ isOk readCoProc "coproc echo bar"
readCoProc = called "coproc" $ do
start <- startSpan
try $ do
string "coproc"
whitespace
choice [ try $ readCompoundCoProc start, readSimpleCoProc start ]
where
readCompoundCoProc start = do
var <- optionMaybe $
readVariableName `thenSkip` whitespace
body <- readBody readCompoundCommand
id <- endSpan start
return $ T_CoProc id var body
readSimpleCoProc start = do
body <- readBody readSimpleCommand
id <- endSpan start
return $ T_CoProc id Nothing body
readBody parser = do
start <- startSpan
body <- parser
id <- endSpan start
return $ T_CoProcBody id body
readPattern = (readNormalWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing)
-- |
-- >>> prop $ isOk readCompoundCommand "{ echo foo; }>/dev/null"
readCompoundCommand = do
cmd <- choice [
readBraceGroup,
readAmbiguous "((" readArithmeticExpression readSubshell (\pos ->
parseNoteAt pos WarningC 1105 "Shells disambiguate (( differently or not at all. For subshell, add spaces around ( . For ((, fix parsing errors."),
readSubshell,
readCondition,
readWhileClause,
readUntilClause,
readIfClause,
readForClause,
readSelectClause,
readCaseClause,
readFunctionDefinition
]
spacing
redirs <- many readIoRedirect
id <- getNextIdSpanningTokenList (cmd:redirs)
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
where
needsSeparator = choice [ g_Then, g_Else, g_Elif, g_Fi, g_Do, g_Done, g_Esac, g_Rbrace ]
readCompoundList = readTerm
readCompoundListOrEmpty = do
allspacing
readTerm <|> return []
readCmdPrefix = many1 (readIoRedirect <|> readAssignmentWord)
readCmdSuffix = many1 (readIoRedirect <|> readCmdWord)
readModifierSuffix = many1 (readIoRedirect <|> readWellFormedAssignment <|> readCmdWord)
readTimeSuffix = do
flags <- many readFlag
pipeline <- readPipeline
return $ flags ++ [pipeline]
where
-- This fails for quoted variables and such. Fixme?
readFlag = do
lookAhead $ char '-'
readCmdWord
-- Fixme: this is a hack that doesn't handle let c='4'"5" or let a\>b
readLetSuffix :: Monad m => SCParser m [Token]
readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord)
where
readLetExpression :: Monad m => SCParser m Token
readLetExpression = do
startPos <- getPosition
expression <- readStringForParser readCmdWord
let (unQuoted, newPos) = kludgeAwayQuotes expression startPos
subParse newPos readArithmeticContents unQuoted
kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos)
kludgeAwayQuotes s p =
case s of
first:rest@(_:_) ->
let (last:backwards) = reverse rest
middle = reverse backwards
in
if first `elem` "'\"" && first == last
then (middle, updatePosChar p first)
else (s, p)
x -> (s, p)
-- bash allows a=(b), ksh allows $a=(b). dash allows neither. Let's warn.
readEvalSuffix = many1 (readIoRedirect <|> readCmdWord <|> evalFallback)
where
evalFallback = do
pos <- getPosition
lookAhead $ char '('
parseProblemAt pos WarningC 1098 "Quote/escape special characters when using eval, e.g. eval \"a=(b)\"."
fail "Unexpected parentheses. Make sure to quote when eval'ing as shell parsers differ."
-- Get whatever a parser would parse as a string
readStringForParser parser = do
pos <- inSeparateContext $ lookAhead (parser >> getPosition)
readUntil pos
where
readUntil endPos = anyChar `reluctantlyTill` (getPosition >>= guard . (== endPos))
-- Like readStringForParser, returning the span as a T_Literal
readLiteralForParser parser = do
start <- startSpan
str <- readStringForParser parser
id <- endSpan start
return $ T_Literal id str
-- |
-- >>> prop $ isOk readAssignmentWord "a=42"
-- >>> prop $ isOk readAssignmentWord "b=(1 2 3)"
-- >>> prop $ isWarning readAssignmentWord "$b = 13"
-- >>> prop $ isWarning readAssignmentWord "b = $(lol)"
-- >>> prop $ isOk readAssignmentWord "b+=lol"
-- >>> prop $ isWarning readAssignmentWord "b += (1 2 3)"
-- >>> prop $ isOk readAssignmentWord "a[3$n'']=42"
-- >>> prop $ isOk readAssignmentWord "a[4''$(cat foo)]=42"
-- >>> prop $ isOk readAssignmentWord "IFS= "
-- >>> prop $ isOk readAssignmentWord "foo="
-- >>> prop $ isOk readAssignmentWord "foo= "
-- >>> prop $ isOk readAssignmentWord "foo= #bar"
-- >>> prop $ isWarning readAssignmentWord "foo$n=42"
-- >>> prop $ isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
-- >>> prop $ isOk readAssignmentWord "a[b <<= 3 + c]='thing'"
-- >>> prop $ isOk readAssignmentWord "var=( (1 2) (3 4) )"
-- >>> prop $ isOk readAssignmentWord "var=( 1 [2]=(3 4) )"
-- >>> prop $ isOk readAssignmentWord "var=(1 [2]=(3 4))"
readAssignmentWord = readAssignmentWordExt True
readWellFormedAssignment = readAssignmentWordExt False
readAssignmentWordExt lenient = try $ do
start <- startSpan
pos <- getPosition
when lenient $
optional (char '$' >> parseNote ErrorC 1066 "Don't use $ on the left side of assignments.")
variable <- readVariableName
when lenient $
optional (readNormalDollar >> parseNoteAt pos ErrorC
1067 "For indirection, use (associative) arrays or 'read \"var$n\" <<< \"value\"'")
indices <- many readArrayIndex
hasLeftSpace <- fmap (not . null) spacing
pos <- getPosition
id <- endSpan start
op <- readAssignmentOp
hasRightSpace <- fmap (not . null) spacing
isEndOfCommand <- fmap isJust $ optionMaybe (try . lookAhead $ (void (oneOf "\r\n;&|)") <|> eof))
if not hasLeftSpace && (hasRightSpace || isEndOfCommand)
then do
when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $
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 indices value
else do
when (hasLeftSpace || hasRightSpace) $
parseNoteAt pos ErrorC 1068 $
"Don't put spaces around the "
++ (if op == Append
then "+= when appending"
else "= in assignments")
++ " (or quote to make it literal)."
value <- readArray <|> readNormalWord
spacing
return $ T_Assignment id op variable indices value
where
readAssignmentOp = do
pos <- getPosition
unexpecting "" $ string "==="
choice [
string "+=" >> return Append,
do
try (string "==")
parseProblemAt pos ErrorC 1097
"Unexpected ==. For assignment, use =. For comparison, use [/[[."
return Assign,
string "=" >> return Assign
]
readEmptyLiteral = do
start <- startSpan
id <- endSpan start
return $ T_Literal id ""
readArrayIndex = do
start <- startSpan
char '['
pos <- getPosition
str <- readStringForParser readIndexSpan
char ']'
id <- endSpan start
return $ T_UnparsedIndex id pos str
readArray :: Monad m => SCParser m Token
readArray = called "array assignment" $ do
start <- startSpan
opening <- getPosition
char '('
optional $ do
lookAhead $ char '('
parseProblemAt opening ErrorC 1116 "Missing $ on a $((..)) expression? (or use ( ( for arrays)."
allspacing
words <- readElement `reluctantlyTill` char ')'
char ')' <|> fail "Expected ) to close array assignment"
id <- endSpan start
return $ T_Array id words
where
readElement = (readIndexed <|> readRegular) `thenSkip` allspacing
readIndexed = do
start <- startSpan
index <- try $ do
x <- many1 readArrayIndex
char '='
return x
value <- readRegular <|> nothing
id <- endSpan start
return $ T_IndexedElement id index value
readRegular = readArray <|> readNormalWord
nothing = do
start <- startSpan
id <- endSpan start
return $ T_Literal id ""
tryToken s t = try $ do
start <- startSpan
string s
id <- endSpan start
spacing
return $ t id
redirToken c t = try $ do
start <- startSpan
char c
id <- endSpan start
notFollowedBy2 $ char '('
return $ t id
tryWordToken s t = tryParseWordToken s t `thenSkip` spacing
tryParseWordToken keyword t = try $ do
start <- startSpan
str <- anycaseString keyword
id <- endSpan start
optional $ do
c <- try . lookAhead $ anyChar
let warning code = parseProblem ErrorC code $ "You need a space before the " ++ [c] ++ "."
case c of
'[' -> warning 1069
'#' -> warning 1099
'!' -> warning 1129
':' -> warning 1130
_ -> return ()
lookAhead keywordSeparator
when (str /= keyword) $
parseProblem ErrorC 1081 $
"Scripts are case sensitive. Use '" ++ keyword ++ "', not '" ++ str ++ "'."
return $ t id
anycaseString =
mapM anycaseChar
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
g_DLESS = tryToken "<<" T_DLESS
g_DGREAT = tryToken ">>" T_DGREAT
g_LESSAND = tryToken "<&" T_LESSAND
g_GREATAND = tryToken ">&" T_GREATAND
g_LESSGREAT = tryToken "<>" T_LESSGREAT
g_DLESSDASH = tryToken "<<-" T_DLESSDASH
g_CLOBBER = tryToken ">|" T_CLOBBER
g_OPERATOR = g_AND_IF <|> g_OR_IF <|> g_DSEMI <|> g_DLESSDASH <|> g_DLESS <|> g_DGREAT <|> g_LESSAND <|> g_GREATAND <|> g_LESSGREAT
g_If = tryWordToken "if" T_If
g_Then = tryWordToken "then" T_Then
g_Else = tryWordToken "else" T_Else
g_Elif = tryWordToken "elif" T_Elif
g_Fi = tryWordToken "fi" T_Fi
g_Do = tryWordToken "do" T_Do
g_Done = tryWordToken "done" T_Done
g_Case = tryWordToken "case" T_Case
g_Esac = tryWordToken "esac" T_Esac
g_While = tryWordToken "while" T_While
g_Until = tryWordToken "until" T_Until
g_For = tryWordToken "for" T_For
g_Select = tryWordToken "select" T_Select
g_In = tryWordToken "in" T_In <* skipAnnotationAndWarn
g_Lbrace = tryWordToken "{" T_Lbrace
g_Rbrace = do -- handled specially due to ksh echo "${ foo; }bar"
start <- startSpan
char '}'
id <- endSpan start
return $ T_Rbrace id
g_Lparen = tryToken "(" T_Lparen
g_Rparen = tryToken ")" T_Rparen
g_Bang = do
start <- startSpan
char '!'
id <- endSpan start
void spacing1 <|> do
pos <- getPosition
parseProblemAt pos ErrorC 1035
"You are missing a required space after the !."
return $ T_Bang id
g_Semi = do
notFollowedBy2 g_DSEMI
tryToken ";" T_Semi
keywordSeparator =
eof <|> void (try allspacingOrFail) <|> void (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 =
(lookAhead (try p) >> t) <|> f
-- |
-- >>> prop $ isOk readShebang "#!/bin/sh\n"
-- >>> prop $ isWarning readShebang "!# /bin/sh\n"
-- >>> prop $ isNotOk readShebang "#shellcheck shell=/bin/sh\n"
-- >>> prop $ isWarning readShebang "! /bin/sh"
-- >>> prop $ isWarning readShebang "\n#!/bin/sh"
-- >>> prop $ isWarning readShebang " # Copyright \n!#/bin/bash"