前言 Link to heading

近日,我对自己的 dotfiles 做了一定的调整。抛弃了用了多年的 alacritty,也抛弃了前一段刚刚切换到的 fish。而是转为使用 eshell 做为主力的 shell。在此,想就此决定的考虑进行讨论。

什么是 Shell? Link to heading

最直观的想法,就是通过一条条指令来完成工作的一个黑乎乎的工具。在一个熟练工的手上,这一行为会比使用 gui 更加的高效,并且有更高的自由度。什么是一条条指令?本质上就是一个个非常小的程序,或是 shell 本身自带的一些指令。这些指令与工具,提供了与系统内核交互的能力。将需要通过 syscall 完成的事情,包装成了一个简单的程序,通过 shell 这一载体来完成程序调试从而给用户提供与操作系统交互的能力。所以,一开始学 Linux shell 时,学习就是这些非常小的工具与 shell 内置的指令,而它们基本上就是 linux 里常用 syscall 的直接包装。通过一些定制化的参数从而来提供对用户更加便利的控制。

所以,它被叫做壳,一个内核的壳。

为什么在 shell 中完成日常工作? Link to heading

与 GUI 应用相比,在 shell 内我们面向的工具粒度更细,有更多的可控制项,以及最重要的,我们有可编程性的控制能力。GUI 能不能完成这样的行为?当然能,但是这非常依赖于开发者对整个程序的设计。而 shell 这一种行为,以非常简单明了的方式,直接提供了高自由度与系统交互的能力。并且这一行为通过脚本的方式可以直接进行批量操作,完成自动化。对于 GUI 程序,要完成这样的行为需要大量的设计,而且局限性会很高。shell 的语法设计同时保证了简单场景自动化的编写与用户交互的使用。相较于图形界面,它可以让我们脱离鼠标,并且在更加丰富的参数中按需使用。

当然,参数设计是由人来设计,图形界面也能完成这样的事情。就像不同的图形界面有不同的设置方式,shell 里每个程序的参数也有不同的设计风格,不论是哪种形式,都需要通过文档来进行知晓,程序也需要提供相应的可配置项。得益于历史发展,大部分命令行中的程序,其可控制参数的设计要优于图形程序,它们提供了更多选项。

进一步,很多操作,其实并不需要一个可视的行为,我们需要的只是从中得到系统内的一些数据,比如目录结构、文件内容。简单的程序可以以简单的格式进行数据的展示,也意味着用户可以自己来对这些数据进行二次加工与处理。没有图形界面也意味更加轻量的执行代码,更加专注于功能本身的执行。

或者说,我们不可能为每种行为都去设计一个图形界面,那样即显得冗余,也难以人为得在图形间进行数据传递时介入。

什么是 TUI Link to heading

TUI 相较于 GUI 而言,则就是在没有图形渲染,在纯文本的情况下来渲染出类似与图形的一些结构,更加近于人类区分数据、获取软件状态的工具。它们与 GUI 相比除了轻量外,并没有特别之外。虽然不依赖于图形渲染,但是这种文本渲染成结构化的“窗口”也需要大量的输出控制,这种控制一般也都是一些公共库来实现,在这一点上,其实与 GUI 并无不同。TUI 能做的 GUI 也能做。通过 ssh x forward,也一样可以直接在远程机器上获取数据,在本地机器上渲染图形来查阅的能力。

以我现在的观念而言,TUI 与 GUI 并无差异,它们都非常的重。而且要高度依赖于其运行的环境。GUI 要依赖于运行的图形服务,TUI 要依赖于其所被渲染的 virtual console。需要一个庞大的渲染框架来控制其渲染(只是纯文本本身要比图开轻量)。但想想看,现在又有多少使用 tty,16 色的场景呢?程序输出的核心是数据,数据并不一定是 TUI 的方式来展示,最直接的是原始数据的直接输出,而要如何展示让用户自己来控制不是更好?GUI/TUI 就可以是这种数据展示的一种工具,而非生成数据本身的功能。比如仿真软件 omnet++,它可以无图形、独立的生成结果数据,也可以使用其图形工具来查阅数据,也可以用使用者自行来处理数据选择其它的工具进行展示。

因此,软件本身设计时,我们需要考虑到:产出数据、控制模式、用户交互,这三项是分离的。其中前二者可以合为一体,而后一者是可以让用户来决定的。特别是控制模式,我认为好的软件设计应该提供一个非用户直接交互的控制方式,比如通过套接字,或是 stdout/stdin,或是系统信号等,这样用户可以由它们自己来选择自己所需要的交互(这不就是 C/S 架构嘛)。极端一点,甚至可以是一个开发库,由用户自己来编写调用控制逻辑。

