packages feed

diohsc-0.1.14: CommandLine.hs

-- This file is part of Diohsc
-- Copyright (C) 2020 Martin Bays <mbays@sdf.org>
--
-- 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 $
    -- Allow escaping chars with \\, but preserve terminal \\
    (char '\\' >> (anyChar <|> (eof >> pure '\\'))) <|> 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