我正在编写一个实用程序脚本来帮助我删除 zsh 中的 Git 分支。目前,它看起来像这样:
git for-each-ref --format="%(refname:short)" refs/heads/ |
while read -r line; do
printf "remove %s? (y/n)" $line;
read ans </dev/tty;
case "$ans" in
y|Y) echo "$line";;
esac;
done
但是,当我直接从命令行而不是作为脚本运行相同的命令序列时,会跳过第一个分支。
git for-each-ref --format="%(refname:short)" refs/heads/ | while read -r line; do printf "remove %s? (y/n)" $line; read ans </dev/tty; case "$ans" in y|Y) echo "$line";; esac; done
知道为什么会出现这种行为差异以及如何解决它吗?
答案1
在 中 zsh
,与在 中一样ksh
,管道的最后一个组件在当前 shell 中运行,而不是在子 shell 中运行。
当您在交互式 shell 实例中在前台运行作业时,shell 会创建一个新的进程组,并使该进程组成为终端的前台进程组。然后,它在放入该进程组的作业中生成的所有进程。
然而,在:
cmd | { some-builtin; some-builtin; }
它是运行的主要 shell 进程some-builtin
。并且该进程已经是进程组的领导者,因此不能放入前台进程组。这意味着只要正在运行的进程cmd
没有退出,它就会一直处于后台。
不在前台进程组中的进程无法从其控制终端读取数据。如果这样做,它将被一个SIGTTIN
信号挂起,如果它忽略这些信号(就像交互式 shell 的主进程所做的那样),read()
系统调用将失败并出现 EIO 错误。
在 ksh93 中看不到相同内容的唯一原因是它的read
内置函数在读取之前执行select()
/poll()
操作,因此如果在返回之前输入某些内容,您只会得到 EIO git
。
在 中zsh
,存在更严重的问题,例如:
cmd1 | { cmd2; cmd3; }
cmd1
并将cmd2
被放入相同的进程组和前台,但在运行时cmd3
, zsh 使自己回到前台进程组,从而导致 和cmd1
都cmd3
处于后台:
~$ (sleep 1; cat) | { ps -opid,pgid,tpgid,stat,args; ps -opid,pgid,tpgid,stat,args; sleep 2; echo done; }
PID PGID TPGID STAT COMMAND
571038 571038 653553 Ss zsh
653553 653553 653553 S+ zsh
653554 653553 653553 S+ sleep 1
653555 653553 653553 R+ ps -opid,pgid,tpgid,stat,args
PID PGID TPGID STAT COMMAND
571038 571038 571038 Ss+ zsh
653553 653553 571038 S zsh
653554 653553 571038 S sleep 1
653556 653553 571038 R ps -opid,pgid,tpgid,stat,args
zsh: suspended (tty input) sleep 2
$ ps -opid,pgid,tpgid,stat,args
PID PGID TPGID STAT COMMAND
571038 571038 654415 Ss zsh
653553 653553 654415 T cat
653557 653553 654415 T sleep 2
653558 653558 654415 T zsh
654415 654415 654415 R+ ps -opid,pgid,tpgid,stat,args
(+
前台的意思,TPGID是前台进程组)
在这里,您可以通过运行整个管道或子 shell 中最右边的元素来解决问题:
(git | while ...; done)
或者:
git | (while ...; done)
这种问题不会发生在脚本中,因为 shell 在非交互时不会进行作业控制。脚本中的所有进程都在同一个进程组中运行,该进程组将在前台或后台(假设它在终端中运行),具体取决于启动它的 shell 的调用方式。