{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE TupleSections #-} -- |Asking the user for input on the console. -- -- The main type is 'Asker', which takes care of parsing -- and verifying user input. module System.REPL.Ask ( -- *Types PromptMsg, TypeError, PredicateError, Predicate, Predicate', Parser, Asker(..), Asker', -- ** Exceptions SomeREPLError(..), SomeAskerError(..), AskerTypeError(..), AskerPredicateError(..), GenericTypeError(..), GenericPredicateError(..), genericTypeError, genericPredicateError, -- * Creating askers -- |These are all just convenience functions. -- You can also create 'Asker's directly via the constructor. -- -- For errors, you can supply a custom exception or use 'GenericTypeError', -- 'GenericPredicateError'. typeAskerP, maybeAskerP, -- **Creating askers via 'Read' -- |These askers use 'Text.Read.readMaybe' as their parser. -- -- It is possible to ask for Strings, but then quotes will be required -- around them (per their Read-instance). To get the user's -- input as-is, use the 'Verbatim' type or 'predAsker'. Verbatim(..), readParser, asker, lineAsker, typeAsker, predAsker, maybeAsker, -- *Running askers -- |Created askers can be run via these functions. -- Since the parsing depends on the Read-instance, the expected result type -- must be explicitly given. E.g.: -- -- @ -- intAsker :: Asker IO Int -- intAsker = typeAsker "> " "Expected Int!" -- @ -- -- or, for polymorphic askers, -- -- @ -- genericAsk :: Read a => Asker IO a -- genericAsk = typeAsker "> " "Couldn't parse value!" -- ... -- do (x :: Int) <- genericAsk -- (y :: Int) <- genericAsk -- putStrLn $ "The sum is: " ++ show (x+y) -- @ ask, ask', askEither, untilValid, -- *Creating predicates boolPredicate, -- *Example askers -- |A few askers for convenience. PathRootDoesNotExist(..), PathIsNotWritable(..), PathExistenceType(..), filepathAsker, writableFilepathAsker, ) where import Prelude hiding (putStrLn, putStr, getLine, reverse) import Control.Arrow (right, (|||)) import Control.Monad.Catch import Control.Monad.IO.Class (MonadIO(liftIO)) import Data.Char (isSpace) import Data.Functor.Monadic import qualified Data.List as L import qualified Data.Text as T import qualified System.Directory as D import qualified System.FilePath as FP import qualified System.IO.Error as ERR import System.REPL.Prompt import System.REPL.Types import Text.Read (readMaybe) -- Askers ------------------------------------------------------------------------------- -- |Creates an 'Asker' which only cares about the type of the input. typeAskerP :: Applicative m => PromptMsg -> Parser a -> Asker' m a typeAskerP pr parse = Asker pr parse (pure . Right) -- |An asker which asks for an optional value. If only whitespace -- is entered (according to 'Data.Char.isSpace'), it returns 'Nothing' -- without further parsing or checking; otherwise, it behaves identically -- to 'asker'. maybeAskerP :: Applicative m => PromptMsg -> Parser a -> Predicate m a b -> Asker m (Maybe a) (Maybe b) maybeAskerP pr parse pred = Asker pr parse' check where parse' t = if T.all isSpace t then Right Nothing else right Just $ parse t check Nothing = pure $ Right Nothing check (Just t) = pred t >$> (\case Right t -> Right (Just t) Left err -> Left err) -- Parsers based on Read ------------------------------------------------------------------------------- -- |A parser based on 'Text.Read.readMaybe'. This suffices for the parsing of -- most data types. readParser :: Read a => (T.Text -> TypeError) -> Parser a readParser errT t = maybe (Left $ errT t) Right . readMaybe . T.unpack $ t -- |Creates a general 'Asker' with 'Text.Read.readMaybe' as its parser. -- Using 'Data.Read.readMaybe' is perfectly fine for most values, keep in mind -- that the input Text has to be unpacked into a string. This can be costly -- on very large inputs. -- -- __NOTE:__ Instances of String/Text have to be surrounded with quotes (\"). -- You practically never want this when asking for input. -- If you want to get the user input as-is, restrict the return type to -- @Asker m Verbatim@ or use 'predAsker'/'lineAsker'. asker :: (Functor m, Read a) => PromptMsg -> (T.Text -> TypeError) -> Predicate' m a -> Asker' m a asker pr errT pred = Asker pr (readParser errT) pred -- |Creates an 'Asker' based on Read which just cares about the type of the input. typeAsker :: (Applicative m, Read a) => PromptMsg -> (T.Text -> TypeError) -> Asker' m a typeAsker p errT = asker p errT (pure . Right) -- |Creates an 'Asker' which takes its input verbatim as 'Text'. -- Quotes around the input are not required. -- The input thus only has to pass a predicate, not any parsing. predAsker :: (Functor m) => PromptMsg -> Predicate m T.Text b -> Asker m T.Text b predAsker pr f = Asker pr Right f -- |A wrapper around 'getLine'. Prints no prompt and returns the user input as-is. lineAsker :: Applicative m => Asker' m T.Text lineAsker = predAsker "" (pure . Right) -- |An asker based on Read which asks for an optional value. maybeAsker :: (Applicative m, Read a) => PromptMsg -> (T.Text -> TypeError) -> Predicate' m a -> Asker' m (Maybe a) maybeAsker pr errT pred = maybeAskerP pr (readParser errT) pred -- Running askers -------------------------------------------------------------------------------- -- |Executes an Asker. A 'SomeAskerError' is thrown if the inpout can't be -- parsing into a value of the correct type, if the input fails the 'Asker''s -- predicate, or if the escape key is pressed. ask :: (MonadIO m, MonadCatch m) => Asker m a b -> Maybe T.Text -> m b ask a v = askEither a v >>= either throwM return -- |See 'ask'. Always reads the input from stdin. -- -- @ -- ask' a = ask a Nothing -- @ ask' :: (MonadIO m, MonadCatch m) => Asker m a b -> m b ask' a = ask a Nothing -- |Executes an 'Asker'. If the Text argument is Nothing, the user is asked -- to enter a line on stdin. If it is @Just x@, @x@ is taken to be input. -- -- Pressing the escape key returns a 'AskerInputAborterError' (if supported). askEither :: (MonadIO m, MonadCatch m) => Asker m a b -> Maybe T.Text -> m (Either SomeAskerError b) askEither a = maybe getInput check where getInput = (promptAbort '\ESC' (askerPrompt a) >>= check) `catch` (return . Left) check inp = case askerParser a inp of Left err -> return . Left . SomeAskerError . AskerTypeError $ err Right t -> askerPredicate a t >>= return . (Left . SomeAskerError . AskerPredicateError ||| Right) -- |Repeatedly executes an ask action until the user enters a valid value. -- Error messages are printed each time. untilValid :: forall m a.(MonadIO m, MonadCatch m, Read a) => m a -> m a untilValid m = m `catch` handler where handler :: SomeAskerError -> m a handler l = liftIO (putStrLn $ show l) >> untilValid m -- Creating predicates ------------------------------------------------------------------------------- -- |Creates a predicate from a boolean function and an error message. boolPredicate :: Functor m => (a -> m Bool) -> (a -> PredicateError) -> Predicate' m a boolPredicate f errP t = (\case {True -> Right t; False -> Left (errP t)}) <$> f t -- Example askers ------------------------------------------------------------------------------- -- |Asks the user for a file or a directory. -- -- Parsing checks for basic validity via 'System.FilePath.isValid'. Invalid paths are rejected. -- -- After that, the asker determines whether the target exists and what type -- it has. You can run a predicate on that information. filepathAsker :: MonadIO m => PromptMsg -> (FilePath -> TypeError) -> Predicate m (PathExistenceType, FilePath) b -> Asker m FilePath b filepathAsker pr errT pred = Asker pr parse pred' where parse = (\fp -> if FP.isValid fp then Right fp else Left $ errT fp) . T.unpack pred' fp = do exType <- liftIO $ getExistenceType fp pred (exType, fp) --return $ if ok then Right (exType, fp) -- else Left $ errP (exType, fp) getExistenceType :: FilePath -> IO PathExistenceType getExistenceType fp = do isDir <- D.doesDirectoryExist fp if isDir then return IsDirectory else do isFile <- D.doesFileExist fp return $ if isFile then IsFile else DoesNotExist -- |See 'filepathAsker'. This 'Asker' also ensures that the given path -- is writeable in the following sense: -- -- * at least some initial part of the path exists and -- * the last existing part of the path is writeable. -- -- 'PathRootDoesNotExist' and 'PathIsNotWritable' exceptions are thrown if the -- first or second of these conditions is violated. -- -- For relative paths, we only check that the current directory is writable. -- -- Handled exceptions: -- -- * 'System.IO.Error.isPermissionError' -- * 'System.IO.Error.isDoesNotExistError' writableFilepathAsker :: MonadIO m => PromptMsg -> (FilePath -> TypeError) -> Predicate m (PathExistenceType, FilePath) b -> Asker m FilePath b writableFilepathAsker pr errT pred = filepathAsker pr errT pred' where permError e = if ERR.isPermissionErrorType (ERR.ioeGetErrorType e) || ERR.isDoesNotExistErrorType (ERR.ioeGetErrorType e) then Just () else Nothing conc :: [FilePath] -> FilePath conc = L.foldl' (FP.) "" doesExist fp = (||) <$> D.doesDirectoryExist (conc fp) <*> D.doesFileExist (conc fp) isWritable fp = catchJust permError (fp >>= D.getPermissions >$> D.writable) (const $ return False) -- A utility function which gets a bool and returns the second argument if its value is false, -- and the third if its true. boolEither :: (Monad m, Exception a) => (m Bool) -> a -> m (Either SomeException b) -> m (Either SomeException b) boolEither x falseCase trueCase = x >>= (\case{True -> trueCase; False -> return $ Left $ SomeException falseCase}) pred' args@(_, fp) = if FP.isRelative fp then boolEither (liftIO $ isWritable D.getCurrentDirectory) (PathIsNotWritable fp) (pred args) else do existingRoot <- liftIO $ takeWhile snd <$> mapM (\x -> (x,) <$> doesExist x) (L.inits $ FP.splitDirectories fp) if null existingRoot then return (Left $ SomeException $ PathRootDoesNotExist fp) else boolEither (liftIO $ isWritable (return . conc . fst . last $ existingRoot)) (PathIsNotWritable fp) (pred args)