-- This file is part of Diohsc -- Copyright (C) 2020 Martin Bays -- -- This program is free software: you can redistribute it and/or modify -- it under the terms of version 3 of the GNU General Public License as -- published by the Free Software Foundation, or any later version. -- -- You should have received a copy of the GNU General Public License -- along with this program. If not, see http://www.gnu.org/licenses/. {-# LANGUAGE LambdaCase #-} {-# LANGUAGE Safe #-} module CommandLine ( CommandLine(..) , CommandArg(..) , ElemsSpecs , PTarget(..) , parseCommandLine , parseCommand , resolveElemsSpecs ) where import Control.Monad (guard, liftM2, mzero) import Data.Bifunctor (first) import Data.Char (isAlphaNum) import Data.List (foldl') import Data.Maybe (maybeToList) import Safe (atMay, headMay) import Text.Parsec import Text.Parsec.String (Parser) data ElemSpec = ESNum Int | ESNumFromEnd Int | ESSearch String deriving (Eq,Ord,Show) data ElemsSpec = EsSElemSpec ElemSpec | EsSRange (Maybe ElemSpec) (Maybe ElemSpec) | EsSSearch String deriving (Eq,Ord,Show) type ElemsSpecs = [ElemsSpec] elemsSpecsNum :: Int -> ElemsSpecs elemsSpecsNum = (:[]) . EsSElemSpec . ESNum resolveElemsSpecs :: String -> (String -> a -> Bool) -> [a] -> ElemsSpecs -> Either String [a] resolveElemsSpecs typeStr match as ess = mapM resolveElemsSpec ess >>= (\case [] -> Left $ "No such " <> typeStr as' -> return as') . concat where resolveElemsSpec (EsSElemSpec es) = maybeToList . atMay as <$> resolveES es resolveElemsSpec (EsSRange mes1 mes2) = do mns1 <- mapM resolveES mes1 mns2 <- mapM resolveES mes2 return $ if Just True == liftM2 (>) mns1 mns2 || mns2 == Just (-1) then reverse . maybe id drop mns2 $ maybe id (take . (+ 1)) mns1 as else maybe id drop mns1 $ maybe id (take . (+ 1)) mns2 as resolveElemsSpec (EsSSearch s) = return $ filter (s `match`) as resolveES :: ElemSpec -> Either String Int resolveES (ESNum n) = return $ n - 1 resolveES (ESNumFromEnd n) = return $ length as - n resolveES (ESSearch s) = maybe (Left $ "No " <> typeStr <> " matches pattern: " ++ s) Right $ headMay . map fst . filter snd . zip [0..] $ (s `match`) <$> as data PTarget = PTargetCurr | PTargetJumpBack | PTargetMark String | PTargetAbs String | PTargetLog ElemsSpecs | PTargetQueue String ElemsSpecs | PTargetRoot PTarget | PTargetAncestors PTarget ElemsSpecs | PTargetDescendants PTarget ElemsSpecs | PTargetChild { ptcIncreasing :: Bool, ptcNoVisited :: Bool , ptcTarget :: PTarget, ptcSpecs :: ElemsSpecs } | PTargetLinks { ptlNoVisited :: Bool , ptlTarget :: PTarget, ptlSpecs :: ElemsSpecs } | PTargetRef PTarget String deriving (Eq,Ord,Show) data CommandArg = CommandArg { commandArgArg :: String , commandArgLiteralTail :: String } deriving (Eq,Ord,Show) data CommandLine = CommandLine (Maybe PTarget) (Maybe (String,[CommandArg])) deriving (Eq,Ord,Show) parseCommandLine :: String -> Either String CommandLine parseCommandLine = first show . parse (spaces >> commandLine <* eof) "" parseCommand :: String -> Either String String parseCommand = first show . parse command "" commandLine :: Parser CommandLine commandLine = choice [ char '#' >> many anyChar >> return (CommandLine Nothing Nothing) , liftM2 CommandLine (optionMaybe target) $ spaces >> optionMaybe commandAndArgs ] commandAndArgs :: Parser (String, [CommandArg]) commandAndArgs = liftM2 (,) command $ spaces >> commandArgs commandArgs :: Parser [CommandArg] commandArgs = liftM2 (flip CommandArg) (lookAhead $ many1 anyChar) commandArg `sepEndBy` spaces command :: Parser String command = choice [ liftM2 (:) (oneOf "|!") (many $ oneOf "|!-") , many1 letter ] commandArg :: Parser String commandArg = escapedArg escapedArg :: Parser String escapedArg = escapedWhile $ noneOf " " escapedWhile :: Parser Char -> Parser String escapedWhile c = many1 $ (char '\\' >> anyChar) <|> c nat :: Parser Int nat = read <$> many1 digit countMany :: Parser String -> Parser Int countMany s = (s >> (+ 1) <$> countMany s) <|> return 0 patt :: Parser String patt = escapedWhile (noneOf "^ ") <* optional (char '^') elemSpec :: Parser ElemSpec elemSpec = choice [ ESNum <$> nat , char '$' >> ESNumFromEnd <$> choice [ nat , (+ 1) <$> countMany (string "$") ] , char '^' >> ESSearch <$> patt ] elemsSpec :: Parser ElemsSpec elemsSpec = choice [ try $ string "^^" >> EsSSearch <$> patt , do mess1 <- optionMaybe elemSpec choice [ char '-' >> EsSRange mess1 <$> optionMaybe elemSpec , maybe mzero (return . EsSElemSpec) mess1 ] ] elemsSpecs :: Parser ElemsSpecs elemsSpecs = sepBy1 elemsSpec (char ',') elemsSpecsBy :: Parser String -> Parser ElemsSpecs elemsSpecsBy s = s >> choice [ elemsSpecs , elemsSpecsNum . (+ 1) <$> countMany s ] escapedArgStartingWith :: Parser Char -> Parser String escapedArgStartingWith p = liftM2 (:) p (escapedArg <|> return "") ref :: String -> Parser String ref = escapedArgStartingWith . oneOf baseTarget :: Parser PTarget baseTarget = choice [ PTargetLog <$> elemsSpecsBy (string "$") , PTargetLinks False PTargetCurr <$> elemsSpecs , PTargetRef PTargetCurr <$> ref "./?" , try $ do s <- many alphaNum PTargetQueue s <$> elemsSpecsBy (string "~") , char '\'' >> choice [ char '\'' >> return PTargetJumpBack , PTargetMark <$> many1 alphaNum ] , try $ do s <- escapedArgStartingWith alphaNum -- scheme required for other uris guard . not $ all (\c -> c `elem` "!|-" || isAlphaNum c) s return $ PTargetAbs s , return PTargetCurr ] targetMod :: Parser (PTarget -> PTarget) targetMod = choice [ char '@' >> return PTargetRoot , flip PTargetAncestors <$> elemsSpecsBy (string "<") , flip PTargetDescendants <$> elemsSpecsBy (string ">") , flip (PTargetChild True False) <$> elemsSpecsBy (string "]") , flip (PTargetChild True True) <$> elemsSpecsBy (string "}") , flip (PTargetChild False False) <$> elemsSpecsBy (string "[") , flip (PTargetChild False True) <$> elemsSpecsBy (string "{") , optional (char '_') >> flip (PTargetLinks False) <$> elemsSpecs , char '*' >> flip (PTargetLinks True) <$> elemsSpecs , flip PTargetRef <$> ref "/?" ] target :: Parser PTarget target = do base <- baseTarget mods <- many targetMod guard . not $ base == PTargetCurr && null mods return $ foldl' (flip ($)) base mods