终端转义序列:为什么终端不报告它们支持哪些功能,而是依赖 terminfo?

终端转义序列:为什么终端不报告它们支持哪些功能,而是依赖 terminfo?

我最近一直在研究转义序列,我对它们的功能感到惊讶。您甚至可以xterm用它们移动 X11 窗口(尝试一下printf '\e[3;0;0t'),哇!

了解终端支持哪些功能的最常见方法似乎是使用数据库。这就是它的ncurses作用,99.9% 依赖转义序列的应用程序都使用它。
Ncurses 读取terminfoshellTERM环境变量的数据库,以确定控制台支持哪些功能。
您可以更改TERMshell 的环境变量,如果您这样做,大多数应用程序可能会开始使用较少的功能或行为不当(尝试运行nanovim设置后TERM="")。

我发现一些转义码会导致终端报告内容。例如<ESC>[6n使终端报告光标位置。 ( printf '\e[6n')

  • 为什么我们不使用类似的报告机制来让控制台报告它支持哪些功能?

每个控制台可以宣传自己的功能,而不是将功能与 的值耦合起来TERM,从而使整个事情更加精确和可靠。为什么这不是一件事?


编辑:我之前应该问过的事情...我想创建一个新的转义序列,破解 konsole 和 gnome-terminal 以支持它并在某些脚本中使用它。
我希望能够查询控制台,以便了解我正在运行的控制台是否支持此功能 - 建议的方法是什么?

答案1

这并不像你想象的那么简单。 xterm(如以 VT100 开头的 DEC VTxxx 终端)有许多针对各种情况的报告特征(参考XTerm 控制序列)。最普遍有用的是告诉它是什么类型的终端:

CSI Ps c  Send Device Attributes (Primary DA).

并非所有终端都有这种类型的响应(Sun 硬件控制台有/有没有任何)。

但还有比报告更多的功能(例如,如何判断终端是否真正解释 UTF-8:可接受的途径是通过语言环境环境变量,因此不需要建立另一个控制序列/响应)。

在实践中,虽然有一些应用程序关注报告(例如维姆,检查功能键的实际值,使用的颜色数量DCS + p Pt ST,甚至光标外观使用DCS $ q Pt ST),该过程不可靠,因为一些开发人员发现返回给定的报告响应比实现该功能更简单。如果您通读各种程序的源代码,您会发现一些有趣的怪癖,其中有人定制了响应,使其看起来像某个版本的 xterm。

答案2

这是我对查询终端模拟器的转义序列的看法。

tl;dr:由于它们的异步特性,应用程序处理它们确实存在问题。 (不是通过终端模拟器,这是小菜一碟。)


首先,为了简单起见,我们假设所有终端仿真器都保证发送对所有此类查询的响应。 (这将要求查询具有明确定义的通用结构,而不是一次性转义序列,但情况似乎并非如此。)

让我们在脑海中设计和实现一个简单的实用程序(例如“ls”)以及一个更复杂的全屏应用程序(例如“mc”或“vim”),然后看看我们面临哪些问题。

  • Unix 的一个标准功能是,您可以在上一个命令运行时提前键入下一个命令(例如,键入“sleep 10”回车,然后键入“mc”回车,然后按 F5;大约 10 秒后,“mc”将打开复制对话框)。人们可能喜欢也可能不喜欢这一功能,但至少该行为在应用程序之间应该是一致的,这是不动态查询终端模拟器的应用程序的行为。我们还假设“mc”在启动时使用这样的查询转义序列来找出哪个功能。现在,mc 将在响应之前收到 F5 的转义序列。它可能会忽略它(在这种情况下,行为将与应用程序的其余部分不一致)或需要将其存储在某处,等待对查询的响应到达,然后处理此 F5。为了做到这一点,它不能再让每个组件直接从 stdin 读取和处理,它之间需要一些包装层。可行,但需要相当多的努力来实现(即使对于从头开始的项目,必要的额外工作也是显而易见的,更不用说那些已经在没有这个标准的情况下实现的项目,因此你必须大量重构。)

  • 如果在操作过程中出于任何原因需要查询终端,也会发生类似的情况。然后在响应到达之前丢弃中间的“正常”字符是绝对不可接受的。

  • 现在,想象一下 shell 脚本本身需要读取此响应以及来自用户的其他正常输入(来自标准输入),而不是用 C 或某种类似语言编写的实用程序。你要怎么走?对于处理此类响应的代码中的每个“读取”,您是否会手动将终止符设置为换行符或响应转义序列的末尾,并从其余部分中找到并剥离转义序列?如何在用户输入仍然出现的情况下使转义序列响应不出现在屏幕上?在期望转义序列响应到期望用户数据的后续“常规”“读取”命令时,您将如何“馈送”收到的正常输入?我不知道如何做到这一点,但即使可以做到,它显然也是难以忍受的复杂、乏味和容易出错。我可以想象的唯一可合理实现且可靠工作的方法是删除预先输入的字符(导致异常行为)并仅在脚本启动时处理此类响应转义序列。

  • 如果您的简单实用程序(例如“ls”)在循环内的 shell 脚本中使用,则终端仿真器和应用程序之间的往返时间可能会变得很重要。在单核系统上,需要在两个应用程序(实用程序和终端仿真器)之间进行上下文切换,尽管与无论如何都会发生的 fork()+execve()+friends 相比,它可能并没有那么糟糕。在多核系统上,我想这是没有必要的,尽管我不确定细节。然而,如果涉及实际网络流量,成本(延迟)可能会变得非常显着。

  • 在极少数情况下,应用程序在没有读取响应的情况下退出(例如,它崩溃或被杀死),响应出现在您开始输入的下一个命令处(我确信您已经看到过这种情况,例如当您不小心cat'ed时)二进制文件)。


现在,我们假设有一些终端模拟器无法识别(或只是选择不响应)某些查询转义序列,包括将来的转义序列。这就是目前的情况。这使得之前的所有要点变得更加困难。在这种情况下,您不能冒应用程序冻结的风险,因此它需要超时。

  • 你要等多久才能得到答案?如何弥补任意超时?您是否(以及是否、如何)根据网络特性(例如,本地终端仿真器、ssh 到邻居建筑物、ssh 到地球另一端)调整此超时?

  • 如果响应没有及时到达(例如由于 ssh 延迟)怎么办?您的应用程序会继续处于用户可见的降级模式吗?

  • 如果响应晚于应用程序放弃怎么办?您的应用程序可能需要为此类响应做好准备,即使响应到达根本不期望的地方。 (例如,在 shell 脚本中,您最初查询某个状态,等待超时响应,但是稍后的每个“读取”都需要准备好被这种延迟响应污染。)

  • 超时会增加很多往返时间,可能比内核上下文切换甚至网络延迟长得多。将此类命令放入 shell 脚本循环中绝对令人难以忍受,但甚至可能对交互式应用程序的可用性产生明显的负面影响。


展示我认为可行的替代方案超出了本回复的范围。 TERM 变量的设计也有很多限制,这里我不打算赘述。对于查询功能(对于终端仿真器来说是静态的,而不是当前属性,例如光标位置),我可能会从描述实际行为的 TERMCAP 方向开始。它甚至可以指向本地文件,并且可以像现在一样称为 TERM,但是类似 ssh 的实用程序将负责将其实际内容转发到远程站点并将 TERM 指向该文件,其方式与 .Xauthority 类似。另一种完全不同的方法可能是使用第四个标准文件描述符来与终端仿真器进行元通信。

相关内容