Lisp REPL作为我的主壳

2021-02-09 20:16:45

如果您喜欢这篇文章并希望帮助我继续写作,请考虑加入,一点点都可以帮助我继续前进:)

更新:截至2021-02-07,尚未发布本演示文稿中使用的所有代码和配置。应该在未来的日子里发生,敬请期待!

我将在本文中介绍的概念在FOSDEM 2021上的演讲中得到了介绍。该视频演示了一种相当新颖的范例,说明了如何将REPL视为机器的“外壳”接口。

在本文中,我将深入探讨理论,设置以及更高级的功能。

注意标题:这与Lisp中实现的另一个系统外壳(例如scsh,clash或shcl)无关;相反,我试图通过将“外壳”引入编程语言REPL来从相反的方向解决问题。

我想强调的是,尽管我使用Common Lisp和Emacs作为演示的支持技术,但本文的主题仍以一种新的shellinterface范式为中心。

如果您不喜欢Emacs或Lisp,请不要介意:这里介绍的概念可以使用其他(编辑器)界面和其他编程语言来适应(但不是全部,我将在后面解释)。

本文的范围很广,涵盖了广泛的教程,因此篇幅相当长。从SLY开始的各个顶级部分:参观最先进的REPL大多是独立的,因此可以随意阅读它们。 (我已在必要时将它们互连在一起。)

计算机用户(特别是技术人员,例如开发人员)在计算机上执行的操作可以理解为以下三种操作之一:

例如,使用某个程序打开一个文件或多个文件来播放音乐专辑,实际上是“收藏+可视化”。缩小一堆照片是“收集+处理”。

我相信,即使不是全部,我们与计算机的大多数交互也可以归纳为这三个任务。

更有趣的是,在收集和可视化之间存在一个反馈循环,这在从shell处理大量文件时非常常见:用户首先显示要处理的文件列表,然后对其进行过滤,显示过滤后的列表,必要时重新过滤,重新显示它,依此类推。确定文件列表后,用户可以对其进行处理并最终可视化结果。

事实证明,传统的shell是处理这种反馈循环的交互式工具特别差。

为了解释霍华德关于吹笛者的演示中的示例,外壳中典型的“收集+处理”过程是通过管道和一些控制结构实现的:

