repl-toolkit 0.3.1.0 → 0.4.0.0
raw patch · 3 files changed
+427/−245 lines, 3 filesdep +semigroupoidsPVP ok
version bump matches the API change (PVP)
Dependencies added: semigroupoids
API changes (from Hackage documentation)
- System.REPL.Command: commandDispatch :: (MonadIO m, MonadCatch m, Functor m) => Text -> [Command m z] -> m z
- System.REPL.Command: commandInfo :: MonadIO m => Command m a -> m ()
- System.REPL.Command: instance Enum ParamNumError
- System.REPL.Command: instance Eq ParamNumError
- System.REPL.Command: instance Exception ParamNumError
- System.REPL.Command: instance Functor m => Functor (Command m)
- System.REPL.Command: instance Ord ParamNumError
- System.REPL.Command: instance Read ParamNumError
- System.REPL.Command: instance Show ParamNumError
- System.REPL.Command: instance Typeable ParamNumError
- System.REPL.Command: numParameters :: Command m a -> Maybe Int
- System.REPL.Command: runOnce :: MonadIO m => Text -> Command m a -> m (Maybe a)
+ System.REPL.Command: MalformedParamsError :: Text -> MalformedParamsError
+ System.REPL.Command: SomeCommandError :: e -> SomeCommandError
+ System.REPL.Command: TooFewParamsError :: Int -> Int -> TooFewParamsError
+ System.REPL.Command: TooManyParamsError :: Int -> Int -> TooManyParamsError
+ System.REPL.Command: data MalformedParamsError
+ System.REPL.Command: data SomeCommandError
+ System.REPL.Command: data TooFewParamsError
+ System.REPL.Command: data TooManyParamsError
+ System.REPL.Command: getName :: (Functor m, MonadThrow m) => Text -> m Text
+ System.REPL.Command: instance (Functor m, Monad m) => Apply (Command m i)
+ System.REPL.Command: instance (Functor m, Monad m) => Bind (Command m i)
+ System.REPL.Command: instance Eq MalformedParamsError
+ System.REPL.Command: instance Eq TooFewParamsError
+ System.REPL.Command: instance Eq TooManyParamsError
+ System.REPL.Command: instance Exception MalformedParamsError
+ System.REPL.Command: instance Exception SomeCommandError
+ System.REPL.Command: instance Exception TooFewParamsError
+ System.REPL.Command: instance Exception TooManyParamsError
+ System.REPL.Command: instance Functor m => Functor (Command m i)
+ System.REPL.Command: instance Ord MalformedParamsError
+ System.REPL.Command: instance Ord TooFewParamsError
+ System.REPL.Command: instance Ord TooManyParamsError
+ System.REPL.Command: instance Show MalformedParamsError
+ System.REPL.Command: instance Show SomeCommandError
+ System.REPL.Command: instance Show TooFewParamsError
+ System.REPL.Command: instance Show TooManyParamsError
+ System.REPL.Command: instance Typeable MalformedParamsError
+ System.REPL.Command: instance Typeable SomeCommandError
+ System.REPL.Command: instance Typeable TooFewParamsError
+ System.REPL.Command: instance Typeable TooManyParamsError
+ System.REPL.Command: 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
+ System.REPL.Command: 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
+ System.REPL.Command: 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 ()
+ System.REPL.Command: oneOf :: Monoid i => Text -> Text -> [Command m i a] -> Command m i a
+ System.REPL.Command: runPartialCommand :: Command m i a -> [i] -> m (a, [i])
+ System.REPL.Command: runSingleCommand :: (MonadThrow m, Functor m) => Command m Text a -> Text -> m a
+ System.REPL.Command: subcommand :: (Functor m, Monad m, Monoid i) => Command m i a -> [a -> Command m i b] -> Command m i b
- System.REPL.Command: Command :: Text -> (Text -> Bool) -> Text -> Maybe Int -> (Text -> m a) -> Command m a
+ System.REPL.Command: Command :: Text -> (i -> Bool) -> Text -> ([i] -> m (a, [i])) -> Command m i a
- System.REPL.Command: commandDesc :: Command m a -> Text
+ System.REPL.Command: commandDesc :: Command m i a -> Text
- System.REPL.Command: commandName :: Command m a -> Text
+ System.REPL.Command: commandName :: Command m i a -> Text
- System.REPL.Command: commandTest :: Command m a -> Text -> Bool
+ System.REPL.Command: commandTest :: Command m i a -> i -> Bool
- System.REPL.Command: data Command m a
+ System.REPL.Command: data Command m i a
- System.REPL.Command: makeCommand :: (MonadIO m, MonadCatch m, Functor m) => Text -> (Text -> Bool) -> Text -> (Text -> m a) -> Command m a
+ System.REPL.Command: makeCommand :: (MonadIO m, MonadCatch m, Functor m, Monoid i) => Text -> (i -> Bool) -> Text -> (i -> m z) -> Command m i z
- System.REPL.Command: makeCommand1 :: (MonadIO m, MonadCatch m, Functor m, Read a) => Text -> (Text -> Bool) -> Text -> Asker m a -> (Text -> a -> m z) -> Command m z
+ System.REPL.Command: makeCommand1 :: (MonadIO m, MonadCatch m, Functor m) => Text -> (Text -> Bool) -> Text -> Bool -> Asker m a -> (Text -> a -> m z) -> Command m Text z
- System.REPL.Command: makeCommand2 :: (MonadIO m, MonadCatch m, Functor m, Read a, Read b) => Text -> (Text -> Bool) -> Text -> Asker m a -> Asker m b -> (Text -> a -> b -> m z) -> Command m z
+ System.REPL.Command: 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
- System.REPL.Command: makeCommand3 :: (MonadIO m, MonadCatch m, Functor m, Read a, Read b, Read c) => Text -> (Text -> Bool) -> Text -> Asker m a -> Asker m b -> Asker m c -> (Text -> a -> b -> c -> m z) -> Command m z
+ System.REPL.Command: 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
- System.REPL.Command: makeCommand4 :: (MonadIO m, MonadCatch m, Functor m, Read a, Read b, Read c, Read d) => Text -> (Text -> Bool) -> Text -> Asker m a -> Asker m b -> Asker m c -> Asker m d -> (Text -> a -> b -> c -> d -> m z) -> Command m z
+ System.REPL.Command: 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
- System.REPL.Command: makeCommand5 :: (MonadIO m, MonadCatch m, Functor m, Read a, Read b, Read c, Read d, Read e) => Text -> (Text -> Bool) -> Text -> 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 z
+ System.REPL.Command: 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
- System.REPL.Command: makeCommand6 :: (MonadIO m, MonadCatch m, Functor m, Read a, Read b, Read c, Read d, Read e, Read f) => Text -> (Text -> Bool) -> Text -> 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 z
+ System.REPL.Command: 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
- System.REPL.Command: makeCommandN :: (MonadIO m, MonadCatch m, Functor m, Read a) => Text -> (Text -> Bool) -> Text -> [Asker m a] -> [Asker m a] -> (Text -> [a] -> m z) -> Command m z
+ System.REPL.Command: 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
- System.REPL.Command: readArgs :: Text -> Either Text [Text]
+ System.REPL.Command: readArgs :: MonadThrow m => Text -> m [Text]
- System.REPL.Command: runCommand :: Command m a -> Text -> m a
+ System.REPL.Command: runCommand :: (Functor m, Monad m, MonadThrow m) => Command m Text a -> Text -> m a
- System.REPL.Command: summarizeCommands :: MonadIO m => [Command m2 z] -> m ()
+ System.REPL.Command: summarizeCommands :: MonadIO m => [Command m2 i z] -> m ()
Files
- System/REPL/Command.hs +423/−243
- changelog.txt +2/−0
- repl-toolkit.cabal +2/−2
System/REPL/Command.hs view
@@ -1,44 +1,67 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE RankNTypes #-} --- |Provides Commands for REPLs. Commands take care of input --- and parameter-handling, and allow parameters to be supplied --- in the same line as the command's name (e.g. ":cmd param1 param2" on stdin). --- Provided parameters can be parsed and checked (say, against databases) --- before they are passed to the actual command function. --- They are relatively large units of abstraction, but they allow the easy --- creation of relatively sophisticated command loops, and have the advantage --- that one doesn't need to fiddle around with input handling in the middle --- of the actual command code. +-- |Provides Commands for REPLs. Commands are there to provide high-level +-- handling of user input and to offer functionality in a standard, composable +-- way. +-- +-- Use cases: +-- +-- 1. Asking the user for input that has to be of the right format or fulfil +-- specific constraints, such as asking for an @Int@ between 0 and 10, or +-- asking for a username that has to exist in some database. +-- 2. Creating command hierarchies, e.g. +-- +-- >>> myVersionControl commit "my first commit" only-binaries +-- +-- In this example, we could have @commit@ as a root command that +-- consumes the commit name "my first commit" and hands over to +-- the sub-command @only-binaries@. +-- +-- 3. Creating a REPL out of individual commands. Let us say that we have +-- a version control system with @commit, revert, diff@ commands, plus +-- an exit command like "exit", and one that prints some +-- "command not recognized" message if the user's input doesn't match +-- any commands. We can compose these commands with 'makeREPL', providing +-- robust command selection and obviating the need for manually writing the +-- input handling loop. +-- +-- Commands can take their input in one line, e.g. +-- +-- >>>:cmd param1 "param2 with spaces" param3 +-- +-- or they can ask for missing parameters if so configured: +-- +-- >>>:cmd param1 "param2 with spaces" +-- Please enter param3: module System.REPL.Command ( - -- *Command dispatch - -- |Using the 'Command' class is not necessary, but it makes dealing with - -- user input considerably easier. When a command is run with a line of - -- input, it automatically segments it by whitespace, tries to interpret - -- each part as one of its arguments and passes them to the actual command - -- function. If any arguments haven't been supplies, it asks for them on - -- stdin. If too many arguments have been supplied, or if any argument' - -- parsing returns an error, the command is aborted. - -- - -- Example: - -- - -- > cd = makeCommand1 ... - -- - -- >>> :cd ../ - -- Directory changed! - -- >>> :cd - -- Enter new directory: - -- >>> ../ - -- Directory changed! + -- *Command class Command(..), - commandInfo, - runOnce, - commandDispatch, - summarizeCommands, + runCommand, + runSingleCommand, + oneOf, + subcommand, + makeREPL, + -- *Exceptions + -- |These are the exceptions that can be thrown during the course of command + -- invocation (in addition to those that you throw yourself, of course). + -- + -- SomeCommandError is an abstract exception and all others are its concrete + -- subclasses. See the example in "Control.Exception" for details. + SomeCommandError(..), + MalformedParamsError(..), + TooFewParamsError(..), + TooManyParamsError(..), + -- * Dealing with arguments readArgs, + getName, quoteArg, - -- ** Making commands. + -- * Helpers + summarizeCommands, + -- * Making commands makeCommand, makeCommand1, makeCommand2, @@ -46,24 +69,29 @@ makeCommand4, makeCommand5, makeCommand6, + makeCommand7, + makeCommand8, makeCommandN, ) where -import Prelude hiding (putStrLn, putStr, getLine, unwords, words, (!!), (++), - length, replicate) +import Prelude hiding (putStrLn, putStr, (++), length, replicate) import qualified Prelude as P -import Control.Arrow (left) +import Control.Arrow (first) import Control.Monad import Control.Monad.Catch import Control.Monad.IO.Class (MonadIO(liftIO)) -import Control.Monad.Loops (unfoldrM) +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, isNothing, isJust) +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 @@ -78,50 +106,153 @@ (++) :: (ListLike full item) => full -> full -> full (++) = append +-- Exceptions +------------------------------------------------------------------------------- + +-- |Generic error related to command execution. +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} + +-- |The input of a command was malformed and could not interpreted. I.e. +-- the input contained inadmissible characters, or quotes were mismatched. +-- The 'Text' argument contains the parser error. +data MalformedParamsError = MalformedParamsError Text deriving (Show, Eq, Typeable, Ord) +instance Exception MalformedParamsError where + toException = commandErrorUpcast + fromException = commandErrorDowncast + +-- |Too many parameters were given to a command. The first value is the maximum, +-- the second the actual number. +data TooManyParamsError = TooManyParamsError Int Int deriving (Show, Eq, Typeable, Ord) +instance Exception TooManyParamsError where + toException = commandErrorUpcast + fromException = commandErrorDowncast + +-- |Too few parameters were given to a command. The first value is the minium, +-- the second the actual number. +data TooFewParamsError = TooFewParamsError Int Int deriving (Show, Eq, Typeable, Ord) +instance Exception TooFewParamsError where + toException = commandErrorUpcast + fromException = commandErrorDowncast + +-- Command type +------------------------------------------------------------------------------- + -- |A REPL command, possibly with parameters. -data Command m a = Command{ - -- |The short name of the command. Purely informative. - commandName :: Text, - -- |Returns whether a string matches - -- a command name. The simplest form is - -- @s==@ for some string s, but more liberal - -- matchings are possible. - commandTest :: Text -> Bool, - -- |A description of the command. - commandDesc :: Text, - -- |The number of parameters, if fixed. - numParameters :: Maybe Int, - -- |Runs the command with the input text as parameter. - runCommand :: Text -> m a} +data Command m i a = Command{ + -- |The short name of the command. Purely informative. + commandName :: Text, + -- |Returns whether the first part of an input + -- (the command name) matches + -- a the command. The simplest form is + -- @((==) . getPart) s@ for some string s, but more liberal + -- matchings are possible. + commandTest :: i -> Bool, + -- |A description of the command. + commandDesc :: Text, + -- |Runs the command with the input text as parameter, + -- returning the unconsumed input. + runPartialCommand :: [i] -> m (a, [i])} -instance Functor m => Functor (Command m) where - fmap f c@Command{runCommand=run} = c{runCommand=(fmap f . run)} +instance Functor m => Functor (Command m i) where + fmap f c@Command{runPartialCommand=run} = c{runPartialCommand=(fmap (first f) . run)} -data ParamNumError = NoParams | ExactParams | TooManyParams - deriving (Enum, Show, Eq, Read, Typeable, Ord) +instance (Functor m, Monad m) => Ap.Apply (Command m i) where + -- |Runs the first command, then the second with the left-over input. + -- The result of the first command is applied to that of the second. + -- + -- All other fields (name, description,...) of the second command are + -- ignored. + f <.> g = f{runPartialCommand = h} + where + h input = do (func, output) <- runPartialCommand f input + (arg, output') <- runPartialCommand g output + return (func arg, output') -instance Exception ParamNumError --- |Prints information (the command name, description and, if given, --- the number of parameters) about a command to the console. -commandInfo :: MonadIO m => Command m a -> m () -commandInfo c = liftIO $ do - putStr $ commandName c - putStrLn $ maybe "" ((" Parameters: " P.++) . show) (numParameters c) - putStrLn $ commandDesc c +instance (Functor m, Monad m) => Bi.Bind (Command m i) where + -- |The same as 'Ap.<.>', but the second argument can read the result of the + -- first. + f >>- g = f{runPartialCommand = h} + where + h input = do (res, output) <- runPartialCommand f input + (res', output') <- runPartialCommand (g res) output + return (res', output') --- |Splits and trims the input of a command. +-- |Runs the command with the input text as parameter, discarding any left-over +-- input. +runCommand :: (Functor m, Monad m, MonadThrow m) => Command m Text a -> Text -> m a +runCommand c = fmap fst . runPartialCommand c <=< readArgs + +-- |Runs the command with the input text as parameter. If any input is left +-- unconsumed, an error is thrown. +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 + + +-- |Takes a list @xs@ and executes the first command in a list whose +-- 'commandTest' matches the input. +-- +-- Note that the resultant command @c@'s' 'runPartialCommand' should only be +-- executed with an input @t@ if 'commandTest c t' == True', where @t'@ is either +-- @head (readArgs t)@ or @mempty@ if @t@ is empty. +-- Otherwise, the result is undefined. +oneOf :: Monoid i + => Text + -- ^Command name. + -> Text + -- ^Command description. + -> [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 + -- because of @test@, the list is guaranteed to be non-empty + cmd input = (`runPartialCommand` input) + . LU.head + . L.dropWhile (not . ($ fromMaybe mempty (L.head input)) . commandTest) $ xs + +-- |Adds a list of possible subcommands after a command (that should leave +-- some input unconsumed). Ignoring all the required parameters for a moment, +-- +-- > subcommand x xs = x >>- oneOf xs +subcommand :: (Functor m, Monad m, Monoid i) + => Command m i a + -- ^The root command. + -> [a -> Command m i b] + -- ^The subcommands that may follow it. This list must be finite. + -> Command m i b +subcommand x xs = x Bi.>>- \y -> oneOf "" "" (L.map ($ y) xs) + + +-- |Splits and trims the input of a command. If the input cannot be parsed, a +-- 'MalformedCommand' exception is thrown. +-- +-- -- * Format +-- -- Any non-whitespace sequence of characters is interpreted as -- one argument, unless double quotes (") are used, in which case -- they demarcate an argument. Each argument is parsed as a haskell -- string literal (quote-less arguments have quotes inserted around them). --- If the number of quotes in the input is not even, the operating will fail. -- -- Arguments are parsed using parsec's @stringLiteral@ (haskell-style), -- meaning that escape sequences and unicode characters are handled automatically. -readArgs :: Text -> Either Text [Text] -readArgs = (left $ T.pack . show) . P.parse parser "" . T.unpack +readArgs :: MonadThrow m => Text -> m [Text] +readArgs = either err return . P.parse parser "" . T.unpack where + err = throwM . MalformedParamsError . T.pack . show -- Main parser. parser = P.many (stringLiteral P.<|> unquotedLiteral) @@ -142,212 +273,233 @@ case res of (Right r) -> return r (Left l) -> fail (show l) --- |Takes a line of text and a command. --- If the text matches the given command's 'commandTest', --- the command is run with it. If not, 'Nothing' is returned. -runOnce :: MonadIO m => Text -> Command m a -> m (Maybe a) -runOnce l c = if commandTest c l then liftM Just (runCommand c l) - else return Nothing - - --- |Returns an error message if an unexpected number of parameters have been --- supplied. -paramErr :: Text -- ^The command name. - -> [Text] -- ^The given input. - -> Int -- ^The minimum number of parameters. - -> Nat -- ^The maximum number of parameters. May be infinite if there - -- is no upper bound. - -> ParamNumError -- ^The kind of error that occurred. - -> Text -paramErr c inp minNum maxNum errType = - "The following " ++ T.pack (show num) ++ " parameters were given to " ++ c ++ ":\n" - ++ T.intercalate " " (maybe [] (L.map wrap) $ L.tail inp) ++ ".\n" - ++ (numErr LU.!! fromEnum errType) - where - -- wraps the argument in quotation marks if it contains a space - wrap t = if T.any isSpace t then "\"" ++ t ++ "\"" else t - -- number of arguments (excluding the command name) - num = L.length inp - 1 - -- error message regarding how many parameters the command takes - numErr = [c ++ " takes no parameters.", - c ++ " takes " ++ T.pack (show minNum) ++ " parameters.", - c ++ " takes at most " ++ T.pack (show (fromPeano maxNum :: Integer)) ++ " parameters."] - --- |Checks the number of parameters before executing a monadic function. --- Only AskFailures (and IOExceptions) will be thrown in this function. -checkParams :: (MonadIO m, MonadThrow m, Functor m) - => Text -- ^The command name. - -> Text -- ^The raw input (including the command name). - -> Int -- ^The minimal number of parameters, excluding the command's name. - -> Nat -- ^The maximal number of parameters, excluding the command's name. - -- This may be infinity if there is no upper bound. - -> ([Text] -> m a) -- ^The command. - -> m a -- ^Result. If too many parameters were - -- passed, this will be a 'ParamNumFailure'. -checkParams n inp minNum maxNum m = - case readArgs inp of - Left l -> throwM (ParamFailure l) - Right r -> - if natLength r > maxNum + 1 then - throwM $ ParamFailure - $ paramErr n r minNum maxNum (errKind $ natLength r) - else m r - where - errKind len = if minNum == 0 && 0 == maxNum then NoParams - else if maxNum < len then TooManyParams - else ExactParams +-- |Gets the first part of a command string, or the empty string, if the comand +-- string is empty. +getName :: (Functor m, MonadThrow m) => Text -> m Text +getName = fromMaybe mempty . L.head <$=< readArgs -- |Surrounds an argument in quote marks, if necessary. -- This is useful when arguments were extracted via 'readArgs', which deletes --- quote marks. Quotes are placed around the input iff it doesn't begin with --- a quote mark (\"). --- 'readArgs' and 'quoteArg' are inverse up to suitable isomorphism, i.e. --- if 'readArgs orig = (Right res)', then it holds that --- @readArgs orig = readArgs $ intercalate " " $ map quoteArg res@ +-- quote marks. Quotes are placed around the input iff it is empty or contains +-- whitespace. quoteArg :: Text -> Text -quoteArg x = if T.null x || T.head x /= '\"' - then '\"' `T.cons` x `T.snoc` '\"' - else x +quoteArg x = if T.null x || T.any isSpace x then '\"' `T.cons` x `T.snoc` '\"' + else x + -- |Creates a command without parameters. makeCommand :: (MonadIO m, MonadCatch m, - Functor m) + Functor m, Monoid i) => Text -- ^Command name. - -> (Text -> Bool) -- ^Command test. + -> (i -> Bool) -- ^Command test. -> Text -- ^Command description. - -> (Text -> m a) -- ^The actual command. - -> Command m a -makeCommand n t d f = - Command n t d (Just 0) (\inp -> checkParams n inp 0 0 c) + -> (i -> m z) + -- ^Command function. It will receive the first part of the input + -- (customarily the command name), or the empty string if the + -- input only contained whitespace. + -> Command m i z +makeCommand n t d f = Command n t d f' where - c inp = do let li = maybe "" id (L.head inp) - f li + f' args = do res <- f $ fromMaybe mempty $ L.head args + return (res, L.drop 1 args) + -- |Creates a command with one parameter. -makeCommand1 :: (MonadIO m, MonadCatch m, Functor m, Read a) +makeCommand1 :: (MonadIO m, MonadCatch m, Functor m) => Text -- ^Command name. -> (Text -> Bool) -- ^Command test. -> Text -- ^Command description + -> Bool -- ^Whether the command can ask for input. + -- ^If True, running the command will run the Asker's + -- IO action if not enough input is provided. If False + -- a 'ParamNumError' will be thrown. -> Asker m a -- ^'Asker' for the first parameter. - -> (Text -> a -> m z) - -> Command m z -makeCommand1 n t d p1 f = - Command n t d (Just 1) (\inp -> checkParams n inp 1 1 c) + -> (Text -> a -> m z) -- ^Command function. + -> Command m Text z +makeCommand1 n t d canAsk p1 f = Command n t d f' where - c inp = do let li = maybe "" id (L.head inp) - x1 <- ask p1 (inp L.!! 1) - f li x1 + 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) -- |Creates a command with two parameters. -makeCommand2 :: (MonadIO m, MonadCatch m, Functor m, Read a, - Read b) +makeCommand2 :: (MonadIO m, MonadCatch m, Functor m) => Text -- ^Command name. -> (Text -> Bool) -- ^Command test. -> Text -- ^Command description + -> Bool -- ^Whether the command can ask for input. -> Asker m a -- ^'Asker' for the first parameter. - -> Asker m b -- ^'Asker' for the second perameter. - -> (Text -> a -> b -> m z) - -> Command m z -makeCommand2 n t d p1 p2 f = - Command n t d (Just 2) (\inp -> checkParams n inp 2 2 c) + -> Asker m b -- ^'Asker' for the second parameter. + -> (Text -> a -> b -> m z) -- ^Command function. + -> Command m Text z +makeCommand2 n t d canAsk p1 p2 f = Command n t d f' where - c inp = do let li = maybe "" id (L.head inp) - x1 <- ask p1 (inp L.!! 1) - x2 <- ask p2 (inp L.!! 2) - f li x1 x2 + 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) -- |Creates a command with three parameters. -makeCommand3 :: (MonadIO m, MonadCatch m, Functor m, Read a, - Read b, Read c) +makeCommand3 :: (MonadIO m, MonadCatch m, Functor m) => Text -- ^Command name. -> (Text -> Bool) -- ^Command test. -> Text -- ^Command description + -> Bool -- ^Whether the command can ask for input. -> Asker m a -- ^'Asker' for the first parameter. - -> Asker m b -- ^'Asker' for the second perameter. + -> Asker m b -- ^'Asker' for the second parameter. -> Asker m c -- ^'Asker' for the third parameter. - -> (Text -> a -> b -> c -> m z) - -> Command m z -makeCommand3 n t d p1 p2 p3 f = - Command n t d (Just 3) (\inp -> checkParams n inp 3 3 c) + -> (Text -> a -> b -> c -> m z) -- ^Command function. + -> Command m Text z +makeCommand3 n t d canAsk p1 p2 p3 f = Command n t d f' where - c inp = do let li = maybe "" id (L.head inp) - x1 <- ask p1 (inp L.!! 1) - x2 <- ask p2 (inp L.!! 2) - x3 <- ask p3 (inp L.!! 3) - f li x1 x2 x3 + 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) -- |Creates a command with four parameters. -makeCommand4 :: (MonadIO m, MonadCatch m, Functor m, Read a, - Read b, Read c, Read d) +makeCommand4 :: (MonadIO m, MonadCatch m, Functor m) => Text -- ^Command name. -> (Text -> Bool) -- ^Command test. -> Text -- ^Command description + -> Bool -- ^Whether the command can ask for input. -> Asker m a -- ^'Asker' for the first parameter. - -> Asker m b -- ^'Asker' for the second perameter. + -> Asker m b -- ^'Asker' for the second parameter. -> Asker m c -- ^'Asker' for the third parameter. -> Asker m d -- ^'Asker' for the fourth parameter. - -> (Text -> a -> b -> c -> d -> m z) - -> Command m z -makeCommand4 n t d p1 p2 p3 p4 f = - Command n t d (Just 4) (\inp -> checkParams n inp 4 4 c) + -> (Text -> a -> b -> c -> d -> m z) -- ^Command function. + -> Command m Text z +makeCommand4 n t d canAsk p1 p2 p3 p4 f = Command n t d f' where - c inp = do let li = maybe "" id (L.head inp) - x1 <- ask p1 (inp L.!! 1) - x2 <- ask p2 (inp L.!! 2) - x3 <- ask p3 (inp L.!! 3) - x4 <- ask p4 (inp L.!! 4) - f li x1 x2 x3 x4 + 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) -- |Creates a command with five parameters. -makeCommand5 :: (MonadIO m, MonadCatch m, Functor m, Read a, - Read b, Read c, Read d, Read e) +makeCommand5 :: (MonadIO m, MonadCatch m, Functor m) => Text -- ^Command name. -> (Text -> Bool) -- ^Command test. -> Text -- ^Command description + -> Bool -- ^Whether the command can ask for input. -> Asker m a -- ^'Asker' for the first parameter. - -> Asker m b -- ^'Asker' for the second perameter. + -> Asker m b -- ^'Asker' for the second parameter. -> Asker m c -- ^'Asker' for the third parameter. -> Asker m d -- ^'Asker' for the fourth parameter. -> Asker m e -- ^'Asker' for the fifth parameter. - -> (Text -> a -> b -> c -> d -> e -> m z) - -> Command m z -makeCommand5 n t d p1 p2 p3 p4 p5 f = - Command n t d (Just 4) (\inp -> checkParams n inp 5 5 c) + -> (Text -> a -> b -> c -> d -> e -> m z) -- ^Command function. + -> Command m Text z +makeCommand5 n t d canAsk p1 p2 p3 p4 p5 f = Command n t d f' where - c inp = do let li = maybe "" id (L.head inp) - x1 <- ask p1 (inp L.!! 1) - x2 <- ask p2 (inp L.!! 2) - x3 <- ask p3 (inp L.!! 3) - x4 <- ask p4 (inp L.!! 4) - x5 <- ask p5 (inp L.!! 5) - f li x1 x2 x3 x4 x5 + 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) --- |Creates a command with four parameters. -makeCommand6 :: (MonadIO m, MonadCatch m, Functor m, Read a, - Read b, Read c, Read d, Read e, Read f) +-- |Creates a command with six parameters. +makeCommand6 :: (MonadIO m, MonadCatch m, Functor m) => Text -- ^Command name. -> (Text -> Bool) -- ^Command test. -> Text -- ^Command description + -> Bool -- ^Whether the command can ask for input. -> Asker m a -- ^'Asker' for the first parameter. - -> Asker m b -- ^'Asker' for the second perameter. + -> Asker m b -- ^'Asker' for the second parameter. -> Asker m c -- ^'Asker' for the third parameter. -> Asker m d -- ^'Asker' for the fourth parameter. -> Asker m e -- ^'Asker' for the fifth parameter. -> Asker m f -- ^'Asker' for the sixth parameter. - -> (Text -> a -> b -> c -> d -> e -> f -> m z) - -> Command m z -makeCommand6 n t d p1 p2 p3 p4 p5 p6 f = - Command n t d (Just 4) (\inp -> checkParams n inp 6 6 c) + -> (Text -> a -> b -> c -> d -> e -> f -> m z) -- ^Command function. + -> Command m Text z +makeCommand6 n t d canAsk p1 p2 p3 p4 p5 p6 f = Command n t d f' where - c inp = do let li = maybe "" id (L.head inp) - x1 <- ask p1 (inp L.!! 1) - x2 <- ask p2 (inp L.!! 2) - x3 <- ask p3 (inp L.!! 3) - x4 <- ask p4 (inp L.!! 4) - x5 <- ask p5 (inp L.!! 5) - x6 <- ask p6 (inp L.!! 6) - f li x1 x2 x3 x4 x5 x6 + 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) +-- |Creates a command with seven parameters. +makeCommand7 :: (MonadIO m, MonadCatch m, Functor m) + => Text -- ^Command name. + -> (Text -> Bool) -- ^Command test. + -> Text -- ^Command description + -> Bool -- ^Whether the command can ask for input. + -> Asker m a -- ^'Asker' for the first parameter. + -> Asker m b -- ^'Asker' for the second parameter. + -> Asker m c -- ^'Asker' for the third parameter. + -> Asker m d -- ^'Asker' for the fourth parameter. + -> Asker m e -- ^'Asker' for the fifth parameter. + -> Asker m f -- ^'Asker' for the sixth parameter. + -> Asker m g -- ^'Asker' for the seventh parameter. + -> (Text -> a -> b -> c -> d -> e -> f -> g -> m z) -- ^Command function. + -> 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) + +-- |Creates a command with eight parameters. +makeCommand8 :: (MonadIO m, MonadCatch m, Functor m) + => Text -- ^Command name. + -> (Text -> Bool) -- ^Command test. + -> Text -- ^Command description + -> Bool -- ^Whether the command can ask for input. + -> Asker m a -- ^'Asker' for the first parameter. + -> Asker m b -- ^'Asker' for the second parameter. + -> Asker m c -- ^'Asker' for the third parameter. + -> Asker m d -- ^'Asker' for the fourth parameter. + -> Asker m e -- ^'Asker' for the fifth parameter. + -> Asker m f -- ^'Asker' for the sixth parameter. + -> Asker m g -- ^'Asker' for the seventh parameter. + -> Asker m h -- ^'Asker' for the eighth parameter. + -> (Text -> a -> b -> c -> d -> e -> f -> g -> h -> m z) -- ^Command function. + -> 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) + + -- |Creates a command with a list of parameters. -- The first list @necc@ of 'Asker's indicates the necessary parameters; -- the user must at least provide this many. The second list @opt@ contains @@ -355,26 +507,29 @@ -- If the number of passed parameters exceeds -- @length necc + length opt@, or if any 'Asker' fails, -- the command returns an 'AskFailure'. -makeCommandN :: (MonadIO m, MonadCatch m, Functor m, Read a) +makeCommandN :: (MonadIO m, MonadCatch m, Functor m) => Text -- ^Command name. -> (Text -> Bool) -- ^Command test. -> Text -- ^Command description + -> Bool -- ^Whether the command can ask for input. This only + -- affects the necessary parameters. -> [Asker m a] -- ^'Asker's for the necessary parameters. -> [Asker m a] -- ^'Asker's for the optional parameters. -> (Text -> [a] -> m z) - -> Command m z -makeCommandN n t d necc opt f = Command n t d Nothing (\inp -> checkParams n inp min max c) + -> 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 - c inp = do let li = maybe "" id (L.head inp) - neccParams <- unfoldrM (comb inp) (necc,1, Nothing) - let from = L.length neccParams + 1 - to = Just $ L.length inp - 1 - - optParams <- unfoldrM (comb inp) (opt, from, to) - f li (neccParams L.++ optParams) + 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) -- |Goes through the list of askers until all are done or until the first -- AskFailure occurs. The results are of type @Either (AskFailure e) z@, @@ -383,33 +538,16 @@ comb _ ([],_,_) = return Nothing comb inp (x:xs, i, j) = if isJust j && fromJust j < i then return Nothing - else ask x (inp L.!! i) >$> args xs >$> Just + else askC canAsk x inp min i >$> args xs >$> Just where args ys y = (y,(ys,i+1,j)) --- |Takes an input and tries to run it against a list of commands, --- trying the out in sequence. The first command whose 'commandTest' --- returns True is executed. If none of the commands match, --- @NothingFoundFailure@ is thrown. -commandDispatch :: (MonadIO m, MonadCatch m, Functor m) - => Text -- ^The user's input. - -> [Command m z] -- ^The command library. - -> m z -commandDispatch input cs = - case readArgs input of - Left l -> throwM (ParamFailure l) - Right input' -> if noMatch input' - then throwM NothingFoundFailure - else do runCommand (fromJust $ first input') input - where - noMatch = isNothing . first - firstArg = maybe "" id . L.head - first r = L.head $ P.dropWhile (not . flip commandTest (firstArg r)) cs - + 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) -- |Prints out a list of command names, with their descriptions. summarizeCommands :: MonadIO m - => [Command m2 z] + => [Command m2 i z] -> m () summarizeCommands [] = return () summarizeCommands xs = liftIO $ mapM_ (\c -> prName c >> prDesc c) xs @@ -424,3 +562,45 @@ 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) + + +-- |Runs a REPL based on a set of commands. +-- For a line of input, the commands are tried in following order: +-- +-- * the "exit" command, +-- * all regular commands, and then +-- * the "unknown" command. +makeREPL :: (Functor m, MonadIO m, MonadCatch m, Functor f, Foldable f) + => [Command m Text a] + -- ^The regular commands. + -> Command m Text b + -- ^The "exit" command which terminates the loop. + -> Command m Text c + -- ^The command that is called when none of the others match. + -- This one's 'commandTest' is replaced with @const True@. + -> m Text + -- ^The asker to execute before each command (i.e. the prompt). + -> f (Handler m ()) + -- ^Handlers for any exceptions that may arise. Generally, you + -- will want to handle at least the exceptions of this module + -- ('SomeCommandError', 'MalformedParamsError', 'TooManyParamsError', + -- 'TooFewParamsError'), and whatever the 'Asker' can throw. + -> m () + -- ^Asks the user repeatedly for input, until the input matches + -- the command test of the "exit" command. +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'])
changelog.txt view
@@ -1,3 +1,5 @@+0.4 Completely overhauled the command system. It is now possible to chain commands together and make subcommands. + 0.3.1 Added functionality for reading configuration files. 0.3 Ditched MonadError in favour of MonadThrow. This should make the functions much easier to use.
repl-toolkit.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/ name: repl-toolkit -version: 0.3.1.0 +version: 0.4.0.0 synopsis: Toolkit for quickly whipping up config files and command-line interfaces. description: A simple toolkit for quickly whipping up REPLs, input validation and sets of commands included. homepage: https://github.com/ombocomp/repl-toolkit @@ -23,6 +23,6 @@ exposed-modules: System.REPL, System.REPL.State, System.REPL.Command, System.REPL.Config other-extensions: OverloadedStrings, DeriveDataTypeable, FlexibleContexts, LambdaCase, ScopedTypeVariables - build-depends: base >=4.7 && <5, functor-monadic >=0.1, text >=1.1, ListLike >=4.1, exceptions >=0.4, parsec >=3.1, numericpeano >= 0.1, listsafe >= 0.1, monad-loops >= 0.3, mtl >= 2.2, transformers >= 0.3, directory >= 1.2.1, system-filepath >= 0.4.13, bytestring >= 0.10, data-default >= 0.5.3, aeson >=0.8.0.2 + build-depends: base >=4.7 && <5, functor-monadic >=0.1, text >=1.1, ListLike >=4.1, exceptions >=0.4, parsec >=3.1, numericpeano >= 0.1, listsafe >= 0.1, monad-loops >= 0.3, mtl >= 2.2, transformers >= 0.3, directory >= 1.2.1, system-filepath >= 0.4.13, bytestring >= 0.10, data-default >= 0.5.3, aeson >=0.8.0.2, semigroupoids >= 4 && <5 default-language: Haskell2010