求解释epipe是怎么实现的?

发现一个好玩的脚本,可以在管道中调用外部编辑器来编辑内容。

它的实现方式只有寥寥数行:

#$!/usr/bin/env bash  -*- mode: sh; -*-
tty="/dev/$(ps -o tty= -p $$)"
temp_file=$(mktemp)
default_editor="emacs"
[ ! -t 0 ] && cat > $temp_file
${EDITOR:-${VISUAL:-$default_editor}} $temp_file <$tty >$tty && cat $temp_file
rm $temp_file

但是有几个地方我不太明白,求高手解释一下:

1。 第四行中的 [ ! -t 0 ] 是个什么意思? 按我的理解,要把stdin的内容读出来不是直接用 cat - 就行了吗? 2。 第五行调用编辑器来编辑临时文件时为什么要重定向stdio到终端tty呢?这有什么意义么?

以下都是猜测:

测试下有没有 stdin,有的话,就放到 temp_file 里面。

~ $ [ ! -t 0 ] && pwd # 没 stdin
~ $ : | [ ! -t 0 ] && pwd # 有 stdin
/Users/xcy

同样不明白,stdin 和 stdout 都指向当前的 tty?不清楚是不是一定要这个 tty,还是随便。为了避免干扰编辑器里的 stdin 和 stdout?

-t 是测试文件是否是tty设备文件,不过直接加个文件描述符可以吗,我表示怀疑,回去试一下

   -t fd  True if file descriptor fd is open and refers to a terminal.

重定向标准IO到tty大概是因为如Emacs等编辑器,需要它的标准IO指向的是终端设备文件

echo hello | emacs

试试

我试着把重定向那一块删掉,貌似也没问题…

(以下是依据测试给出的猜测)

好像 Vim 要求 stdout 必须是 terminal

~ $ ( vim tmp && cat tmp ) | nl 
Vim: Warning: Output is not to a terminal

work-around:把 stdout 改成 terminal

~ $ tty=`tty`
~ $ ( 1>$tty vim tmp && cat tmp ) | nl

Emacs 没这个要求

~ $ ( emacs -Q tmp && cat tmp ) | nl

但它们都要求 stdin 是 terminal

~ $ pwd | ( vim tmp && cat tmp )
Vim: Warning: Input is not from a terminal
~ $ pwd | ( emacs -Q tmp && cat tmp )
emacs: standard input is not a tty

work-around,把 stdin 改成 terminal

~ $ pwd | ( 0<$tty vim tmp && cat tmp )
1 个赞

@lujun9972 看见 GitHub 的 issue 了,我就在这边解释一下吧。原理很简单。

首先,重定向是必要的。具体的解释见 @xuchunyang 。他的回复里把除 emacsclient 以外的情况 (Vim,Emacs 等)差不多都解释清楚了,这也是原来 vipe 的实现。

接下来解释如何才能兼容 emacsclient。这里的问题在于在 vipe 的实现中,将 stdin 和 stdout 重定向到 /dev/tty。也就是说,第五行相当于是这样的:

${EDITOR:-${VISUAL:-$default_editor}} $temp_file </dev/tty >/dev/tty

一般来说,即使 stdin 和 stdout 被重定向过了,/dev/tty 仍然还是指向原来的 terminal (严格地说,/dev/tty 是当前 process 的 terminal),所以这是“找回”原有的标准输入输出的一种很普遍的做法,对于大部分情况来说,这样是没问题的,比如 Vim,Emacs。但是对于 emacsclient,这样不行。

查看 emacsclient.c,能看见里面会根据当前的 stdout 来获取 ttyname:

 ttyname (fileno (stdout))

然后将当前的 ttyname 发送给 server。如果此时 stdout 是 /dev/tty 的话 (vipe 的实现),这时候发送的就是 /dev/tty,然而 /dev/tty 对于 client 和 server 来说,含义是不一样的(他们不在同一个 terminal)。发送到 server 后,server 会以为是 server 本身的 process 所在的 tty,这个 tty 和 client 所在的 tty 不一样,然后导致无法创建 client 的frame。

解决方案是:显式获得 emacsclient 实际的 tty,然后将 stdout 设成该 tty (实际实现中 stdin 也修改了,这是为了简单方便),而不是发送类似别名一样的 /dev/tty,这也就是第一行的作用。或者修改 emacsclient.c 的代码,获取 stderr 的 ttyname (太麻烦)。

需要注意的是,emacsclient 只在 -nw 的时候需要获取 tty,如果你用图形界面的 emacsclient,并不需要上面的 workaround。也就是说,用 vipe 能满足除了 emacsclient -nw 以外的需求。

其实 epipe 没有解决所有问题。如果你理解里上面我说的,那么一个问题就是如果他们确实在一个 terminal 怎么办?如果是通过图形界面开启的 Emacs server,这时候没有 tty 关联,自然不会出现这种情况。但是如果是在 terminal 中启动 Emacs: emacs &,然后在同一个 terminal 调用 epipe,这时候就不 work 了。

我觉得避免这种情况就行。有好的想法的话可以提 issue 或者 PR。

2 个赞

我的测试结果到有点不一样,pwd | ( emacs -Q tmp && cat tmp )的结果居然没有出错。可能跟版本呢有关吧。 vi/vim到是确实有这个要求。

想不明白,server与client共用一个terminal会有啥问题?

额,我可能没表达清楚。我其实是想引入一个问题,如果 server 和 client 在一个 tty 上会怎样,然后这样的结果我测试是不行。至于原因,我也没彻底搞懂(所以我没解决这个问题)。用 strace 测试了一下,emacsclient 会卡在 recv 这个 syscall 上,也就是 server 没能送回答复。

如果有兴趣,欢迎解决这个问题~

注:上面有个 typo,引入这个问题的 server 的启动方式是 emacs -nw &

有点奇怪,我在arch的纯字符界面下实验是没问题的。难道是我的Emacs比较特殊?哈哈

不过你的解释很清楚,谢谢了。