【问题标题】:Any readline binding support for elixir?任何对 elixir 的 readline 绑定支持?
【发布时间】:2015-05-17 08:58:13
【问题描述】:

我正在尝试创建一个程序,它接受用户输入的方式与 iexerl 类似(例如,当按下允许键浏览以前的历史时)。

如果使用标准IO.gets如下,

IO.gets "user> "

当按下 up-allow 时,控制台会显示以下内容。

user> ^[[A^[[A

是否有任何函数/库具有可在 elixir 代码中使用的 readline 功能?


到目前为止,我调查的是,

  • 有些语言对 readline 库有绑定支持,但我一直没能找到 elixir 的对应能力。
  • iex implementation 似乎将此功能委托给 erl (/lib/iex/history.ex 似乎只是管理历史列表),但我无法在 erlang 方面找到相应的功能。
  • 我已经尝试过erl_ddll.load,但未能更进一步。

在 iex 上,

iex(4)> :erl_ddll.load('/usr/lib', 'libreadline')
{:error, {:open_error, -10}}
iex(5)> :erl_ddll.format_error({:open_error, -10})
'dlopen(/usr/lib/libreadline.so, 2): image not found'

我在 OSX 上并通过自制软件安装了 libreadline,我可以在 /usr/lib 中找到 libreadline.dylib


[关于目的的附加说明]

我正在使用 elixir 进行以下 (mal) 实验,这是一个用各种语言实现的 lisp repl(但不是使用 elixir/erlang)。

https://github.com/kanaka/mal

该步骤的一部分是使用历史记录实现 repl,如果没有原生的,一些语言正在使用 readline 绑定库。

[更多更新 - 2015/3/22]

我正在尝试使用 NIF 方法(与 encurses 类似)来使用 readline 库。我可以让它在 erlang (erl) 上工作,但停留在长生不老药方面。从 C 库(readline 或只是普通的 scanf)读取输入时,“mix run -e”或“iex”的行为似乎有点奇怪(跳过或忽略某些输入),但找不到原因。 encurses 似乎表现相似。

以下是我的试验。

https://github.com/parroty/ereadline

https://github.com/parroty/readline

我可能会采用更通用的方法,例如 rlwrap。

【问题讨论】:

  • 我不知道有任何 readline 集成,但 ncurses 对您想要实现的目标有帮助吗? github.com/jzellner/encurses
  • 感谢您的参考。我一直在寻找 iex 之外的实现历史(我已经更新了问题的背景)。但是,某些技术可能适用。我会进一步检查他们。我也会检查 ncurses。
  • 据我所知,erlang 所做的 readline 仿真(和 elixir 使用的)在 OS X 上不可用。即使在可用的平台上,它实际上也是一个相当复杂的过程,而且远不及就像使用 libreadline 通常一样简单。请参阅stackoverflow.com/questions/22426716/… 的答案。使用 rlwrap 是快速的解决方案。
  • rlwrap 似乎是一个不错的库。我一直在寻找特定于语言的解决方案(如问题中所添加),但似乎 readline 比我想象的要复杂。如果找不到更好的解决方案,我会尝试使用 rlwrap。

标签: elixir


【解决方案1】:

免责声明:我绝不是陪审团操纵 Erlang 的 shell 代码以听命于命的专家。非常欢迎对此答案进行更正或澄清。这对我来说也是一个学习过程。

tl;dr:erliex 依赖于 edlin 模块和 tty_sl 端口之间的合作来实现其类似于 Readline 的功能。您也可以使用这些,但至少可以说缺乏官方文档。


Erlang 的外壳(Elixir 就是在此基础上构建的)比典型的 REPL 复杂得多。与典型的 REPL 不同,它不仅仅是循环输入并评估它;它实际上构建起来很像典型的 OTP 应用程序,监督树一直向下。

This article by the author of Learn You Some Erlang for Great Good! 更详细地介绍了整个 Erlang shell 的架构。总结一下:

  • 特定于平台的 TTY 驱动程序将用户输入传递给user_drv
  • user_drv 要么进入 shell 管理模式(如果它接收到 ^G^C),要么将输入传递给当前选择的 group(可以有多个 groups,因此 shells; 当你按^G,您可以选择创建更多groups,切换到现有groups,等等)
  • groupedlin 一起使用一行代码进行评估;一旦它有一条线,它就会将它发送到shell
  • shell 进行实际评估,然后将结果发送给groupgroup 将结果发送给user_drv
  • 如果向user_drv 发送内容的group 是活动组,则user_drv 将其传递给TTY 驱动程序(进而传递给用户);否则,它会静音

此过程中与问题相关的部分是edlin,它是类似 Readline 功能的 Erlang 实现。不幸的是,据我所知,edlin 的文档并没有特别好,但是在 Elixir 中使用它的要点(基于我能够从 lib/kernel/src/group.erl 中收集到的内容)如下:

  • 在使用 edlin 的进程中调用 :edlin.init(这会设置一个“终止缓冲区”,在 Emacs 的“终止环”意义上)
  • 当您准备好阅读一行时,调用 {:more_chars,continuation,requests} = :edlin.start(prompt),其中 prompt 是一个字符列表,代表 - 您猜对了 - 您的 shell 的命令行提示符
  • 句柄requests,格式应为[{:put_chars,:unicode,prompt}];在 Erlang shell 中,“句柄”表示“发送到 user_drv 以进行打印”,但在您的情况下,这可能会有所不同

此时,循环开始。在每次迭代中,您将调用:edlin.edit_line(characters,continuation)(其中characters 是一个字符列表,即来自用户输入)。每次调用都会为您提供以下元组之一:

  • {:done,line,rest,requests}: edlin 遇到换行符,已完成处理您的行;此时,您可以使用line(您的行)和rest(您行后的所有字符)做任何您想做的事情
  • {:more_chars,continuation,requests}: edlin 需要更多字符;致电:edlin.edit_line(characters,continuation)
  • {:blink,continuation,requests}:我在这里不是 100% 确定,但我认为这与 edlin 突出显示字符有关(例如当您键入 ) 时光标跳转到匹配的 (
  • {:undefined,character,rest,continuation,requests}:这里也不是 100% 确定,但我认为这与处理命令历史等事情有关

在所有情况下,requests 将是对应于 user_drv 指令的元组列表,通常用于写字符、移动光标等操作。


接下来是处理 TTY 的问题。 user_drv.erl 使用名为 tty_sl 的东西来实现这一点,这是一个 Erlang 端口(即,设计为像 Erlang 进程一样运行的外部程序),具有不同的 Windows 和 Unix 版本。基本程序(再次,Elixirified):

  • 定义以下内容(我们稍后会用到):

    def put_int16(num, tail) do  # we need this in a bit
      use Bitwise  # because macros
      [num |> bsr(8) |> band(255), num |> band(255) | tail]
    end
    
  • 致电port = Port.open {:spawn,'tty_sl -c -e'}-e 代表“回声”,-c 代表“佳能”的任何含义);在这一步中对user_drv 进行了更多的错误检查,显然是为了让它可以启动旧的user(根据上面链接的文章,这似乎是旧版本的 Erlang shell)

  • 启动上述edlin-using 进程并将其存储在例如shell 中(user_drv 在这里做了很多事情以设置多个groups)
  • 开始循环处理来自shell 的请求

然后,在循环中:

  • 收到request(实际上是上面的requests 列表)
  • 将每个request 转换为tty_sl 可以理解的内容:

    command = case request do
      {:put_chars,:unicode,chars} ->            # OP_PUTC
        {:command, [0|:unicode.characters_to_binary(chars,:utf8)]}
      {:move_rel,count} ->                      # OP_MOVE
        {:command, [1|put_int16(count, [])]}
      {:insert_chars,:unicode,chars} ->         # OP_INSC
        {:command, [2|:unicode.characters_to_binary(chars,:utf8)]}
      {:delete_chars,count} ->                  # OP_DELC
        {:command, [3|put_int16(count, [])]}
      :beep ->                                  # OP_BEEP
        {:command, [4]}
      {:put_chars_sync,:unicode,chars,reply} -> # OP_PUTC_SYNC
        {{:command, [5|:unicode.characters_to_binary(chars,:utf8)]}, reply}
      else ->
        else
    end
    
  • 发送command 到 TTY:

    result = case command do
      {:requests,requests} ->
        # Handle more requests
      {:command,_} = command ->
        send port, command
        :ok
      {command,reply} ->
        send port, command
        reply
      _ ->
        :ok
    end
    

您还需要从 TTY 接收资料。对于user_drv,TTY 将消息发送到与group 进程相同的user_drv 进程。无论如何,除了edlin 通过group 发送的请求之外,您还需要处理一些额外的消息:

  • {port,{:data,bytes}}:将bytes 转换为字符并发送到您的shell。由于我们在 Elixir-land,我们甚至可能不需要进行转换。
  • {port,:eof}:类似的交易;将:eof 发送到你的shell
  • {port,:ok}user_drv 不是 100% 确定这一点,但我认为这与 :put_chars_sync 命令有关,因为 user_drv 中处理此消息的代码处理 Reply 变量,并且唯一涉及回复的tty_sl 命令是:put_chars_sync

user_drv 中处理的其余消息属于监督树(即:tty_sl 和各种groups 的处理进程都退出)。


当然,对于所有这些,可能有一个更简单的答案:只需使用user_drv 并为自己创建一个新的shell。这可以通过user_drv_pid = :user_drv.start('tty_sl -c -e', {MyShell,:start}) 的方式来完成(我认为;这里不是 100% 确定)。这似乎是iex 的工作原理(参见IEx.CLI.start/0)。

【讨论】:

  • 哇 - 一个非常详细的答案,我希望长生不老药的人在很久以前就已经很容易解决了,默认情况下。好像他们不使用交互式 iex。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-11
相关资源
最近更新 更多