免责声明:我绝不是陪审团操纵 Erlang 的 shell 代码以听命于命的专家。非常欢迎对此答案进行更正或澄清。这对我来说也是一个学习过程。
tl;dr:erl 和 iex 依赖于 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,等等)
-
group 与 edlin 一起使用一行代码进行评估;一旦它有一条线,它就会将它发送到shell
-
shell 进行实际评估,然后将结果发送给group,group 将结果发送给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)。