需要承认,图形的展示能力是远强于纯文本的,但要注意,很多时候,我们不需要一个很强力的展示功能,我们要注意的是执行与结果本身。这种情况,分离设计是最好的。TUI 这个话题并不是很重要,因为它是一个前端,而是操作本身,但是有很多功能,把这二者强耦合在一起(当然,也有工具很好的对环境做了判断,不同的执行环境、编译参数,以不同的形式来展示结果)。图形界面的软件这类强耦合的设计更多,另人感慨。

什么是终端模拟器 Link to heading

现如今,图形界面对于人而言基本是必需品。不可否认,它们提供了更加好的视觉效果,与展示数据的能力。但同时,有相当一部分软件,就其功能而言并不需要图形界面来与用户交互,在图形环境下来执行无图形程序的一种交互方式,即终端模拟器。里面跑着的,就是没有图形场景下的 shell。

不知何时起,人们为了效率与美观,开始对终端模拟器添加各样的功能。比如更好的字体渲染、分屏。相较传统的终端,这是提高效率的方法。但同时,这也使得这些程序变得越来越庞大。甚至做起了在终端里渲染网页、图形的事情。感觉就像是在图形界面里,又完成了一个内嵌图形界面的渲染。

有点跑题了,就目的上来说,是为了提供一个自动化、无图形程序交互的图形程序。

假如终端模拟器并不是图形界面下来实现无图形工具调用的接口,那么这些功能可能也不会像现如今这样发展。或者,有一个更加简单的无图形界面调用工具,那么可能这些工具也不会像现如今这样持久讨论。但实际上是,shell 的这种设计,同时提供了高定制性与易用性,现在并没有可以完全替代的场景。

当然,shell 能长久存在是因为其设计使用足够简单、便利,其设计又易于理解,又直接与系统交互,有足够的扩展能力。基本上不太存在需要重新设计新的封装形式。像是手机或 windows,它们的全图形操作的前提在于,所有的功能都是图形的,并没有非常直接与系统内核的交互,提供的均上层功能。软件与软件间的交互是非常受限的,也不太有用户可以直接介入余地。

shell 的效率工具 Link to heading

在现代终端模拟器中,很少有直接提高 shell 本身能力的功能,但多的是在外部进行一些扩展。比如窗口分隔、字体渲染。像 iTerm 这类,则通过侵入式的工具来完成了一定程度上原 shell 扩展,但这种行为非常依赖于 shell 本身与模拟器的支持,它并不通用。为了解决这些,各种各样的小工具被开发出来,以提供在终端下完成各类重复性事项的能力。

比如:fzf、zoxide、completion 等。它们提供了更快速的路径切换、文件查寻等。以得到类似于图形界面下,通过文件浏览器快速切换目录而非手动打完每一个路径的功能。提供了可以快速选择历史指令并进行重复执行、协助记录的功能,甚至提供了文件预览等能力。它们的实现即使在纯终端也是可行,但在终端模拟器中,可以有更丰富的色彩加以辨别,更甚通过图片进行渲染。

什么是 Bash/Zsh/fish? Link to heading

它们是一种 shell 的实现。以提供人类方便交互为前提而设计的 shell。就与系统内核交互这一点,并不一定得是它们。比如你每次系统启动后,与用户交互的是一个 python 解释环境也不是不可,只不过这一解释环境并不是针对日常使用、与其它程序交互而设计的,它不够便利。

换句话说,如果有个工具,它能提供足够便利与完善得与系统内核交互的能力,我们并不一定需要通过上面这些工具来进行日常的调用。

需要注意,这里我想说明的是与人交互的这一部分。这些工具,特别是 Bash,因为广泛的使用与系统的各个组件中,除非你要把所有的基础系统组件都通过其它方式来实现一遍,在当前的 linux 中是不可能被剔除的(例如开机流程的控制、xorg 启用初始化的脚本、图形启动环境的准备都是它们完成的)。

可以不用终端模拟器么? Link to heading

答案当然是肯定的。就如上所说,如果我们有一个能方便、高自由度的交互软件,并且可以提供与上述那些 shell 扩展工具一样的效率,当然就不需要它们了。

那肯定是有的啦,不然这篇文章是在干什么嘛。

