mirror of
https://github.com/GrammaticalFramework/gf-core.git
synced 2026-04-10 05:29:30 -06:00
+ Generalize the CommandInfo type by parameterizing it on the monad
instead of just the environment.
+ Generalize the commands defined in
GF.Command.{Commands,Commands2,CommonCommands,SourceCommands,HelpCommand}
to work in any monad that supports the needed operations.
+ Liberate GF.Command.Interpreter from the IO monad.
Also, move the current PGF from CommandEnv to GFEnv in
GF.Interactive, making the command interpreter even more generic.
+ Use a state monad to maintain the state of the interpreter in
GF.{Interactive,Interactive2}.
421 lines
14 KiB
Haskell
421 lines
14 KiB
Haskell
{-# LANGUAGE CPP, ScopedTypeVariables, FlexibleInstances #-}
|
||
-- | GF interactive mode
|
||
module GF.Interactive (mainGFI,mainRunGFI,mainServerGFI) where
|
||
import Prelude hiding (putStrLn,print)
|
||
import qualified Prelude as P(putStrLn)
|
||
import GF.Command.Interpreter(CommandEnv(..),mkCommandEnv,interpretCommandLine)
|
||
--import GF.Command.Importing(importSource,importGrammar)
|
||
import GF.Command.Commands(flags,options,PGFEnv,HasPGFEnv(..),pgf,pgfEnv,pgfCommands)
|
||
import GF.Command.CommonCommands(commonCommands,extend)
|
||
import GF.Command.SourceCommands
|
||
--import GF.Command.CommandInfo(mapCommandEnv,liftCommandInfo)
|
||
import GF.Command.Help(helpCommand)
|
||
import GF.Command.Abstract
|
||
import GF.Command.Parse(readCommandLine,pCommand)
|
||
import GF.Data.Operations (Err(..),done)
|
||
import GF.Data.Utilities(repeatM)
|
||
import GF.Grammar hiding (Ident,isPrefixOf)
|
||
import GF.Infra.UseIO(ioErrorText,putStrLnE)
|
||
import GF.Infra.SIO
|
||
import GF.Infra.Option
|
||
import qualified System.Console.Haskeline as Haskeline
|
||
--import GF.Text.Coding(decodeUnicode,encodeUnicode)
|
||
|
||
--import GF.Compile.Coding(codeTerm)
|
||
|
||
import PGF
|
||
import PGF.Internal(abstract,funs,lookStartCat,emptyPGF)
|
||
|
||
import Data.Char
|
||
import Data.List(isPrefixOf)
|
||
import qualified Data.Map as Map
|
||
import qualified Text.ParserCombinators.ReadP as RP
|
||
--import System.IO(utf8)
|
||
--import System.CPUTime(getCPUTime)
|
||
import System.Directory({-getCurrentDirectory,-}getAppUserDataDirectory)
|
||
import Control.Exception(SomeException,fromException,evaluate,try)
|
||
import Control.Monad.State
|
||
import qualified GF.System.Signal as IO(runInterruptibly)
|
||
#ifdef SERVER_MODE
|
||
import GF.Server(server)
|
||
#endif
|
||
|
||
import GF.Command.Messages(welcome)
|
||
|
||
-- | Run the GF Shell in quiet mode (@gf -run@).
|
||
mainRunGFI :: Options -> [FilePath] -> IO ()
|
||
mainRunGFI opts files = shell (beQuiet opts) files
|
||
|
||
beQuiet = addOptions (modifyFlags (\f -> f{optVerbosity=Quiet}))
|
||
|
||
-- | Run the interactive GF Shell
|
||
mainGFI :: Options -> [FilePath] -> IO ()
|
||
mainGFI opts files = do
|
||
P.putStrLn welcome
|
||
shell opts files
|
||
|
||
shell opts files = flip evalStateT emptyGFEnv $
|
||
do mapStateT runSIO $ importInEnv opts files
|
||
loop opts
|
||
|
||
#ifdef SERVER_MODE
|
||
-- | Run the GF Server (@gf -server@).
|
||
-- The 'Int' argument is the port number for the HTTP service.
|
||
mainServerGFI opts0 port files =
|
||
server jobs port root execute1' . snd
|
||
=<< runSIO (runStateT (importInEnv opts files) emptyGFEnv)
|
||
where
|
||
root = flag optDocumentRoot opts
|
||
opts = beQuiet opts0
|
||
jobs = join (flag optJobs opts)
|
||
|
||
execute1' gfenv0 cmd =
|
||
do (quit,gfenv) <- runStateT (execute1 opts cmd) gfenv0
|
||
return $ if quit then Nothing else Just gfenv
|
||
#else
|
||
mainServerGFI opts files =
|
||
error "GF has not been compiled with server mode support"
|
||
#endif
|
||
|
||
-- | Read end execute commands until it is time to quit
|
||
loop :: Options -> StateT GFEnv IO ()
|
||
loop opts = repeatM $ readAndExecute1 opts
|
||
|
||
-- | Read and execute one command, returning Just an updated environment for
|
||
-- | the next command, or Nothing when it is time to quit
|
||
readAndExecute1 :: Options -> StateT GFEnv IO Bool
|
||
readAndExecute1 opts =
|
||
mapStateT runSIO . execute1 opts =<< readCommand opts
|
||
|
||
-- | Read a command
|
||
readCommand :: Options -> StateT GFEnv IO String
|
||
readCommand opts =
|
||
case flag optMode opts of
|
||
ModeRun -> lift tryGetLine
|
||
_ -> lift . fetchCommand =<< get
|
||
|
||
timeIt act =
|
||
do t1 <- liftSIO $ getCPUTime
|
||
a <- act
|
||
t2 <- liftSIO $ getCPUTime
|
||
return (t2-t1,a)
|
||
|
||
-- | Optionally show how much CPU time was used to run an IO action
|
||
optionallyShowCPUTime :: (Monad m,MonadSIO m) => Options -> m a -> m a
|
||
optionallyShowCPUTime opts act
|
||
| not (verbAtLeast opts Normal) = act
|
||
| otherwise = do (dt,r) <- timeIt act
|
||
liftSIO $ putStrLnFlush $ show (dt `div` 1000000000) ++ " msec"
|
||
return r
|
||
|
||
{-
|
||
loopOptNewCPU opts gfenv'
|
||
| not (verbAtLeast opts Normal) = return gfenv'
|
||
| otherwise = do
|
||
cpu' <- getCPUTime
|
||
putStrLnFlush (show ((cpu' - cputime gfenv') `div` 1000000000) ++ " msec")
|
||
return $ gfenv' {cputime = cpu'}
|
||
-}
|
||
|
||
type ShellM = StateT GFEnv SIO
|
||
|
||
-- | Execute a given command, returning Just an updated environment for
|
||
-- | the next command, or Nothing when it is time to quit
|
||
execute1 :: Options -> String -> ShellM Bool
|
||
execute1 opts s0 =
|
||
do modify $ \ gfenv0 -> gfenv0 {history = s0 : history gfenv0}
|
||
interruptible $ optionallyShowCPUTime opts $
|
||
case pwords s0 of
|
||
-- cc, sd, so, ss and dg are now in GF.Commands.SourceCommands
|
||
-- special commands
|
||
"q" :_ -> quit
|
||
"!" :ws -> system_command ws
|
||
"eh":ws -> eh ws
|
||
"i" :ws -> import_ ws
|
||
-- other special commands, working on GFEnv
|
||
"e" :_ -> empty
|
||
"dc":ws -> define_command ws
|
||
"dt":ws -> define_tree ws
|
||
"ph":_ -> print_history
|
||
"r" :_ -> reload_last
|
||
-- ordinary commands
|
||
_ -> do env <- gets commandenv
|
||
interpretCommandLine env s0
|
||
continue
|
||
where
|
||
-- loopNewCPU = fmap Just . loopOptNewCPU opts
|
||
continue,stop :: ShellM Bool
|
||
continue = return True
|
||
stop = return False
|
||
|
||
pwords s = case words s of
|
||
w:ws -> getCommandOp w :ws
|
||
ws -> ws
|
||
|
||
interruptible :: ShellM Bool -> ShellM Bool
|
||
interruptible act =
|
||
do gfenv <- get
|
||
mapStateT (
|
||
either (\e -> printException e >> return (True,gfenv)) return
|
||
<=< runInterruptibly) act
|
||
|
||
-- Special commands:
|
||
|
||
quit = do when (verbAtLeast opts Normal) $ putStrLnE "See you."
|
||
stop
|
||
|
||
system_command ws = do lift $ restrictedSystem $ unwords ws ; continue
|
||
|
||
|
||
{-"eh":w:_ -> do
|
||
cs <- readFile w >>= return . map words . lines
|
||
gfenv' <- foldM (flip (process False benv)) gfenv cs
|
||
loopNewCPU gfenv' -}
|
||
eh [w] = -- Ehhh? Reads commands from a file, but does not execute them
|
||
do env <- gets commandenv
|
||
cs <- lift $ restricted (readFile w) >>= return . map (interpretCommandLine env) . lines
|
||
continue
|
||
eh _ = do putStrLnE "eh command not parsed"
|
||
continue
|
||
|
||
import_ args =
|
||
do case parseOptions args of
|
||
Ok (opts',files) -> do
|
||
curr_dir <- lift getCurrentDirectory
|
||
lib_dir <- lift $ getLibraryDirectory (addOptions opts opts')
|
||
importInEnv (addOptions opts (fixRelativeLibPaths curr_dir lib_dir opts')) files
|
||
continue
|
||
Bad err ->
|
||
do putStrLnE $ "Command parse error: " ++ err
|
||
continue
|
||
continue
|
||
|
||
empty = do modify $ \ gfenv -> gfenv { commandenv=emptyCommandEnv }
|
||
continue
|
||
|
||
define_command (f:ws) =
|
||
case readCommandLine (unwords ws) of
|
||
Just comm ->
|
||
do modify $
|
||
\ gfenv ->
|
||
let env = commandenv gfenv
|
||
in gfenv {
|
||
commandenv = env {
|
||
commandmacros = Map.insert f comm (commandmacros env)
|
||
}
|
||
}
|
||
continue
|
||
_ -> dc_not_parsed
|
||
define_command _ = dc_not_parsed
|
||
|
||
dc_not_parsed = putStrLnE "command definition not parsed" >> continue
|
||
|
||
define_tree (f:ws) =
|
||
case readExpr (unwords ws) of
|
||
Just exp ->
|
||
do modify $
|
||
\ gfenv ->
|
||
let env = commandenv gfenv
|
||
in gfenv { commandenv = env {
|
||
expmacros = Map.insert f exp (expmacros env) } }
|
||
continue
|
||
_ -> dt_not_parsed
|
||
define_tree _ = dt_not_parsed
|
||
|
||
dt_not_parsed = putStrLnE "value definition not parsed" >> continue
|
||
|
||
print_history =
|
||
do mapM_ putStrLnE . reverse . drop 1 . history =<< get
|
||
continue
|
||
|
||
reload_last = do
|
||
gfenv0 <- get
|
||
let imports = [(s,ws) | s <- history gfenv0, ("i":ws) <- [pwords s]]
|
||
case imports of
|
||
(s,ws):_ -> do
|
||
putStrLnE $ "repeating latest import: " ++ s
|
||
import_ ws
|
||
_ -> do
|
||
putStrLnE $ "no import in history"
|
||
continue
|
||
|
||
|
||
printException e = maybe (print e) (putStrLn . ioErrorText) (fromException e)
|
||
|
||
fetchCommand :: GFEnv -> IO String
|
||
fetchCommand gfenv = do
|
||
path <- getAppUserDataDirectory "gf_history"
|
||
let settings =
|
||
Haskeline.Settings {
|
||
Haskeline.complete = wordCompletion gfenv,
|
||
Haskeline.historyFile = Just path,
|
||
Haskeline.autoAddHistory = True
|
||
}
|
||
res <- IO.runInterruptibly $ Haskeline.runInputT settings (Haskeline.getInputLine (prompt gfenv))
|
||
case res of
|
||
Left _ -> return ""
|
||
Right Nothing -> return "q"
|
||
Right (Just s) -> return s
|
||
|
||
importInEnv :: Options -> [FilePath] -> ShellM ()
|
||
importInEnv opts files =
|
||
do pgf0 <- gets multigrammar
|
||
if flag optRetainResource opts
|
||
then do src <- lift $ importSource opts files
|
||
pgf <- lift . lazySIO $ importPGF pgf0 -- duplicates some work, better to link src
|
||
modify $ \ gfenv -> gfenv {retain=True, pgfenv = (src,pgfEnv pgf)}
|
||
else do pgf1 <- lift $ importPGF pgf0
|
||
modify $ \ gfenv->gfenv { retain=False,
|
||
pgfenv = (emptyGrammar,pgfEnv pgf1) }
|
||
where
|
||
importPGF pgf0 =
|
||
do let opts' = addOptions (setOptimization OptCSE False) opts
|
||
pgf1 <- importGrammar pgf0 opts' files
|
||
if (verbAtLeast opts Normal)
|
||
then putStrLnFlush $
|
||
unwords $ "\nLanguages:" : map showCId (languages pgf1)
|
||
else done
|
||
return pgf1
|
||
|
||
tryGetLine = do
|
||
res <- try getLine
|
||
case res of
|
||
Left (e :: SomeException) -> return "q"
|
||
Right l -> return l
|
||
|
||
prompt env
|
||
| retain env || abs == wildCId = "> "
|
||
| otherwise = showCId abs ++ "> "
|
||
where
|
||
abs = abstractName (multigrammar env)
|
||
|
||
type CmdEnv = (Grammar,PGFEnv)
|
||
|
||
data GFEnv = GFEnv {
|
||
retain :: Bool, -- grammar was imported with -retain flag
|
||
pgfenv :: CmdEnv,
|
||
commandenv :: CommandEnv ShellM,
|
||
history :: [String]
|
||
}
|
||
|
||
emptyGFEnv :: GFEnv
|
||
emptyGFEnv = GFEnv False (emptyGrammar,pgfEnv emptyPGF) emptyCommandEnv [] {-0-}
|
||
|
||
emptyCommandEnv = mkCommandEnv allCommands
|
||
multigrammar = pgf . snd . pgfenv
|
||
|
||
allCommands =
|
||
extend pgfCommands [helpCommand allCommands]
|
||
`Map.union` sourceCommands
|
||
`Map.union` commonCommands
|
||
|
||
instance HasGrammar ShellM where getGrammar = gets (fst . pgfenv)
|
||
instance HasPGFEnv ShellM where getPGFEnv = gets (snd . pgfenv)
|
||
|
||
wordCompletion gfenv (left,right) = do
|
||
case wc_type (reverse left) of
|
||
CmplCmd pref
|
||
-> ret (length pref) [Haskeline.simpleCompletion name | name <- Map.keys (commands cmdEnv), isPrefixOf pref name]
|
||
CmplStr (Just (Command _ opts _)) s0
|
||
-> do mb_state0 <- try (evaluate (initState pgf (optLang opts) (optType opts)))
|
||
case mb_state0 of
|
||
Right state0 -> let (rprefix,rs) = break isSpace (reverse s0)
|
||
s = reverse rs
|
||
prefix = reverse rprefix
|
||
ws = words s
|
||
in case loop state0 ws of
|
||
Nothing -> ret 0 []
|
||
Just state -> let compls = getCompletions state prefix
|
||
in ret (length prefix) (map (\x -> Haskeline.simpleCompletion x) (Map.keys compls))
|
||
Left (_ :: SomeException) -> ret 0 []
|
||
CmplOpt (Just (Command n _ _)) pref
|
||
-> case Map.lookup n (commands cmdEnv) of
|
||
Just inf -> do let flg_compls = [Haskeline.Completion ('-':flg++"=") ('-':flg) False | (flg,_) <- flags inf, isPrefixOf pref flg]
|
||
opt_compls = [Haskeline.Completion ('-':opt) ('-':opt) True | (opt,_) <- options inf, isPrefixOf pref opt]
|
||
ret (length pref+1)
|
||
(flg_compls++opt_compls)
|
||
Nothing -> ret (length pref) []
|
||
CmplIdent (Just (Command "i" _ _)) _ -- HACK: file name completion for command i
|
||
-> Haskeline.completeFilename (left,right)
|
||
CmplIdent _ pref
|
||
-> do mb_abs <- try (evaluate (abstract pgf))
|
||
case mb_abs of
|
||
Right abs -> ret (length pref) [Haskeline.simpleCompletion name | cid <- Map.keys (funs abs), let name = showCId cid, isPrefixOf pref name]
|
||
Left (_ :: SomeException) -> ret (length pref) []
|
||
_ -> ret 0 []
|
||
where
|
||
pgf = multigrammar gfenv
|
||
cmdEnv = commandenv gfenv
|
||
optLang opts = valCIdOpts "lang" (head (languages pgf)) opts
|
||
optType opts =
|
||
let str = valStrOpts "cat" (showCId $ lookStartCat pgf) opts
|
||
in case readType str of
|
||
Just ty -> ty
|
||
Nothing -> error ("Can't parse '"++str++"' as type")
|
||
|
||
loop ps [] = Just ps
|
||
loop ps (t:ts) = case nextState ps (simpleParseInput t) of
|
||
Left es -> Nothing
|
||
Right ps -> loop ps ts
|
||
|
||
ret len xs = return (drop len left,xs)
|
||
|
||
|
||
data CompletionType
|
||
= CmplCmd Ident
|
||
| CmplStr (Maybe Command) String
|
||
| CmplOpt (Maybe Command) Ident
|
||
| CmplIdent (Maybe Command) Ident
|
||
deriving Show
|
||
|
||
wc_type :: String -> CompletionType
|
||
wc_type = cmd_name
|
||
where
|
||
cmd_name cs =
|
||
let cs1 = dropWhile isSpace cs
|
||
in go cs1 cs1
|
||
where
|
||
go x [] = CmplCmd x
|
||
go x (c:cs)
|
||
| isIdent c = go x cs
|
||
| otherwise = cmd x cs
|
||
|
||
cmd x [] = ret CmplIdent x "" 0
|
||
cmd _ ('|':cs) = cmd_name cs
|
||
cmd _ (';':cs) = cmd_name cs
|
||
cmd x ('"':cs) = str x cs cs
|
||
cmd x ('-':cs) = option x cs cs
|
||
cmd x (c :cs)
|
||
| isIdent c = ident x (c:cs) cs
|
||
| otherwise = cmd x cs
|
||
|
||
option x y [] = ret CmplOpt x y 1
|
||
option x y ('=':cs) = optValue x y cs
|
||
option x y (c :cs)
|
||
| isIdent c = option x y cs
|
||
| otherwise = cmd x cs
|
||
|
||
optValue x y ('"':cs) = str x y cs
|
||
optValue x y cs = cmd x cs
|
||
|
||
ident x y [] = ret CmplIdent x y 0
|
||
ident x y (c:cs)
|
||
| isIdent c = ident x y cs
|
||
| otherwise = cmd x cs
|
||
|
||
str x y [] = ret CmplStr x y 1
|
||
str x y ('\"':cs) = cmd x cs
|
||
str x y ('\\':c:cs) = str x y cs
|
||
str x y (c:cs) = str x y cs
|
||
|
||
ret f x y d = f cmd y
|
||
where
|
||
x1 = take (length x - length y - d) x
|
||
x2 = takeWhile (\c -> isIdent c || isSpace c || c == '-' || c == '=' || c == '"') x1
|
||
|
||
cmd = case [x | (x,cs) <- RP.readP_to_S pCommand x2, all isSpace cs] of
|
||
[x] -> Just x
|
||
_ -> Nothing
|
||
|
||
isIdent c = c == '_' || c == '\'' || isAlphaNum c
|