$(systemctl --all | grep openstack | sed&#。service。* //' | cut -c3-)中的S做systemctl重新启动$ Sdone

在上面的示例中,按照Shell中的惯例,整个“收集+处理”步骤对用户来说仍然是一个黑匣子,该用户只能在数据之前和之后可视化数据,而对于中间步骤则永不可视化。

在我们讨论的同时,sh家族的shell语言的控制结构也很差,这使得诸如for循环之类的简单事情变得过于繁琐而无法编写,并且充满了陷阱。

常见的误解是端子和外壳固有地相互绑定,以至于有时两者之间会产生混淆。

终端,实际上是终端仿真器,是一种以视觉方式模拟1970年代和1980年代的硬件(例如VT100)的程序。从定义上讲,这些工具都停留在过去。

(一个常见的误解是它们速度很快。具有讽刺意味的是,它们不是:仿真器模拟终端的物理属性(例如波特率),这限制了文本的打印速度。)

Shell是一种编程语言解释器。一种REPL,通常嵌入交互式功能,例如具有历史记录支持的提示。

我认为终端没有理由继续使用。请注意,这并不意味着我们不应该使用“文本”界面,而恰恰相反:textualdata是一种极易操纵的事物。但是我们可以在终端以外的其他方式中很好地处理文本以及其他类型的数据,从而更快,更漂亮,更强大。 (图形Emacs就是这样的示例之一。)

过去,我曾讨论过使用终端作为接口的弊端(请参阅我的文章Eshell作为主Shell)。我不再赘述,但让我总结一下终端引起的传统外壳的局限性:

诸如ncurses之类的界面工具包无法呈现结构化的小部件(要明白我的意思,请尝试在ncurses框架中选择文本:所选内容将抓住整个行,超出框架)。

我曾经是Eshell用户一段时间,然后切换到M-x shell。这两个基于非终端的外壳都摆脱了上述大多数终端限制,因为图形化的Emacs是成熟的图形化应用程序。

但是我仍然不满意,特别是对于shell使用的编程语言(我使用过Bash,Zsh,Fish和Eshell)。

我喜欢Lisp编程语言,那么为什么不使用我最喜欢的语言,例如Common Lisp?

长期以来,我一直非常努力地坚持我所说的“ readline shell”范式。几乎所有的shell都是以在终端上运行基于readline的提示符(如果不是readline,则为类似的界面)的想法开发的。即使是像scsh或SHCL这样奇特的shell也都考虑了这种范例。

这只能使问题永久存在:由于用户界面中缺少交互性选项,这些外壳无法真正避免我上面提到的糟糕的反馈循环问题。而且在渲染,提示导航和操纵等方面仍然受到限制。

超级用户和开发人员都喜欢拥有自己的“ shell脚本”,通常是简短而又简单的程序,无需官方分发,即可执行从文件处理到Shell助手的日常任务。

这些程序可以用多种语言编写,但不幸的是,出于实际原因,必须限制选择:

某些语言解释器启动速度太慢(例如,超过100毫秒),这使得该脚本无法在与其他脚本的紧密循环中使用。

可移植性可能是一个问题:如果脚本要求将库本地安装,则可能会妨碍其在其他系统(例如您的另一台计算机或朋友的计算机)上的使用。

Bash,POSIX sh和朋友很好地解决了所有这些问题,这可能解释了为什么它们如此普遍(尤其是点可移植性)。

但是,是否有足够的理由放弃并继续使用当今最差的编程语言?

我相信现在是时候挑战我们的现状,并加紧进行壳牌游戏了。

正如我们将在本文的其余部分中看到的那样,用作外壳程序语言的一项必需功能是元编程能力,或者至少可以重新定义其部分语法的功能。 Lisp语言在这里表现得非常好,这要归功于它们的同源性。

人们经常将Perl或Python称为替代品:在享受广泛的库生态系统的同时,它比Bash更具表达力,而且没有局限性。

scsh scsh是一个显而易见的选择,因为它的名称代表“ Scheme shell”。虽然是aScheme,但它似乎没有广泛的库生态系统,这可能使其在实践中有点局限性,或者迫使用户自己编写大部分内容。 (如果我错了,请纠正我。)

Guile Guile是一个很好的竞争者:相当快,它有一个发展中的生态系统,对脚本有很好的支持。

Gauche Gauche的设计目标是成为一种快速的脚本语言。因此,它可能是理想的放弃脚本。

TXR我对TXR几乎一无所知,所以我只能从我玩过的东西中得知:它看起来相当慢(也许在Bash的球场上),我不确定它的生态系统。

球拍首先,在我的机器上,球拍的启动时间约为100毫秒,这似乎令人望而却步。但是也许有一种方法可以克服这个问题。周围有任何cket子手吗? :)

Emacs Lisp Howard的吹笛者非常鼓舞人心,但我决定不选择Elisp,而Ibelieve对此的限制太有限了(暂时),无法用作外壳语言。

Eshell无法将标准输入与标准错误分开。我个人认为这是一个阻碍因素,直到我们修复它或在Emacs Lisp中实现其他shell为止。

作为脚本语言,Emacs Lisp也不是一个好选择,因为emacs--script只能打印到stderr。

此外,从Emacs 27开始,Emacs Lisp具有较差的线程支持。这是很快的限制,特别是在过程管理方面。

最终,Emacs Lisp在该语言中没有命名空间,这使得托管像shell这样的核心内容变得不切实际。

普通Lisp普通Lisp怎么办?某些常见的Lisp实现(例如SBCL)甚至明确支持用于脚本编写(例如SBCL的--script标志)。

当我探索上述可能性时,令我感到惊讶的是,毕竟“ go-toscripts”可能不是正确的方法。

通过使高级用户能够通过可以组合在一起的简单“应用程序”来扩展其工作流程,脚本应该是“点文件”的典型组成部分。

关于编写的最后一点很关键,因为这样做做错了。实际上,脚本是编写代码的糟糕方法:

脚本的唯一接口是“参数传递”。更重要的是:您不能将数据结构传递给另一个脚本!

每个脚本调用都会启动一个新的系统进程(解释器)。(可以通过使用解释器守护程序进行调用来缓解,并使脚本以诸如emacsclient之类的客户端启动。但这仍然会启动一个新的系统进程。)

脚本内部(功能,选项等)通常是其他脚本无法访问的。这通常会导致大量代码重复。

在这一点上,对我来说很清楚,我仍在努力遵循现状。如果我开始反方向思考怎么办?与其尝试将Lisp(或您最喜欢的编程语言)带到Shell中,不如将其带到Lisp REPL中?

