相同的命令序列在命令行和脚本中产生不同的结果

相同的命令序列在命令行和脚本中产生不同的结果

我正在编写一个实用程序脚本来帮助我删除 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 使自己回到前台进程组,从而导致 和cmd1cmd3处于后台:

~$ (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 的调用方式。

相关内容