erebos-tester-0.3.6: src/Parser/Shell.hs
module Parser.Shell (
ShellScript,
shellScript,
) where
import Control.Applicative (liftA2)
import Control.Monad
import Data.Char
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Lazy qualified as TL
import Text.Megaparsec
import Text.Megaparsec.Char
import Text.Megaparsec.Char.Lexer qualified as L
import Parser.Core
import Parser.Expr
import Script.Expr
import Script.Shell
parseTextArgument :: TestParser (Expr Text)
parseTextArgument = lexeme $ fmap (App AnnNone (Pure T.concat) <$> foldr (liftA2 (:)) (Pure [])) $ some $ choice
[ doubleQuotedString
, singleQuotedString
, standaloneEscapedChar
, stringExpansion
, unquotedString
]
where
specialChars = [ '"', '\'', '\\', '$', '#', '|', '>', '<', ';', '[', ']'{-, '{', '}' -}, '(', ')'{-, '*', '?', '~', '&', '!' -} ]
stringSpecialChars = [ '"', '\\', '$' ]
unquotedString :: TestParser (Expr Text)
unquotedString = do
Pure . TL.toStrict <$> takeWhile1P Nothing (\c -> not (isSpace c) && c `notElem` specialChars)
doubleQuotedString :: TestParser (Expr Text)
doubleQuotedString = do
void $ char '"'
let inner = choice
[ char '"' >> return []
, (:) <$> (Pure . TL.toStrict <$> takeWhile1P Nothing (`notElem` stringSpecialChars)) <*> inner
, (:) <$> stringEscapedChar <*> inner
, (:) <$> stringExpansion <*> inner
]
App AnnNone (Pure T.concat) . foldr (liftA2 (:)) (Pure []) <$> inner
singleQuotedString :: TestParser (Expr Text)
singleQuotedString = do
Pure . TL.toStrict <$> (char '\'' *> takeWhileP Nothing (/= '\'') <* char '\'')
stringEscapedChar :: TestParser (Expr Text)
stringEscapedChar = do
void $ char '\\'
fmap Pure $ choice $
map (\c -> char c >> return (T.singleton c)) stringSpecialChars ++
[ char 'n' >> return "\n"
, char 'r' >> return "\r"
, char 't' >> return "\t"
, return "\\"
]
standaloneEscapedChar :: TestParser (Expr Text)
standaloneEscapedChar = do
void $ char '\\'
fmap T.singleton . Pure <$> printChar
parseRedirection :: TestParser (Expr ShellArgument)
parseRedirection = choice
[ do
rsymbol "<"
fmap ShellRedirectStdin <$> parseTextArgument
, do
rsymbol ">"
fmap (ShellRedirectStdout False) <$> parseTextArgument
, do
rsymbol ">>"
fmap (ShellRedirectStdout True) <$> parseTextArgument
, do
rsymbol "2>"
fmap (ShellRedirectStderr False) <$> parseTextArgument
, do
rsymbol "2>>"
fmap (ShellRedirectStderr True) <$> parseTextArgument
]
where
rsymbol str = void $ try $ (string str <* notFollowedBy (satisfy $ (`elem` [ '<', '>', '|' ]))) <* sc
parseArgument :: TestParser (Expr ShellArgument)
parseArgument = choice
[ parseRedirection
, expressionExpansion "shell argument" <* sc
, fmap ShellArgument <$> parseTextArgument
]
parseArguments :: TestParser (Expr ShellArguments)
parseArguments = do
arglists <- many $ choice
[ do
off <- stateOffset <$> getParserState
se <- someExpansion
choice
[ do
notFollowedBy space1
arg <- expansionTypeCheck off "shell argument" se
txt <- parseTextArgument
return $ joinArgument <$> arg <*> txt
, do
expansionTypeCheck off "shell arguments" se <* sc
]
, fmap (ShellArguments . (: [])) <$> parseArgument
]
return $ fmap mconcat $ foldr (liftA2 (:)) (Pure []) $ arglists
where
joinArgument (ShellArgument x) y = ShellArguments [ ShellArgument (x <> y) ]
joinArgument ax y = ShellArguments [ ax, ShellArgument y ]
parseCommand :: TestParser (Expr ShellCommand)
parseCommand = label "shell statement" $ do
line <- getSourceLine
choice
[ do
args <- expressionExpansion "shell command" <* sc
args' <- parseArguments
return $ commandFromArgLists line <$> args <*> args'
, do
command <- parseTextArgument
args <- parseArguments
return $ ShellCommand
<$> command
<*> args
<*> pure line
]
where
commandFromArgLists line (ShellArguments (ShellArgument cmd : args)) (ShellArguments args') =
ShellCommand cmd (ShellArguments (args ++ args')) line
commandFromArgLists line (ShellArguments args) (ShellArguments args') =
ShellCommand "" (ShellArguments (args ++ args')) line
parsePipeline :: Maybe (Expr ShellPipeline) -> TestParser (Expr ShellPipeline)
parsePipeline mbupper = do
cmd <- parseCommand
let pipeline =
case mbupper of
Nothing -> fmap (\ecmd -> ShellPipeline ecmd Nothing) cmd
Just upper -> liftA2 (\ecmd eupper -> ShellPipeline ecmd (Just eupper)) cmd upper
choice
[ do
psymbol "|"
parsePipeline (Just pipeline)
, do
return pipeline
]
where
psymbol str = void $ try $ (string str <* notFollowedBy (satisfy $ (`elem` [ '<', '>', '|', '&' ]))) <* sc
parseStatement :: TestParser (Expr [ ShellStatement ])
parseStatement = do
line <- getSourceLine
fmap ((: []) . flip ShellStatement line) <$> parsePipeline Nothing
shellScript :: TestParser (Expr ShellScript)
shellScript = do
indent <- L.indentLevel
fmap ShellScript <$> blockOf indent parseStatement