module Data.Makefile.Parse.Internal where
import Control.Monad
import Data.Foldable
import Data.Attoparsec.Text
import Data.Makefile
import Data.Monoid
import Control.Applicative
import qualified Data.Attoparsec.Text as Atto
import qualified Data.Text as T
import qualified Data.Text.IO as T
parseMakefile :: IO (Either String Makefile)
parseMakefile = Atto.parseOnly makefile <$> T.readFile "Makefile"
parseAsMakefile :: FilePath -> IO (Either String Makefile)
parseAsMakefile f = Atto.parseOnly makefile <$> T.readFile f
parseMakefileContents :: T.Text -> Either String Makefile
parseMakefileContents = Atto.parseOnly makefile
parseAll :: Parser a -> T.Text -> Either String a
parseAll p = Atto.parseOnly (p <* Atto.endOfInput)
makefile :: Parser Makefile
makefile = Makefile <$> many' entry
entry :: Parser Entry
entry = assignment <|> rule <|> otherLine
assignment :: Parser Entry
assignment = do
varName <- variableName
assType <- assignmentType
varVal <- toEscapedLineEnd
return (Assignment assType varName varVal)
takeWhileM :: (Char -> Parser Bool) -> Parser T.Text
takeWhileM a = (T.pack . reverse) <$> go []
where
go cs = do
c <- Atto.anyChar
True <- a c
go (c:cs) <|> pure (c:cs)
variableName :: Parser T.Text
variableName = stripped $ takeWhileM go
where
go '+' = Atto.peekChar' >>= \case
'=' -> return False
_c -> return True
go '?' = Atto.peekChar' >>= \case
'=' -> return False
_c -> return True
go '!' = Atto.peekChar' >>= \case
'=' -> return False
_c -> return True
go ':' = return False
go '#' = return False
go '=' = return False
go (Atto.isEndOfLine -> True) = return False
go _c = return True
assignmentType :: Parser AssignmentType
assignmentType =
("=" *> pure RecursiveAssign)
<|> ("+=" *> pure AppendAssign)
<|> ("?=" *> pure ConditionalAssign)
<|> ("!=" *> pure ShellAssign)
<|> (":=" *> pure SimpleAssign)
<|> ("::=" *> pure SimplePosixAssign)
rule :: Parser Entry
rule =
Rule
<$> target
<*> (many' dependency <* (Atto.takeWhile (not.Atto.isEndOfLine) <* endOfLine'))
<*> many' command
endOfLine' :: Parser ()
endOfLine' =
Atto.endOfLine <|> (Atto.atEnd >>= check)
where
check True = pure ()
check False = mzero
command :: Parser Command
command = Command <$> recipeLine
recipeLine :: Parser T.Text
recipeLine =
Atto.char '\t' *> recipeLineContents ""
where
recipeLineContents pre = do
cur <- Atto.takeWhile $ \c ->
c /= '\\' && not (Atto.isEndOfLine c)
asum
[
Atto.char '\\'
*> Atto.endOfLine
*> (void (Atto.char '\t') <|> pure ())
*> recipeLineContents (pre <> cur <> "\\\n")
,
endOfLine' *> pure (pre <> cur)
,
Atto.char '\\' *> recipeLineContents (pre <> cur <> "\\")
]
target :: Parser Target
target = Target <$> (go $ stripped (Atto.takeWhile (/= ':') <* Atto.char ':'))
where
go :: Parser a -> Parser a
go p =
Atto.takeWhile (liftA2 (||) (== ' ') (== '\t'))
*> (Atto.peekChar >>= \case
Just '#' -> mzero
Just '\n' -> mzero
_ -> p)
dependency :: Parser Dependency
dependency = Dependency <$> (sameLine <|> newLine)
where
sameLine =
Atto.takeWhile (== ' ')
*> Atto.takeWhile1 (`notElem` [' ', '\n', '#', '\\'])
newLine =
Atto.takeWhile (== ' ')
*> Atto.char '\\'
*> Atto.char '\n'
*> (sameLine <|> newLine)
otherLine :: Parser Entry
otherLine = OtherLine <$> go
where
go = asum
[
Atto.endOfLine *> pure ""
,
Atto.takeWhile1 (not . Atto.isEndOfLine) <* Atto.endOfLine
]
toLineEnd :: Parser T.Text
toLineEnd = Atto.takeWhile (`notElem` ['\n', '#'])
toEscapedLineEnd :: Parser T.Text
toEscapedLineEnd = (T.unwords . filter (not . T.null)) <$> go
where
go = do
l <- toLineEnd <* (void (Atto.char '\n') <|> pure ())
case T.stripSuffix "\\" l of
Nothing -> return [T.strip l]
Just l' -> (T.strip l':) <$> go
stripped :: Parser T.Text -> Parser T.Text
stripped = fmap T.strip