首先,让我们明确一下为什么我们要用终端模拟器:

  1. 无图形程序的调用
  2. 高效的输入与执行、较少的源
  3. 更直接的结果数据与输入控制
  4. 数据的二次加工
  5. 高效的自动化流程
  6. 可能是一种喜爱

排除第 6 点外,其它的即使不在终端模拟器上也能完成。

说是有,其实也不完全是有。因为核心上还是一个 shell,只不过是另外一种壳罢了。摆脱了终端模拟器的一种壳,但依赖于 emacs 的一个壳。

emacs 本身是一个提供了 lisp 执行环境的编辑器。这里自然也提供了与 shell 相似直接与内核交互的功能(不如说是个软件都得与内核相互),提益与 lisp 与高度可编程的执行环境,它将与内核交互的功能也像上述 shell 一样提供给了用户。而基于这一能力实现了 eshell。什么嘛,这不就是个 shell 么。除了没有其它 shell 那样 tui 渲染的功能而已。但 tui 渲染其实还是需要模拟器来支持的(这里还要涉及到获取窗口大小,可渲染区域等等等)。

eshell 里,可以直接调用 emacs lisp 定义的各种函数,这里当然也包含了我们常用的与系统内核交互的那样操作。基于 emacs 的生态,我们也可以直接来完成补全、文件选择等能力,而不再是通过侵入式与多工具结合的形式。所以它也是一种壳。只不过是 emacs 上的一种壳。而 emacs 又可以被视为系统内核的一个壳。它做的事情与 shell 并没有太多差别,但是这让我们可以脱离终端模拟,脱离各类重新设计的各种 shell,各种可以因终端模拟器不够强大而去拓展出来但并不好用或兼容性很差的特性。

eshell Link to heading

再一次强调,如其名,eshell 也是一个 shell。只不过是基于 emacs 的,但正因此,我们不再需要各种终端模拟器了。不再需要各种针对不同 shell 与终端模拟器设计的效率功能,而是直接使用为 emacs 设计的代替品。

eshell 里,我们可以通过像其它 shell 里一样,简单直观的调用外部程序、获取数据。同时又能编写 lisp 来完成简单地参数的传递、数据的处理。注意,是简单的、与人交互的。复杂的事情,通常需要更长的上下文,反复被调用,那肯定还是要形成便于维护与阅读的脚本(题外话,jupyter note 这样的设计也可以认为是这种场景的一种交互的设计,但它专注点不同,它更加注重的是文档性、给人查阅的结果的展示,而非操作本身)。

在 eshell 里,我们可以直接使用 emacs 的补全框架、emacs 的历史记录、emacs 的书签、emacs 的函数等。比如一个 grep,我们在 eshell 里通过 rx 来帮我们生成正则表达式。

当然,eshell 不是一个终端模拟器,它不具有一些 tui 渲染所需要的功能。但这不重要,因为 tui 程序与 gui 程序一样,通常它们功能是复杂的、是由一个个小组件组合而成。我们可以直接使用 emacs 包装的各种其它功能来使用,而是局限与 tui 这一行为。

软件状态与上下文 Link to heading

到上一节为止,原本想到的基本都有了。这里想讨论的内容与原主题不一。考虑 XMonad 与 emacs 的设计。

前者本身是一个库,一个提供了入口函数的库。用户通过 haskell 来定义各种状态的变化与状态的初始值,来形成最终的程序。这一程序在运行期并不具有动态增减其状态变化的能力(其实也有,但会比较麻烦)。emacs 而基于 lisp 的内嵌语言,用户可以在其运行其任由改变其上下文环境。

前者非常注重统一性与不可变性,后者则有非常便利的运行期拓展能力。

对于前者,状态变换的定义是预先的,是可控的,状态变换的输入是运行期产生的,但状态变换的定义是无副作用,可预测的。但后者的设计确实也提供了非常高的运行期定制性,相对的所有的操作都是有副作用的,是受前一操作后的上下文而影响的,并非由输入而直接决定。

做为一个使用者,我喜欢后者的便利;做为一个开发者,我喜欢前者的严格约束。或许,我们可以用前者想法,来设计出一个允许定义状态变换的变换(怎么感觉就像是 JIT),将执行环境的定义也定义为一种变换,再而配合后者来完成使用上的便利。不过现在似乎并没有这样的软件,或是不是常用的软件。毕竟这样的设计过于复杂,远不如直接内嵌隐性的全局状态变换来得简单。