我正在尝试创建一个接受用户输入的程序,与iex
或erl
类似(例如,当按下允许键导航以前的历史记录时)。
如果使用标准IO.gets
,
IO.gets "user> "
按下向上允许时,控制台会以下列结果显示。
user> ^[[A^[[A
是否有任何函数/库具有可在elixir代码中使用的readline功能?
到目前为止,我所调查的是,
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)。
该步骤的一部分是使用历史记录实现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这样更通用的方法。
答案 0 :(得分:7)
免责声明:我绝不是关于陪审团操纵Erlang的shell代码进行竞标的专家。对此答案的更正或澄清非常受欢迎。这对我来说也是一个学习过程。
tl; dr:erl
和iex
依靠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的体系结构。总结一下:
user_drv
user_drv
要么进入shell管理模式(如果它收到^G
或^C
),要么将输入传递给当前选定的group
(可能有多个{{} 1}} s因此group
s;当您按shell
时,您可以选择创建更多^G
,切换到现有group
等。< / LI>
group
与group
合作,汇总一行代码进行评估;一旦有一行,就会将其发送到edlin
shell
执行实际评估,然后将结果发送到shell
,group
将结果发送到user_drv
group
向user_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}})。