epipe的实现原理是什么?
最近发现一个好玩的脚本,可以在管道中调用外部编辑器来编辑内容:
https://github.com/cute-jumper/epipe5
它的实现方式只有寥寥数行:
#$!/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
然而就这么几行的脚本,我却发现看不懂~~~
主要有三点:
- [ ! -t 0 ] 是什么意思
- 用编辑器来编辑临时文件时为什么要重定向stdio呢?
- 为什么重定向的时候不直接重定向到
/dev/tty
而要显示的获取编辑器实际的tty呢?
在经过与大伙的讨论后, 最终引来原作者的解释,才最终恍然大悟. 现在把讨论的结果整理如下(其中大量引用了讨论时的原文):
首先第一个问题其实很简单, -t fd
用来检查 fd
是否是是表示tty设备的文件描述符,所以这句话就是确保只有在该脚本的stdin被管道连接后才用cat从管道中读出内容放到临时文件中.
第二个问题,为什么要重定向编辑器的stdio呢? 这个问题可以通过几个实验来解答.
Vim 要求 stdout 必须是 terminal
~ $ ( vim tmp && cat tmp ) | nl Vim: Warning: Output is not to a terminal
解决方案是将stdout重定向到terminal
~ $ tty=`tty` ~ $ ( 1>$tty vim tmp && cat tmp ) | nl
Vim 要求 stdin 也必须是 terminal
~ $ pwd | ( vim tmp && cat tmp ) Vim: Warning: Input is not from a terminal
解决方案是将stdin重定向到terminal
~ $ pwd | ( 0<$tty vim tmp && cat tmp )
也就是说,至少对于某些编辑器而言,是要求stdio必须为terminal的.
那么为什么不能直接重定向到 /dev/tty
呢? 毕竟即使 stdin 和 stdout 被重定向过了,/dev/tty 仍然还是指向原来的 terminal (严格地说,/dev/tty 是当前 process 的 terminal),这也是“找回”原有的标准输入输出的一种很普遍的做法啊.
对于大部分情况来说,这样是没问题的,比如 Vim,Emacs。但是对于 emacsclient,这样却行不通,这实际上与emacsclient的实现有关.
查看 emacsclient.c,能看见里面会根据当前的 stdout 来获取 ttyname:
ttyname(fileno(stdout))
emacsclient将当前的 ttyname 发送给 server后。如果此时 stdout 是 /dev/tty 的话 ,这时候发送的就是 /dev/tty,然而 /dev/tty 对于 client 和 server 来说,含义是不一样的(他们不在同一个 terminal)。发送到 server 后,server 会以为是 server 本身的 process 所在的 tty,而这个 tty 和 client 所在的 tty 不一样,这就会导致无法创建 client 的frame。
解决方案是:显式获得 emacsclient 实际的 tty,然后将 stdout 设成该 tty,而不是发送类似别名一样的 /dev/tty,这也就是第一行的作用。