基于上下文的命令行自动完成 [英] Command line autocompletion based on context

查看:104
本文介绍了基于上下文的命令行自动完成的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个以下程序(这里是在线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
}
main
中,我们从> topLevelSearchFunc
$ 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: "


Full gist


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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