在命令替换中使用外部调用的陷阱会破坏父级 Bash shell
Trap with external call in command substitution breaks the parent Bash shell
我有一个基于文本的用户界面脚本,允许我浏览目录和 select 文件。图形输出到stderr
,选择的文件路径发送到stdout
。这允许以这种方式获取所选文件:
file="$(./script)"
这非常方便,因为命令替换只抓取 stdout
。
但是我需要我的脚本来处理信号,这样当脚本被中断时,它可以重置显示。我设置了一个处理 INT
信号的陷阱。要模拟它在做什么,请考虑以下脚本:
catch() {
echo "caught"
ps # Calling an external command
exit
}
trap catch INT
while read -sN1; do # Reading from the keyboard
echo $REPLY >&2
done
然后使用 var="$(./script)"
调用脚本。现在,如果您通过点击 ^C
发送 INT
信号,父 shell 中断:您键入的任何内容(包括控制字符)都将被打印出来,直到您点击 return,然后将显示 none 的输入。
删除 catch
函数中的外部命令调用似乎可以解决问题(但 echo
似乎不起作用),但我不明白为什么,我在我的最终脚本中离不开它。
有什么我想念的吗?为什么这会破坏父 shell?
我未经证实但最好的理论是,这是由 Parent 读取终端设置和 Child 恢复它们之间的竞争造成的。
中断时,交互式 shell 将停止尝试从管道读取,并仔细检查当前的终端设置以避免以后破坏它们。如果 child 尚未恢复它们,parent 将读取错误的设置并假设终端应该是这样。
这解释了为什么您可以在它开始混乱之前键入一行:child 已将良好的设置恢复为缓冲规范模式,因此您可以键入整行。一旦你按下回车键,bash 得到命令,并且作为提示的一部分恢复它认为终端应该有的错误设置。
为了解决这个问题,您可以让 parent 在捕获期间处理 SIGINT。处理程序做什么并不重要,因为唯一的一点是导致 Bash 等待当前命令完成,以便它可以调用处理程序。
这是一个例子:
#!/bin/bash
catch() {
sleep 1 # Make sure to lose the race
echo "caught"
ps
exit
}
trap catch INT
while read -sN1; do # Reading from the keyboard
echo $REPLY >&2
done
这是在输入 x
并点击 Ctrl-C 后的交互式 shell:
bash-5.0$ trap 'true' INT; var=$(./script)
x
bash-5.0$ echo "The prompt works fine"
The prompt works fine
bash-5.0$ declare -p var
declare -- var="caught
PID TTY TIME CMD
650388 pts/3 00:00:00 bash
650859 pts/3 00:00:00 script
650862 pts/3 00:00:00 ps"
bash-5.0$
这里没有 parent 中的陷阱,展示了如何只有第一个命令直到第一次输入有效,而其余输入被隐藏:
bash-5.0$ trap - INT; var=$(./script)
x
bash-5.0$ echo "I can see this first line"
I can see this first line
bash-5.0$ bash: fasdfasdfasdfasdfa: command not found
由于其他用户似乎同意这是一个错误,因此我提交了错误报告。我得到以下答案:
This is a race condition -- the parent shell handles the SIGINT before it should. This will be fixed in the next devel branch push.
所以最好的办法是密切关注 Bash's git。
作为 "fix",我不得不重构要获取的脚本 (. script.sh
),以便它可以在不涉及临时文件的情况下与调用者通信,因为进程替换导致了确切的与命令替换相同的行为。
我有一个基于文本的用户界面脚本,允许我浏览目录和 select 文件。图形输出到stderr
,选择的文件路径发送到stdout
。这允许以这种方式获取所选文件:
file="$(./script)"
这非常方便,因为命令替换只抓取 stdout
。
但是我需要我的脚本来处理信号,这样当脚本被中断时,它可以重置显示。我设置了一个处理 INT
信号的陷阱。要模拟它在做什么,请考虑以下脚本:
catch() {
echo "caught"
ps # Calling an external command
exit
}
trap catch INT
while read -sN1; do # Reading from the keyboard
echo $REPLY >&2
done
然后使用 var="$(./script)"
调用脚本。现在,如果您通过点击 ^C
发送 INT
信号,父 shell 中断:您键入的任何内容(包括控制字符)都将被打印出来,直到您点击 return,然后将显示 none 的输入。
删除 catch
函数中的外部命令调用似乎可以解决问题(但 echo
似乎不起作用),但我不明白为什么,我在我的最终脚本中离不开它。
有什么我想念的吗?为什么这会破坏父 shell?
我未经证实但最好的理论是,这是由 Parent 读取终端设置和 Child 恢复它们之间的竞争造成的。
中断时,交互式 shell 将停止尝试从管道读取,并仔细检查当前的终端设置以避免以后破坏它们。如果 child 尚未恢复它们,parent 将读取错误的设置并假设终端应该是这样。
这解释了为什么您可以在它开始混乱之前键入一行:child 已将良好的设置恢复为缓冲规范模式,因此您可以键入整行。一旦你按下回车键,bash 得到命令,并且作为提示的一部分恢复它认为终端应该有的错误设置。
为了解决这个问题,您可以让 parent 在捕获期间处理 SIGINT。处理程序做什么并不重要,因为唯一的一点是导致 Bash 等待当前命令完成,以便它可以调用处理程序。
这是一个例子:
#!/bin/bash
catch() {
sleep 1 # Make sure to lose the race
echo "caught"
ps
exit
}
trap catch INT
while read -sN1; do # Reading from the keyboard
echo $REPLY >&2
done
这是在输入 x
并点击 Ctrl-C 后的交互式 shell:
bash-5.0$ trap 'true' INT; var=$(./script)
x
bash-5.0$ echo "The prompt works fine"
The prompt works fine
bash-5.0$ declare -p var
declare -- var="caught
PID TTY TIME CMD
650388 pts/3 00:00:00 bash
650859 pts/3 00:00:00 script
650862 pts/3 00:00:00 ps"
bash-5.0$
这里没有 parent 中的陷阱,展示了如何只有第一个命令直到第一次输入有效,而其余输入被隐藏:
bash-5.0$ trap - INT; var=$(./script)
x
bash-5.0$ echo "I can see this first line"
I can see this first line
bash-5.0$ bash: fasdfasdfasdfasdfa: command not found
由于其他用户似乎同意这是一个错误,因此我提交了错误报告。我得到以下答案:
This is a race condition -- the parent shell handles the SIGINT before it should. This will be fixed in the next devel branch push.
所以最好的办法是密切关注 Bash's git。
作为 "fix",我不得不重构要获取的脚本 (. script.sh
),以便它可以在不涉及临时文件的情况下与调用者通信,因为进程替换导致了确切的与命令替换相同的行为。