module System.REPL.Command (
Command(..),
runCommand,
runSingleCommand,
oneOf,
subcommand,
makeREPL,
SomeCommandError(..),
MalformedParamsError(..),
TooFewParamsError(..),
TooManyParamsError(..),
readArgs,
getName,
quoteArg,
summarizeCommands,
makeCommand,
makeCommand1,
makeCommand2,
makeCommand3,
makeCommand4,
makeCommand5,
makeCommand6,
makeCommand7,
makeCommand8,
makeCommandN,
) where
import Prelude hiding (putStrLn, putStr, (++), length, replicate)
import qualified Prelude as P
import Control.Arrow (first)
import Control.Monad
import Control.Monad.Catch
import Control.Monad.IO.Class (MonadIO(liftIO))
import Control.Monad.Loops (unfoldrM, iterateUntil)
import Data.Char (isSpace)
import Data.Foldable (Foldable)
import qualified Data.Functor.Apply as Ap
import qualified Data.Functor.Bind as Bi
import Data.Functor.Monadic
import qualified Data.List as LU
import qualified Data.List.Safe as L
import Data.ListLike(ListLike(..))
import Data.Maybe (fromJust, isJust, fromMaybe)
import Data.Monoid (mempty, Monoid)
import Data.Ord
import Data.Text.Lazy (Text)
import qualified Data.Text.Lazy as T
import Data.Typeable
import Numeric.Peano
import System.REPL
import qualified Text.Parsec as P
import qualified Text.Parsec.Language as P
import qualified Text.Parsec.Token as P
(++) :: (ListLike full item) => full -> full -> full
(++) = append
data SomeCommandError = forall e.Exception e => SomeCommandError e deriving (Typeable)
instance Show SomeCommandError where show (SomeCommandError e) = show e
instance Exception SomeCommandError
commandErrorUpcast :: (Exception a) => a -> SomeException
commandErrorUpcast = toException . SomeCommandError
commandErrorDowncast :: (Exception a) => SomeException -> Maybe a
commandErrorDowncast x = do {SomeCommandError y <- fromException x; cast y}
data MalformedParamsError = MalformedParamsError Text deriving (Show, Eq, Typeable, Ord)
instance Exception MalformedParamsError where
toException = commandErrorUpcast
fromException = commandErrorDowncast
data TooManyParamsError = TooManyParamsError Int Int deriving (Show, Eq, Typeable, Ord)
instance Exception TooManyParamsError where
toException = commandErrorUpcast
fromException = commandErrorDowncast
data TooFewParamsError = TooFewParamsError Int Int deriving (Show, Eq, Typeable, Ord)
instance Exception TooFewParamsError where
toException = commandErrorUpcast
fromException = commandErrorDowncast
data Command m i a = Command{
commandName :: Text,
commandTest :: i -> Bool,
commandDesc :: Text,
runPartialCommand :: [i] -> m (a, [i])}
instance Functor m => Functor (Command m i) where
fmap f c@Command{runPartialCommand=run} = c{runPartialCommand=(fmap (first f) . run)}
instance (Functor m, Monad m) => Ap.Apply (Command m i) where
f <.> g = f{runPartialCommand = h}
where
h input = do (func, output) <- runPartialCommand f input
(arg, output') <- runPartialCommand g output
return (func arg, output')
instance (Functor m, Monad m) => Bi.Bind (Command m i) where
f >>- g = f{runPartialCommand = h}
where
h input = do (res, output) <- runPartialCommand f input
(res', output') <- runPartialCommand (g res) output
return (res', output')
runCommand :: (Functor m, Monad m, MonadThrow m) => Command m Text a -> Text -> m a
runCommand c = fmap fst . runPartialCommand c <=< readArgs
runSingleCommand :: (MonadThrow m, Functor m) => Command m Text a -> Text -> m a
runSingleCommand c t = do
t' <- readArgs t
(res, output) <- runPartialCommand c t'
let act = length t'
mx = act length output
when (not . L.null $ output) (throwM $ TooManyParamsError mx act)
return res
oneOf :: Monoid i
=> Text
-> Text
-> [Command m i a]
-> Command m i a
oneOf n d xs = Command n test d cmd
where
test t = L.any (($ t) . commandTest) xs
cmd input = (`runPartialCommand` input)
. LU.head
. L.dropWhile (not . ($ fromMaybe mempty (L.head input)) . commandTest) $ xs
subcommand :: (Functor m, Monad m, Monoid i)
=> Command m i a
-> [a -> Command m i b]
-> Command m i b
subcommand x xs = x Bi.>>- \y -> oneOf "" "" (L.map ($ y) xs)
readArgs :: MonadThrow m => Text -> m [Text]
readArgs = either err return . P.parse parser "" . T.unpack
where
err = throwM . MalformedParamsError . T.pack . show
parser = P.many (stringLiteral P.<|> unquotedLiteral)
stringLiteral = P.stringLiteral P.haskell >$> T.pack
unquotedLiteral =
do raw <- P.many1 $ P.satisfy $ not . isSpace
P.eof P.<|> (P.many1 P.space >> return ())
let lit = stringLiteral
res = P.parse lit "" ("\"" ++ raw ++ "\"")
case res of (Right r) -> return r
(Left l) -> fail (show l)
getName :: (Functor m, MonadThrow m) => Text -> m Text
getName = fromMaybe mempty . L.head <$=< readArgs
quoteArg :: Text -> Text
quoteArg x = if T.null x || T.any isSpace x then '\"' `T.cons` x `T.snoc` '\"'
else x
makeCommand :: (MonadIO m, MonadCatch m,
Functor m, Monoid i)
=> Text
-> (i -> Bool)
-> Text
-> (i -> m z)
-> Command m i z
makeCommand n t d f = Command n t d f'
where
f' args = do res <- f $ fromMaybe mempty $ L.head args
return (res, L.drop 1 args)
makeCommand1 :: (MonadIO m, MonadCatch m, Functor m)
=> Text
-> (Text -> Bool)
-> Text
-> Bool
-> Asker m a
-> (Text -> a -> m z)
-> Command m Text z
makeCommand1 n t d canAsk p1 f = Command n t d f'
where
mx = 1
f' args = do let x0 = fromMaybe mempty $ L.head args
x1 <- askC canAsk p1 args mx 1
res <- f x0 x1
return (res, L.drop (mx+1) args)
makeCommand2 :: (MonadIO m, MonadCatch m, Functor m)
=> Text
-> (Text -> Bool)
-> Text
-> Bool
-> Asker m a
-> Asker m b
-> (Text -> a -> b -> m z)
-> Command m Text z
makeCommand2 n t d canAsk p1 p2 f = Command n t d f'
where
mx = 2
f' args = do let x0 = fromMaybe mempty $ L.head args
x1 <- askC canAsk p1 args mx 1
x2 <- askC canAsk p2 args mx 2
res <- f x0 x1 x2
return (res, L.drop (mx+1) args)
makeCommand3 :: (MonadIO m, MonadCatch m, Functor m)
=> Text
-> (Text -> Bool)
-> Text
-> Bool
-> Asker m a
-> Asker m b
-> Asker m c
-> (Text -> a -> b -> c -> m z)
-> Command m Text z
makeCommand3 n t d canAsk p1 p2 p3 f = Command n t d f'
where
mx = 3
f' args = do let x0 = fromMaybe "" $ L.head args
x1 <- askC canAsk p1 args mx 1
x2 <- askC canAsk p2 args mx 2
x3 <- askC canAsk p3 args mx 3
res <- f x0 x1 x2 x3
return (res, L.drop (mx+1) args)
makeCommand4 :: (MonadIO m, MonadCatch m, Functor m)
=> Text
-> (Text -> Bool)
-> Text
-> Bool
-> Asker m a
-> Asker m b
-> Asker m c
-> Asker m d
-> (Text -> a -> b -> c -> d -> m z)
-> Command m Text z
makeCommand4 n t d canAsk p1 p2 p3 p4 f = Command n t d f'
where
mx = 4
f' args = do let x0 = fromMaybe "" $ L.head args
x1 <- askC canAsk p1 args mx 1
x2 <- askC canAsk p2 args mx 2
x3 <- askC canAsk p3 args mx 3
x4 <- askC canAsk p4 args mx 4
res <- f x0 x1 x2 x3 x4
return (res, L.drop (mx+1) args)
makeCommand5 :: (MonadIO m, MonadCatch m, Functor m)
=> Text
-> (Text -> Bool)
-> Text
-> Bool
-> Asker m a
-> Asker m b
-> Asker m c
-> Asker m d
-> Asker m e
-> (Text -> a -> b -> c -> d -> e -> m z)
-> Command m Text z
makeCommand5 n t d canAsk p1 p2 p3 p4 p5 f = Command n t d f'
where
mx = 5
f' args = do let x0 = fromMaybe "" $ L.head args
x1 <- askC canAsk p1 args mx 1
x2 <- askC canAsk p2 args mx 2
x3 <- askC canAsk p3 args mx 3
x4 <- askC canAsk p4 args mx 4
x5 <- askC canAsk p5 args mx 5
res <- f x0 x1 x2 x3 x4 x5
return (res, L.drop (mx+1) args)
makeCommand6 :: (MonadIO m, MonadCatch m, Functor m)
=> Text
-> (Text -> Bool)
-> Text
-> Bool
-> Asker m a
-> Asker m b
-> Asker m c
-> Asker m d
-> Asker m e
-> Asker m f
-> (Text -> a -> b -> c -> d -> e -> f -> m z)
-> Command m Text z
makeCommand6 n t d canAsk p1 p2 p3 p4 p5 p6 f = Command n t d f'
where
mx = 6
f' args = do let x0 = fromMaybe mempty $ L.head args
x1 <- askC canAsk p1 args mx 1
x2 <- askC canAsk p2 args mx 2
x3 <- askC canAsk p3 args mx 3
x4 <- askC canAsk p4 args mx 4
x5 <- askC canAsk p5 args mx 5
x6 <- askC canAsk p6 args mx 6
res <- f x0 x1 x2 x3 x4 x5 x6
return (res, L.drop (mx+1) args)
makeCommand7 :: (MonadIO m, MonadCatch m, Functor m)
=> Text
-> (Text -> Bool)
-> Text
-> Bool
-> Asker m a
-> Asker m b
-> Asker m c
-> Asker m d
-> Asker m e
-> Asker m f
-> Asker m g
-> (Text -> a -> b -> c -> d -> e -> f -> g -> m z)
-> Command m Text z
makeCommand7 n t d canAsk p1 p2 p3 p4 p5 p6 p7 f = Command n t d f'
where
mx = 7
f' args = do let x0 = fromMaybe "" $ L.head args
x1 <- askC canAsk p1 args mx 1
x2 <- askC canAsk p2 args mx 2
x3 <- askC canAsk p3 args mx 3
x4 <- askC canAsk p4 args mx 4
x5 <- askC canAsk p5 args mx 5
x6 <- askC canAsk p6 args mx 6
x7 <- askC canAsk p7 args mx 7
res <- f x0 x1 x2 x3 x4 x5 x6 x7
return (res, L.drop (mx+1) args)
makeCommand8 :: (MonadIO m, MonadCatch m, Functor m)
=> Text
-> (Text -> Bool)
-> Text
-> Bool
-> Asker m a
-> Asker m b
-> Asker m c
-> Asker m d
-> Asker m e
-> Asker m f
-> Asker m g
-> Asker m h
-> (Text -> a -> b -> c -> d -> e -> f -> g -> h -> m z)
-> Command m Text z
makeCommand8 n t d canAsk p1 p2 p3 p4 p5 p6 p7 p8 f = Command n t d f'
where
mx = 8
f' args = do let x0 = fromMaybe "" $ L.head args
x1 <- askC canAsk p1 args mx 1
x2 <- askC canAsk p2 args mx 2
x3 <- askC canAsk p3 args mx 3
x4 <- askC canAsk p4 args mx 4
x5 <- askC canAsk p5 args mx 5
x6 <- askC canAsk p6 args mx 6
x7 <- askC canAsk p7 args mx 7
x8 <- askC canAsk p8 args mx 8
res <- f x0 x1 x2 x3 x4 x5 x6 x7 x8
return (res, L.drop (mx+1) args)
makeCommandN :: (MonadIO m, MonadCatch m, Functor m)
=> Text
-> (Text -> Bool)
-> Text
-> Bool
-> [Asker m a]
-> [Asker m a]
-> (Text -> [a] -> m z)
-> Command m Text z
makeCommandN n t d canAsk necc opt f = Command n t d f'
where
min = P.length necc
max = natLength necc + natLength opt
f' args = do neccParams <- unfoldrM (comb args) (necc,1, Nothing)
let x0 = maybe "" id (L.head args)
from = L.length neccParams + 1
to = Just $ L.length args 1
optParams <- unfoldrM (comb args) (opt, from, to)
let params = neccParams L.++ optParams
res <- f x0 params
return (res, L.drop (length params + 1) args)
comb _ ([],_,_) = return Nothing
comb inp (x:xs, i, j) =
if isJust j && fromJust j < i then return Nothing
else askC canAsk x inp min i >$> args xs >$> Just
where args ys y = (y,(ys,i+1,j))
askC True f xs _ i = ask f (xs L.!! i)
askC False f xs j i = maybe (throwM $ TooFewParamsError j (length xs 1)) (ask f . Just) (xs L.!! i)
summarizeCommands :: MonadIO m
=> [Command m2 i z]
-> m ()
summarizeCommands [] = return ()
summarizeCommands xs = liftIO $ mapM_ (\c -> prName c >> prDesc c) xs
where
maxLen :: Int
maxLen = fromIntegral
$ T.length
$ commandName
$ fromJust
$ L.minimumBy (comparing $ (* (1)) . T.length . commandName) xs
prName = putStr . padRight ' ' maxLen . commandName
prDesc = putStrLn . (" - " ++) . commandDesc
padRight c i cs = cs ++ replicate (i length cs) c
askC :: (MonadIO m, MonadCatch m, Functor m)
=> Bool -> Asker m a -> [Text] -> Int -> Int -> m a
askC True f xs _ i = ask f (xs L.!! i)
askC False f xs j i = maybe (throwM $ TooFewParamsError j (length xs 1)) (ask f . Just) (xs L.!! i)
makeREPL :: (Functor m, MonadIO m, MonadCatch m, Functor f, Foldable f)
=> [Command m Text a]
-> Command m Text b
-> Command m Text c
-> m Text
-> f (Handler m ())
-> m ()
makeREPL regular exit unknown prompt handlers = void $ iterateUntil id iter
where
iter = (prompt >>= runSingleCommand allCommands)
`catches` handlers'
handlers' = fmap (\(Handler f) -> Handler (\e -> f e >> return False)) handlers
exit' = fmap (const True) exit
regular' = L.map (fmap (const False)) regular
unknown' = fmap (const False) $ unknown{commandTest = const True}
allCommands = oneOf "" "" (exit' : regular' ++ [unknown'])