对elixir的任何readline绑定支持?

时间:2015-03-15 02:29:51

标签: elixir

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

如果使用标准IO.gets

IO.gets "user> "

按下向上允许时,控制台会以下列结果显示。

user> ^[[A^[[A

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


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

  • 有些语言对readline库有绑定支持,但我无法找到elixir的相应功能。
  • iex实现似乎将此功能委托给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,我可以在libreadline.dylib中找到/usr/lib


[关于目的的附加说明]

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

https://github.com/kanaka/mal

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

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

我尝试使用NIF方法(与encurses类似)来使用readline库。我可以在erlang(erl)上做一些工作,但是坚持使用elixir方面。当从C库(readline或只是普通scanf)读取输入时,“mix run -e”或“iex”似乎表现得有些奇怪(跳过或忽略某些输入),但无法找出原因。 encurses似乎行为相似。

以下是我的试验。

https://github.com/parroty/ereadline

https://github.com/parroty/readline

我可能会采用像rlwrap这样更通用的方法。

1 个答案:

答案 0 :(得分:7)

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

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

Erlang的shell(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(可能有多个{{} 1}} s因此group s;当您按shell时,您可以选择创建更多^G,切换到现有group等。< / LI>
  • groupgroup合作,汇总一行代码进行评估;一旦有一行,就会将其发送到edlin
  • shell执行实际评估,然后将结果发送到shellgroup将结果发送到user_drv
  • 如果groupuser_drv发送内容是活动组,则user_drv会将其传递给TTY驱动程序(从而传递给用户);否则,它是静音的

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

  • 在使用:edlin.init的过程中调用edlin(这会设置一个&#34; kill buffer&#34;,在Emacs意义上的&#34; kill ring& #34)
  • 当您准备好阅读某一行时,请致电{:more_chars,continuation,requests} = :edlin.start(prompt),其中prompt是一个代表的焦点列表 - 您猜对了 - 您的shell的命令行提示
  • 处理requests,其格式应为[{:put_chars,:unicode,prompt}];在Erlang shell中,&#34;处理&#34;表示&#34;发送到user_drv进行打印&#34;,但在您的情况下,这可能会有所不同

此时,循环开始。在每次迭代中,您将调用: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代表&#34; echo&#34;,-c代表&#34;佳能&#34;意味着);在user_drv的此步骤中进行了更多的错误检查,显然是因为它可以启动较早的user(根据上面链接的文章,它似乎是旧版本) (Erlang shell)

  • 启动edlin - 使用上述过程并将其存储在shell中(user_drv在此处添加更多内容以设置多个group多个)
  • 开始循环以处理来自shell
  • 的请求

然后,在循环中:

  • 收到request(实际上是request以上的列表)
  • 将每个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会将消息发送到与user_drv进程相同的group进程。在任何情况下,除了group edlin通过{port,{:data,bytes}}发送的请求之外,您还要处理其他一些消息:

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

tty_sl中处理的其余消息属于监督树(即group和各种user_drv的处理流程退出。

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