暗无天日

=============>DarkSun的个人博客

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

然而就这么几行的脚本,我却发现看不懂~~~

主要有三点:

  1. [ ! -t 0 ] 是什么意思
  2. 用编辑器来编辑临时文件时为什么要重定向stdio呢?
  3. 为什么重定向的时候不直接重定向到 /dev/tty 而要显示的获取编辑器实际的tty呢?

在经过与大伙的讨论后, 最终引来原作者的解释,才最终恍然大悟. 现在把讨论的结果整理如下(其中大量引用了讨论时的原文):

首先第一个问题其实很简单, -t fd 用来检查 fd 是否是是表示tty设备的文件描述符,所以这句话就是确保只有在该脚本的stdin被管道连接后才用cat从管道中读出内容放到临时文件中.

第二个问题,为什么要重定向编辑器的stdio呢? 这个问题可以通过几个实验来解答.

  1. 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
    
  2. 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,这也就是第一行的作用。