TL;DR:我们来看看当Unix编程模型排除检查输出本身时,Zsh和Fish如何能够在程序输出中指出缺少的终止换行符。
大多数shell,包括bash、ksh、dash和ash,都会在前一个命令退出光标时离开光标的位置显示提示符。
提示符(几乎)总是出现在下一行熟悉的最左边一列,这是因为Unix程序在退出时普遍合作将光标停在那里。
要做到这一点,请始终确保输出终止换行符\n(也称为换行符):
vidar@vidarholen-vm2~$whamividarvidar@vidarholen-vm2~$whomami|HEXDUMP-c0000000 v i d a r\n。
如果程序未能遵循此约定,提示将在错误的位置结束:
但是,我最近注意到,zsh和fish将改为显示一个字符,指示缺少换行符,并且仍然在您期望找到它的位置开始提示:
vidarholen-vm2%ECHO-n34;hello zsh";hello zsh%vidarholen-vm2%vidar@vidarholen-vm2~>;ECHO-n34;hello fish⏎vidar@vidarholen-vm2~>;
如果您对有一整篇博客文章都是关于这一点感到失望,那么您可能还没有尝试编写一个shell。这是一个问题,你知道的越多,看起来就越难(强制性的XKCD)。
如果您心中有一个简单的解决方案,可能是这样的:if(!output.ends_with(";\n";))printf(";%\n";);,请考虑以下限制*:
与普遍认为的相反,shell并不位于程序和终端之间。外壳不能拦截或检查程序的终端输出。
终端编程模型基于电传打字机(又名TTY),即20世纪初的机电打字机。他们逐个字母地打印到纸上,因此没有内存或屏幕缓冲区可以编程回读。
外壳可以使用管道拦截所有输出,并将其中继到终端。虽然它在woami这样的普通情况下有效,但有些程序会检查stdout是否是终端并改变它们的行为,有些程序会越过您的权限直接与TTY对话(例如,ssh的密码提示符),有些程序会使用特定于TTY的ioctls,如果输出不是TTY,比如查询窗口大小或禁用密码输入的本地回显,这些ioctls就会失败。
外壳程序可以ptrace该进程,以查看它在哪里写入了什么内容。这会带来巨大的开销,并破坏sudo、ping和其他依赖suid的命令。
shell可以创建伪tty(Pty),在其中运行命令,并像ssh或脚本一样来回转发信息。这是一种烦人且笨重的方法,其最终形式需要重新实现整个终端仿真器。
外壳可以使用ECMA-48光标位置报告功能:printf';\e[6n&39;]在支持的终端上,将导致终端模拟表单^[[y;xr]上的用户输入,其中y和x是行和列。然后,外壳可以读取它来确定光标的位置。这些类型的往返是可行的,但是对于如此简单的功能,实现起来有些缓慢和烦人。
相反,Zsh和Fish有一种简单得多、聪明得多的方法来做这件事:
这个解决方案非常简单,因为它只需要在每个提示之前打印一个固定的字符串,但它在所有终端上都非常有效。
让我们假设我们的终端有10列宽,3行高,一个规范的程序刚刚编写了一个带有尾随换行符的短字符串:
光标(由|表示)位于该行的开头。这是在步骤1和2中将发生的情况:
此时将显示指示符,由于我们正好写入了$Column字符,因此光标位于最后一列之后。步骤3(回车符)现在将其移回起点:
最终结果与我们简单地写出光标所在位置的提示符完全相同。
现在,让我们看看当程序没有输出终止换行符时会发生什么:
将显示指示符,但这一次步骤2中的空格会导致该行一直换行到下一行:
提示现在显示在该行上,因此不会覆盖指示器:
现在你就知道了。一个看似简单的问题被证明比预期的要难,但巧妙地使用换行使它再次变得容易。
现在我们知道了秘诀,我们当然可以在Bash中做同样的事情:
虽然很有用并且经常被请求,但是没有可靠的方法来获取先前执行的命令的输出。
截取终端的屏幕截图/转储令人惊讶地棘手,而且它只在特定的终端上工作。
背景进程输出装饰性地破坏前景进程的现象是众所周知的,但是没有解决方案