基于上下文的命令行自动完成 [英] Command line autocompletion based on context
问题描述
我有一个以下程序(这里是在线IDE中的程序链接),目的其中之一是探索Haskell命令行自动完成功能:
$ $ p $ { - #LANGUAGE FlexibleInstances,MultiParamTypeClasses,UndecidableInstances# - }
导入System.Console.Haskeline
导入System.IO
导入System.IO.Unsafe
导入Control.Monad.State.Strict
导入限定的Data.ByteString .Char8作为B
导入Data.Maybe
导入Data.List
将合格的Data.Map导入为M
data MyDataState = MyDataState {
mydata: :[Int],
selectedElement :: Int,
showEven :: Bool
}派生(显示)
实例MonadState sm => MonadState s(InputT m)其中
get = lift得到
put = lift。把
state = lift。状态
myfile :: FilePath
myfile =data.txt
defaultFlagValue :: Bool
defaultFlagValue = False
defaultSelectedElement :: Int
defaultSelectedElement = 0
saveDataToFile :: [Int] - > IO()
saveDataToFile _data = withFile myfile WriteMode $ \h - > hPutStr h(unwords $ map show _data)
{ - #NOINLINE loadDataFromFile# - }
loadDataFromFile :: [Int]
loadDataFromFile =地图读取。单词$ B.unpack $ unsafePerformIO $ B.readFile myfile
$ b generalSetOfCommands = M.fromList [
(:help,outputs this help),
( ),
(:commands,适用于当前选择的所有命令的列表),
(:show,show current set of data ),
(:save,将数据保存到文件),
(:load,从文件加载数据),
(:select, ),
(:new,将元素添加到数据集),
(:toggleShowEven,切换控制输出的标志甚至数据集元素)
]
firstSetOfCommands = M.fromList [
(:command1_1,description of:command1_1),
(:command1_2,描述:command1_2),
(:command1_3,description of:command1_3),
(:command1_4,description of:command1_4)
]
secondSetOfCommands = M.fromList [
(:command2_1,description of:comma
(:command2_2,description of:command2_2),
(:command2_3,description of:command2_3),
(:command2_4, description of:command2_4)
]
thirdSetOfCommands = M.fromList [
(:command3_1,description of:command3_1),
(:command3_2, description of:command3_2),
(:command3_3,description of:command3_3),
(:command3_4,description of:command3_4)
]
searchFunc :: MyDataState - >字符串 - > [完成]
searchFunc(MyDataState mydata selectedElement showEven)str = $ b $ map simpleCompletion $ filter(str`isPrefixOf`)(M.keys generalSetOfCommands ++
case selectedElement of
1 - > M.keys firstSetOfCommands
2 - > M.keys secondSetOfCommands
3 - > M.keys thirdSetOfCommands
otherwise - > []
)
mySettings :: Settings(StateT MyDataState IO)
mySettings =设置{historyFile =只是myhist
,complete = completeWord Nothing\t$ \str - > do
_data< - get
return $ searchFunc _data str
,autoAddHistory = True
}
help :: InputT(StateT MyDataState IO)( )
help =命令
命令:: InputT(StateT MyDataState IO)()
命令=
(MyDataState mydata selectedElement标志)< - get
liftIO $ mapM_ putStrLn $ case selectedElement
1 - > (++)generalSetOfCommands firstSetOfCommands
2 - > (++)generalSetOfCommands secondSetOfCommands
3 - > M.elems $ M.mapWithKey(\ k v - > k ++\ t - ++ v)$ M.unionWith(++)generalSetOfCommands thirdSetOfCommands
otherwise - >> M.elems $ M.mapWithKey(\ kv - > k ++\ t - ++ v)generalSetOfCommands
toggleFlag :: InputT(StateT MyDataState IO)()
toggleFlag = do
MyDataState mydata selectedElement标志< - get
put $ MyDataState mydata selectedElement(不标志)
parseInput :: String - > InputT(StateT MyDataState IO)()
parseInput inp
| :q== inp = return()
| :help== inp = help>> mainLoop
| :命令== inp =(commands>> mainLoop)
| :toggleShowEven== inp = do
toggleFlag
MyDataState mydata selectedElement标志< - get
liftIO $ putStrLn $Flag已设置为++(显示标志)
mainLoop
| :select== inp = do
MyDataState mydata selectedElement showEven< - get
inputData< - getInputLine\t选择其中一个数据元素为当前值:
case inputData
无 - > put(MyDataState mydata selectedElement showEven)
只需inputD - >
let inputInt = read inputD
in if elem inputInt mydata
then put(MyDataState mydata inputInt showEven)
else do
liftIO $ putStrLn $您输入的元素( ++(show inputInt)++)在数据集
put(MyDataState mydata selectedElement showEven)
mainLoop
| :show== inp = do
MyDataState mydata selectedElement showEven< - get
liftIO $ putStrLn $ unwords $ if showEven
then map(\ x - > if x == selectedElement then[++ show x ++]else show x)mydata
else map(\ x - > if x == selectedElement then[++ show x ++]否则显示x)$过滤奇数mydata
mainLoop
| :save== inp = do
MyDataState mydata selectedElement _< - get
liftIO $ saveDataToFile mydata
mainLoop
| :load== inp = do
put(MyDataState loadDataFromFile defaultSelectedElement defaultFlagValue)
mainLoop
| :new== inp = do
MyDataState mydata selectedElement showEven< - get - 读取状态
inputData< - getInputLine\ tEnter data:
case inputData of
Nothing - >
如果为null mydata
then(MyDataState [0] selectedElement showEven)
放置$ $ else $(MyDataState mydata selectedElement showEven)
只要inputD - >
如果为null,则为$ mydata
然后MyDataState [read inputD] selectedElement showEven
MyDataState(mydata ++ [read inputD])selectedElement showEven - 更新状态
mainLoop
| :== inp = do
outputStrLn $\\\
No命令\++ inp ++\\\\
mainLoop
|否则= handleInput inp
handleInput :: String - > InputT(StateT MyDataState IO)()
handleInput inp = mainLoop
mainLoop :: InputT(StateT MyDataState IO)()
mainLoop = do
inp< - getInputLine%
也许(return())parseInput inp
greet :: IO()
greet = mapM_ putStrLn
[
, MyProgram
,==============================
,For help type \\ \\:help \
,
]
main :: IO(((),MyDataState)
main = do
greet
runStateT(runInputT mySettings mainLoop)MyDataState {mydata = [],selectedElement = defaultSelectedElement,showEven = defaultFlagValue}
在我的上一个问题中我一直在努力增加考虑程序状态的可能性,并基于此形成自动完成列表。现在我已经解决了这个问题,另一个问题出现了 - 我怎么能考虑命令行命令的当前上下文?
例如,下面是一个简短的例子:与我的程序互动: * Main>主
MyProgram
==============================
如需帮助键入:help
%:显示
:新
输入数据:1
%:新
输入数据:2
%:new
输入数据:3
%:select
选择其中一个数据元素为当前值:2
%:show
1 3
%:toggleShowEven
标志已被设置为真
%:显示
1 [2] 3
%:
:命令:load:q: select:toggleShowEven:command2_2:command2_4
:help:new:save:show:command2_1:command2_3
%
正如您所看到的,它会根据当前选择自动完成当前可用命令的列表(在此示例中,它是值 2
)。但是如果我想为现有命令生成一组新命令,例如:select
?
case,on input
%:select
选择其中一个数据元素为当前值:
当按下 Tab 时,我想获得自动填充的可用值列表 1 2 3
,只有这些值。是否有可能以某种方式考虑我调用autocompletion函数的地方?
我期望的是不同版本的 searchFunc
函数用于不同的上下文。例如,对于:select
命令它会是 selectSearchFunc
。但是我不知道如何才能使它仅在调用:select
命令时应用。似乎应该重新定义 mySettings
不是应用于全局作用域,而是应用于本地作用域,但如何做到这一点并不明显。我会很感激任何有助于解决此问题的建议。
我们可以扩展状态,使 searchFunc
在select中可以有不同的表现。
data WholeState = WholeState MyDataState MyCmdlineState
数据MyCmdlineState = TopLevel |选择 - 等
searchFunc(WholeState mydatastate TopLevel)str =(...) - 当前searchFunc做什么
searchFunc(WholeState mydatastate Select)str =(...) - 在一个选择
中特别完成
然后使用括号函数设置命令行状态在一个固定的范围内。
localCmdlineState :: MonadState WholeState m => MyCmdlineState - > m a - > ma
localCmdlineState mcstate run = do
WholeState mydatastate s0< - get
put(WholeState mydatastate mcstate)
run
WholeState mydatastate'_< - get
put(WholeState mydatastate's0)
这可以在 parseInput中使用,在
中:选择
情况下, getInputLine
变为
inputData< - localCmdlineState选择$ getInputLine\ t选择其中一个数据元素为当前值:
可以说, localCmdlineState
复杂。你必须注意国家的每一个位置。另一个问题是 MyCmdlineState
引入了一些间接性,导致代码难以遵循。
缓解这是使用镜头,所以当我们访问它们时,只有 WholeState
的相关部分出现在代码中。
更好的方法是使用与 MonadState
不同的抽象来执行命令行完成的当前状态( MyCmdlineState
)。特别是,我在考虑 MonadReader
,它的 local
函数正是我们所需要的。
而不是新的枚举类型,为什么不只是携带 searchFunc
本身:
类型SearchFunc = MyDataState - >字符串 - > [完成]
而不是模式匹配,我们只是做更多的定义。也可以在运行时创建并传递 SearchFunc
。
topLevelSearchFunc :: SearchFunc
$
selectSearchFunc :: SearchFunc
$ b $ p
pre
$ M $ ReaderT SearchFunc(StateT MyDataState IO)
实现 MonadReader
for InputT
有点棘手。 lift
-ing是不够的。希望有 mapInputT
。
instance MonadReader s m => MonadReader s(InputT m)其中
reader = lift。阅读器
本地f = mapInputT(本地f)
需要更改的另一位是 mySettings
,从而从其环境中获取 searchFunc
而不是常量。
mySettings ::设置M
mySettings =设置{historyFile =只需myhist
,complete = completeWord无\t$ \str - > do
_data< - get
searchFunc< - ask
return $ searchFunc _data str
,autoAddHistory = True
}
$ c $在 main
中,我们从> topLevelSearchFunc $ c
$ b $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ runStateT(runReaderT(runInputT mySettings mainLoop)topLevelSearchFunc)MyDataState在中,{mydata = [],selectedElement = defaultSelectedElement,showEven = defaultFlagValue}
> parseInput ,我们在本地设置了 SearchFunc
,语法与我之前的解决方案非常相似:
inputData< - local(\_ - > selectSearchFunc)$ getInputLine\ t选择其中一个数据元素为当前值:
这样做的好处是可以制作 SearchFunc
只有通过 MonadReader
效果才可用,这表明它只能在本地进行修改(使用 local
希望是因此将应用程序状态的各个组件分隔开来,防止它们相互干扰并减少出错的可能性。
I have a following program (and here is the link to the program in an online IDE), purpose of which is to explore Haskell command line autocompletion capabilities:
{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, UndecidableInstances #-}
import System.Console.Haskeline
import System.IO
import System.IO.Unsafe
import Control.Monad.State.Strict
import qualified Data.ByteString.Char8 as B
import Data.Maybe
import Data.List
import qualified Data.Map as M
data MyDataState = MyDataState {
mydata :: [Int],
selectedElement :: Int,
showEven :: Bool
} deriving (Show)
instance MonadState s m => MonadState s (InputT m) where
get = lift get
put = lift . put
state = lift . state
myfile :: FilePath
myfile = "data.txt"
defaultFlagValue :: Bool
defaultFlagValue = False
defaultSelectedElement :: Int
defaultSelectedElement = 0
saveDataToFile :: [Int] -> IO ()
saveDataToFile _data = withFile myfile WriteMode $ \h -> hPutStr h (unwords $ map show _data)
{-# NOINLINE loadDataFromFile #-}
loadDataFromFile :: [Int]
loadDataFromFile = map read . words $ B.unpack $ unsafePerformIO $ B.readFile myfile
generalSetOfCommands = M.fromList [
(":help", "outputs this help"),
(":q", "quits the program"),
(":commands", "list of all commands applicable to the current selection"),
(":show", "show current set of data"),
(":save", "saves data to file"),
(":load", "loads data from file"),
(":select", "selects one of the data set elements to be current"),
(":new", "adds element to the data set"),
(":toggleShowEven", "toggles the flag that controls output of even data set elements")
]
firstSetOfCommands = M.fromList [
(":command1_1", "description of :command1_1"),
(":command1_2", "description of :command1_2"),
(":command1_3", "description of :command1_3"),
(":command1_4", "description of :command1_4")
]
secondSetOfCommands = M.fromList [
(":command2_1", "description of :command2_1"),
(":command2_2", "description of :command2_2"),
(":command2_3", "description of :command2_3"),
(":command2_4", "description of :command2_4")
]
thirdSetOfCommands = M.fromList [
(":command3_1", "description of :command3_1"),
(":command3_2", "description of :command3_2"),
(":command3_3", "description of :command3_3"),
(":command3_4", "description of :command3_4")
]
searchFunc :: MyDataState -> String -> [Completion]
searchFunc (MyDataState mydata selectedElement showEven) str =
map simpleCompletion $ filter (str `isPrefixOf`) (M.keys generalSetOfCommands ++
case selectedElement of
1 -> M.keys firstSetOfCommands
2 -> M.keys secondSetOfCommands
3 -> M.keys thirdSetOfCommands
otherwise -> []
)
mySettings :: Settings (StateT MyDataState IO)
mySettings = Settings { historyFile = Just "myhist"
, complete = completeWord Nothing " \t" $ \str -> do
_data <- get
return $ searchFunc _data str
, autoAddHistory = True
}
help :: InputT (StateT MyDataState IO) ()
help = commands
commands :: InputT (StateT MyDataState IO) ()
commands = do
(MyDataState mydata selectedElement flag) <- get
liftIO $ mapM_ putStrLn $ case selectedElement of
1 -> M.elems $ M.mapWithKey (\k v -> k ++ "\t - " ++ v) $ M.unionWith (++) generalSetOfCommands firstSetOfCommands
2 -> M.elems $ M.mapWithKey (\k v -> k ++ "\t - " ++ v) $ M.unionWith (++) generalSetOfCommands secondSetOfCommands
3 -> M.elems $ M.mapWithKey (\k v -> k ++ "\t - " ++ v) $ M.unionWith (++) generalSetOfCommands thirdSetOfCommands
otherwise -> M.elems $ M.mapWithKey (\k v -> k ++ "\t - " ++ v) generalSetOfCommands
toggleFlag :: InputT (StateT MyDataState IO) ()
toggleFlag = do
MyDataState mydata selectedElement flag <- get
put $ MyDataState mydata selectedElement (not flag)
parseInput :: String -> InputT (StateT MyDataState IO) ()
parseInput inp
| ":q" == inp = return ()
| ":help" == inp = help >> mainLoop
| ":commands" == inp = (commands >> mainLoop)
| ":toggleShowEven" == inp = do
toggleFlag
MyDataState mydata selectedElement flag <- get
liftIO $ putStrLn $ "Flag has been set to " ++ (show flag)
mainLoop
| ":select" == inp = do
MyDataState mydata selectedElement showEven <- get
inputData <- getInputLine "\tSelect one of the data elements to be current: "
case inputData of
Nothing -> put (MyDataState mydata selectedElement showEven)
Just inputD ->
let inputInt = read inputD
in if elem inputInt mydata
then put (MyDataState mydata inputInt showEven)
else do
liftIO $ putStrLn $ "The element you entered (" ++ (show inputInt) ++ ") has not been found in the data set"
put (MyDataState mydata selectedElement showEven)
mainLoop
| ":show" == inp = do
MyDataState mydata selectedElement showEven <- get
liftIO $ putStrLn $ unwords $ if showEven
then map (\x -> if x == selectedElement then "[" ++ show x ++ "]" else show x) mydata
else map (\x -> if x == selectedElement then "[" ++ show x ++ "]" else show x) $ filter odd mydata
mainLoop
| ":save" == inp = do
MyDataState mydata selectedElement _ <- get
liftIO $ saveDataToFile mydata
mainLoop
| ":load" == inp = do
put (MyDataState loadDataFromFile defaultSelectedElement defaultFlagValue)
mainLoop
| ":new" == inp = do
MyDataState mydata selectedElement showEven <- get -- reads the state
inputData <- getInputLine "\tEnter data: "
case inputData of
Nothing ->
put $ if null mydata
then ( MyDataState [0] selectedElement showEven )
else ( MyDataState mydata selectedElement showEven )
Just inputD ->
put $ if null mydata
then MyDataState [read inputD] selectedElement showEven
else MyDataState (mydata ++ [read inputD]) selectedElement showEven -- updates the state
mainLoop
| ":" == inp = do
outputStrLn $ "\nNo command \"" ++ inp ++ "\"\n"
mainLoop
| otherwise = handleInput inp
handleInput :: String -> InputT (StateT MyDataState IO) ()
handleInput inp = mainLoop
mainLoop :: InputT (StateT MyDataState IO ) ()
mainLoop = do
inp <- getInputLine "% "
maybe (return ()) parseInput inp
greet :: IO ()
greet = mapM_ putStrLn
[ ""
, " MyProgram"
, "=============================="
, "For help type \":help\""
, ""
]
main :: IO ((), MyDataState)
main = do
greet
runStateT (runInputT mySettings mainLoop) MyDataState {mydata = [] , selectedElement = defaultSelectedElement, showEven = defaultFlagValue}
In my previous question I was struggling with adding possibility to take into account program state and form autocompletion list based on that. Now that I have overcome this problem, another question arises - how could I take into account current context of the command line command?
For instance, here is a short example of interaction with my program:
*Main> main
MyProgram
==============================
For help type ":help"
% :show
% :new
Enter data: 1
% :new
Enter data: 2
% :new
Enter data: 3
% :select
Select one of the data elements to be current: 2
% :show
1 3
% :toggleShowEven
Flag has been set to True
% :show
1 [2] 3
% :
:commands :load :q :select :toggleShowEven :command2_2 :command2_4
:help :new :save :show :command2_1 :command2_3
%
As you can see, it autocompletes list of currently available commands based on current selection (in this example it is value 2
). But what if I want to generate new set of commands for existing command, :select
for example?
In this case, on input
% :select
Select one of the data elements to be current:
when pressing Tab, I want to get list of available values for autocompletion 1 2 3
and only those values. Is it possible to somehow take into account the place where I am calling autocompletion function?
What I expect it to be is different versions of searchFunc
function for different context. For example, for :select
command it would be selectSearchFunc
. But I don't know how could I make it be applied only when :select
command is called. It seems that mySettings
somehow should be redefined to be applied not on global scope, but on local scope, but it is not really obvious how to do that. I would appreciate any suggestion that would help to resolve this issue.
We can extend the state so that searchFunc
can behave differently inside a select.
data WholeState = WholeState MyDataState MyCmdlineState
data MyCmdlineState = TopLevel | Select -- etc.
searchFunc (WholeState mydatastate TopLevel) str = (...) -- what the current searchFunc does
searchFunc (WholeState mydatastate Select ) str = (...) -- special completion in a select
Then use a "bracket function" to set the command-line state in a fixed scope.
localCmdlineState :: MonadState WholeState m => MyCmdlineState -> m a -> m a
localCmdlineState mcstate run = do
WholeState mydatastate s0 <- get
put (WholeState mydatastate mcstate)
run
WholeState mydatastate' _ <- get
put (WholeState mydatastate' s0)
This can be used in parseInput
, in the ":select"
case, the getInputLine
becomes
inputData <- localCmdlineState Select $ getInputLine "\tSelect one of the data elements to be current: "
Arguably, localCmdlineState
is a bit complex. You have to pay attention to where each bit of the state goes. Another issue is that the MyCmdlineState
introduces some indirection that makes the code a bit hard to follow.
One way to alleviate this is to use lenses, so only the relevant parts of WholeState
appear in the code when we access them.
An even better approach is to use a different abstraction than MonadState
to carry the current state of command-line completion (MyCmdlineState
). In particular, I'm thinking of MonadReader
, whose local
function is exactly what we need.
Instead of a new enumeration type, why not just carry the searchFunc
itself:
type SearchFunc = MyDataState -> String -> [Completion]
And instead of pattern-matching, we just make more definitions. It's also possible to create and pass SearchFunc
on the fly.
topLevelSearchFunc :: SearchFunc
selectSearchFunc :: SearchFunc
We make the stack a bit longer:
type M = ReaderT SearchFunc (StateT MyDataState IO)
Implementing MonadReader
for InputT
is a bit tricky. lift
-ing is not sufficient. Hopefully there is mapInputT
.
instance MonadReader s m => MonadReader s (InputT m) where
reader = lift . reader
local f = mapInputT (local f)
Another bit that needs to change is mySettings
, which thus gets searchFunc
from its environment instead of a constant.
mySettings :: Settings M
mySettings = Settings { historyFile = Just "myhist"
, complete = completeWord Nothing " \t" $ \str -> do
_data <- get
searchFunc <- ask
return $ searchFunc _data str
, autoAddHistory = True
}
In main
, we start with topLevelSearchFunc
main = do
greet
runStateT (runReaderT (runInputT mySettings mainLoop) topLevelSearchFunc) MyDataState {mydata = [] , selectedElement = defaultSelectedElement, showEven = defaultFlagValue}
In parseInput
, we set the SearchFunc
locally, with syntax very similar to my previous solution:
inputData <- local (\_ -> selectSearchFunc) $ getInputLine "\tSelect one of the data elements to be current: "
The advantage of this is that making SearchFunc
only available via a MonadReader
effect makes it clear that it can only be modified locally (using local
).
The hope is that thus compartmentalizing the various components of the application state prevents them from interfering with each other and reduces the potential for mistakes.
这篇关于基于上下文的命令行自动完成的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!