解决问题具有许多直接的好处:它使我们(免费)拥有一流的编程工具和功能,例如调试器,步进器,交互式堆栈跟踪以及非常重要的一个检查器。可以实现传统Shell所擅长的功能:流程管理,便捷的输入输出重定向,管道…(令人惊讶的是,没有那么多!)

我选择SLY作为起点,因为它可能是那里最高级的REPL之一,而这恰好也是在运行Lisp(Common Lisp)。

SLY是SLIME的一个分支:尽管非常相似,但SLY具有一些额外的功能,这些功能对于外壳的制造很有帮助。下面将介绍SLIME缺少的功能。

仅SLY可能不足以提供完善的Shell体验。不用担心!通过结合各种Emacs软件包,Common Lisp库和其他实用程序,我成功地补充了大多数缺少的功能。

像许多外壳爱好者一样,您最喜欢的运动可能是自定义提示(也称为PS1):)

它看起来像是一个普通的shell,但是我们不要太快得出结论,因为其中显示了许多微妙而又重要的功能。

这是多行提示。第一行指示路径。第二行具有两个值得注意的元素:0是此提示结果的后向引用(如果有)。我们将在一段时间后恢复到正常状态。

$是当前的Common Lisp程序包(在许多其他编程语言中称为“名称空间”),这里是我自己的“ shell”程序包。太好了,因为这意味着我们会在提示时立即提供命名空间支持!

在这里,SLY提示自定义通过使用常规Lisp(例如,将颜色称为其实际名称,如“绿色”),使其摆脱了外壳隐含语法的束缚。我的提示不仅可以是字符串,还可以使用Lisp代码动态生成!

自动持续时间报告看到倒数第二提示?它休眠2秒钟,然后显示有关结束时间和持续时间的状态通知。

我已将提示设置为仅显示持续1秒以上的命令的状态。这对于避免持续时间报告为0时的输出混乱是很有用的。

自动报告持续时间非常有用,实际上这是唯一的方法。

在Bash中,通常需要在运行时测量命令的持续时间。问题是您需要预料到要持续多长时间,因此您必须已经知道该命令将花费一些时间来完成。

我们只是常常在事后才意识到这一点,这促使我们重新运行该命令,这次以时间为前缀!如果该命令需要很长时间才能完成,那么这可能是一个巨大的障碍。

缺少SLIME功能如果我没记错的话,SLIME不允许自2021-02-06起自定义提示,但是并不难于将其移植。 由于SLY在Emacs中运行,因此您可以在提示符下立即获得强大的文本编辑器的所有功能! 如果发生舵机事件或类似事件,则可以在新窗口中列出搜索匹配项,将其缩小,导航(按C-c C-f),等等。 使用您喜欢的键绑定,CUA,Emacs风格或VI风格(带有Emacs Evil和Evil Collection软件包)。 使用Paredit,Lispy或类似工具进行智能S-exp操纵,非常方便快捷地编写和操纵Lisp代码。 通常,多个外壳程序不会共享相同的基础系统进程,这意味着,如果您在一个外壳程序中定义一个函数或变量,则在其他外壳程序中会看到它。 虽然有时将各个shell相互隔离有时会很有用,但其他时候我希望我可以在我的shell实例之间共享代码和数据!

SLY开箱即用地支持“ multi-REPL”。当您打开一个新的REPL时,您可以决定是开始一个新过程还是重用一个现有的过程。

另一个好处是,共享一个实例的REPL仅使用一个进程的内存。有关大小和内存使用的部分中有更多内容。

如果您经常使用Shell,并且既可以用于常规Shell使用,又可以像我在Common Lisp中一样使用您的编程项目,那么在许多Shell窗口之间迷路就太容易了。

它允许您在所有SLY REPL之间进行模糊搜索。您甚至可以通过劣等的Lisp程序对它们进行分组。

更重要的是,您可以选择多个REPL,然后至少一次执行所有操作,例如删除它们或重新启动它们。

注意,此“选择器”还为您提供Lisp文件和所有相关窗口的列表,例如调试器和编译结果窗口。

提示:您可以使用各种信息来配置shell列表的外观,例如所使用的Lisp编译器,窗口的名称(在Emacs术语中为“ buffer”)等。示例:

(defun ambrevar / helm-sly-format-connection(连接缓冲区)(let((fstring"%s%2s%s")))(format fstring(if(eq sly-default-connection connection)& #34; *"")(helm-sly-connection-number连接)(替换字符串中的正则表达式" * $""& #34;(替换字符串中的正则表达式" * sly-mrepl用于"""(替换字符串中的正则表达式" * sly-inferior-lisp用于"""(缓冲区名称缓冲区))))))))))()setq helm-sly-connection-formatter#' ambrevar / helm-sly-format-connection)

我认为,基本功能是“转到给定的提示”的能力。当命令的输出很长时,回溯到上一个提示可能很麻烦(只有在输出颜色很大的情况下,这种情况才会更糟,从那时起,提示不会突出那么多)。

在SLY中,您可以使用sly-mrepl-previous-prompt和sly-mrepl-next-prompt将光标移动到REPL中的各种提示。

只会变得更好:使用helm-comint-prompts-all,您可以列出所有REPL的所有提示,对其进行模糊搜索,缩小实时范围,最后确认转到所需的提示。

有了这种武器,您将永远不会丢失即时输入或再次输出!

提示:由于默认情况下未绑定,因此我想将其绑定到M-s f:

在搜索和编辑中,我们讨论了搜索整个REPL,有时将搜索限制为单个或选择输出会很有用。

一种方法是将REPL“缩小”到所需的提示。如果我将mycursor放置在提示符或其输出上,然后按C-x n d(缩小到缩小范围),所有其他提示符和输出将消失(仅虚拟出现),从而将搜索和其他命令限制在我所看到的范围内。

向后引用就像自动变量,它分配给所有提示命令的每个单个结果。

如果没有反向引用,则必须以明确定义的名称系统地存储结果。在Bash中,您可以执行以下操作:

这很快变得很麻烦。如果您输入的数字错误,则可能会跳过一个数字(这可能会造成混淆)或意外覆盖以前的结果。

反向引用在重新考虑如何使用外壳程序中起着至关重要的作用。请参阅以下部分“优于管道”:图形!进行介绍。

缺少SLIME功能SLIME具有相对的反向引用,其中*,**和***分别引用了last,last和next。

可悲的是,这使得无法引用从倒数第三到最后的结果。最糟糕的是,这意味着需要根据当前提示的相对位置来调整命令。

有人开玩笑地称CD为可怜的文件管理器! :)可能只有正义:缓慢,麻烦,效率低下。我们可以做的更好。

我相信每个人都应该能够使用他们喜欢的文件管理器。好消息:借助SLY,可以将目录“更改”到与文件管理器指向的目录相对应的文件!

作为Helm的忠实拥护者,我使用helm-find-files作为文件管理器。我发现使用它导航目录确实非常不错,因为您可以模糊搜索目录名称,而无需精确输入名称。

与Helm Switch to REPL扩展名一起,从helm-find文件中的任意位置按M-e会将所需的REPL切换到相应的目录。

要转到以前访问的目录,我可以按M-p来提示可搜索的历史记录。

使用helm-locate,我可以在指尖单击的范围内在所有myhard驱动器上的任何位置模糊搜索任何文件和目录。然后按M-e切换到所选文件的目录。

由于SLY默认情况下不使用Helm,因此我只需在Emacs配置中将历史记录绑定替换为对应的Helm命令:

在传统的外壳世界中,完成功能无处不在。 Zsh和Fishboast惊人的完成功能。 使用SLY,由于您现在编写Common Lisp并获得Common Lisp完成,因此该主题已被颠倒了! 因此,您可以完成Common Lisp函数,符号等。还可以使用sly-apropos(或helm-sly-apropos)从给定的包或任何包中模糊搜索任何符号。 调用函数时,将自动显示签名(感谢eldoc),以便您知道函数采用的参数。 与外壳程序中暴露的各种不一致的命令行参数相比,这使得使用起来更好。 在上面的< |中, 代表我的光标位置。 如果我在此处按Tab,它将提示可能完成的列表,我可以对其进行模糊搜索! 您还可以通过其他方式触发文件完成,例如在#p之后(这是Common Lisp中路径名的语法)。 请参阅此讨论。

此外,您甚至不必使用完成插入路径。相反,您可以使用文件管理器在REPL中插入选定的文件路径。 缺少SLIME功能,我可能错了,但似乎文件补全在SLIME中不起作用,因为REPL不是基于comint-mode。 也就是说,实现起来并不难。 将来的工作在REPL执行程序时,您可能仍希望外部程序完成。 例如,在 如果在上一行的末尾按Tab键,您可能希望查看ls接受的所有参数。 好消息 ......