发现一个好玩的脚本,可以在管道中调用外部编辑器来编辑内容。
它的实现方式只有寥寥数行:
#$!/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比较特殊?哈哈
不过你的解释很清楚,谢谢了。