如何从函数发出“全局”bash 范围内的陷阱信号?

如何从函数发出“全局”bash 范围内的陷阱信号?

我想在“全局”范围内执行陷阱命令,但信号将来自函数内部。当然,可以预先全局声明变量或使用-g声明选项。但在我想获取陷阱来源的情况下,这不太实用,如下所示:

#!/bin/bash
# ./variables
declare say=hello
declare -i times=4

和实际的脚本:

#!/bin/bash
# ./trapsource

setTraps () {
  trap 'echo trap called in scope ${FUNCNAME[@]}; source ./variables' SIGUSR1
}

sourceVariables () {
  kill -SIGUSR1 $$
}

setTraps
sourceVariables
echo I want you to $say \"hello\" $times times.
printf "$say\n%.0s" $(seq 1 $times)

但是没有declare -g NAME,谢谢。

编辑:

是否可以将该kill -s USR1 $$部分完全从当前进程中分离出来,以便信号来自“外部”?

我尝试过nohup,但disown到目前为止没有成功。

PS:@LL2 在这里提供了使用后台进程或 coproc 的解决方案:https://unix.stackexchange.com/a/518720/154557 但我担心会出现时间同步问题并使代码变得异常复杂。

PPS:实际上@LL2解决方案甚至可以很好地与IPC配合使用,所以我认为这个问题已经解决了。缺点是,如果需要在主作用域中进一步使用,函数参数必须在源文件中进行处理,因为子 shell 中的符号当然会丢失。看看编辑2解决此问题的另一种解决方案。

#!/bin/bash
# ./trapsource
set -x  # <-- set debugging to see what happens

setTraps () {
  trap 'declare IPC=./ipc.fifo; \
    [ -p $IPC ] \
    && exec {IPCFD}<>$IPC \
    && { read -a ARGS <&$IPCFD; eval "exec $IPCFD>&-"; } \
    && source "${ARGS[@]}" \
    && IPC_RCV=true' SIGUSR1
}

sourceVariables () {
  declare IPC=./ipc.fifo
  declare IPC_RCV=false
  [ ! -p $IPC ] \
  && mkfifo $IPC
  [ $# -gt 0 ] \
  && exec {IPCFD}<>$IPC \
  && echo "${@:2}" >&$IPCFD \
  && eval "exec $IPCFD>&-" \
  && kill -s USR1 $1
}

test () {
  sourceVariables "$@" &
}

setTraps
test $$ ./variables a b c
while ! [ $say ] ; do :; done  # <-- careful: cpu-intensive loop, only for 
demonstration
echo I want you to $say \"hello\" $times times.
printf "$say\n%.0s" $(seq 1 $times)
printf "'%s' " "${args[@]}"

编辑 2(提案 3 正在进行中):

有没有人有足够的 bash 自定义内置经验来指出可以实现真正解决方案的方向?

PS:Bash 可加载内置函数非常强大,最有可能用于在主范围内接收 dbus 方法调用。

解决方案 II(没有 bg 或 coproc):

看来信号总是在当前执行行的范围内触发。很可能还有另一个更复杂的解决方案,使用自定义 bash 内置函数(非常强大的可加载内置函数)。我什至可以想到 IPC 的 bash-dbus-builtins。不管怎样,如果你想告诉自己/另一个在后台休眠的PID在信号上获取某些脚本,而不必在源脚本中设置-g全局标志./变量,那么使用别名的解决方案可能会很有用,因此./如有必要,变量脚本也可以在函数范围内专门使用。由于别名无法导出到子 shell,因此必须将别名包含到 BASH_ENV 变量引用的环境脚本中。在这里,我用 IPC 机制扩展了原始示例,以使用例的目标更加清晰。

变量存储现在也接收参数

#!/bin/bash
# ./variables
declare say=hello
declare -i times=4
declare -a args=("$@")

首先我准备一个环境脚本

#!/bin/bash
# ./env
shopt -s expand_aliases
alias source_ipc='source ./ipc'
alias source_ipc_rcv='source ./ipc_rcv'

并确保别名在每个 bash 脚本中可用。

$> export BASH_ENV=./env

此外,我需要使用 fifo 的实际 IPC 发送和接收机制

#!/bin/bash
# ./ipc

declare IPC=./ipc.fifo
declare IPC_RCV=false
[ ! -p $IPC ] \
&& mkfifo $IPC
[ $# -gt 0 ] \
&& exec {IPCFD}<>$IPC \
&& echo "${@:2}" >&$IPCFD \
&& eval "exec $IPCFD>&-" \
&& kill -s USR1 $1

加ipc接收

#!/bin/bash
# ./ipc_rcv

declare IPC=./ipc.fifo
[ -p $IPC ] \
&& exec {IPCFD}<>$IPC \
&& { read -a ARGS <&$IPCFD; eval "exec $IPCFD>&-"; } \
&& source "${ARGS[@]}" \
&& IPC_RCV=true

现在我可以跨多个进程透明地触发源信号

#!/bin/bash
# ./trapsource u+x

trap 'source_ipc_rcv' SIGUSR1

log="$0.log"
err="$0.err.log"
exec 1>$log
exec 2>$err

# test inside script
source_ipc $$ ./variables a b c

while true; do
  echo I want you to \"$say\" $times times.
  if $IPC_RCV; then
    printf "$say\n%.0s" $(seq 1 $times)
    printf "'%s' " "${args[@]}"
  fi
  sleep 1
done

从进程命名空间的“外部”,可以按如下方式触发信号。我假设 Alias 已经由 BASH_ENV 引用提供。

$> ./trapsource & TSPID=$!; sleep 1; source_ipc $TSPID ./variables arg1 arg2; sleep 2; kill $TSPID

答案1

trap 'echo trap called in scope ${FUNCNAME[@]}; declare say=hello; declare -ri times=3' SIGUSR1

declare -i不要在陷阱中使用,而是预先执行此操作,然后在陷阱中分配新值:

declare -i times=999
trap 'times=3' USR1

我认为你甚至可以readonly times在陷阱中使用,因为readonly它本身并不使变量成为本地变量。

例如,这会打印 1, 3, 3,然后会打印一个修改只读变量的错误。

#!/bin/bash
trap 'readonly num=3' USR1
sub() {
        kill -USR1 $$
        echo "$num"
}

declare -i num=999
num=1
echo "$num"
sub
echo "$num"
num=1234

话又说回来,如果你要修改的是全局变量,为什么不使用declare -g呢?

是否可以将该kill -s USR1 $$部分完全从当前进程中分离出来,以便信号来自“外部”?

kill我认为脚本本身(内置)发送的信号与另一个进程发送的信号之间没有区别。正如您所看到的,Bash 似乎在运行函数的上下文中运行陷阱代码。

答案2

是否可以将kill -s USR1 $$部分完全与当前进程分离,以便信号来自“外部”?

问题不在于您的信号来自“内部”,而在于您的主脚本仍在函数范围内运行时接收到该信号。从“外部”发送信号是不够的,否则一个简单的子 shell(kill -SIGUSR1 $$)就足够了。您还需要以某种方式发送它,以使主脚本有机会从函数返回并输入您希望在其中运行的sourceVariables任何其他范围。trap假设是主作用域,如果您想让变量成为“全局”变量而不明确标记它们。

对于您的示例代码,我会说:只需sourceVariables 在后台运行该函数即可。这样陷阱肯定会在主范围内运行。

例如,以下代码按照(我认为)您的意图执行:

#!/bin/bash

set -x  # <-- set debugging to see what happens

setTraps () {
  trap 'echo trap called in scope ${FUNCNAME[@]}; source ./variables' SIGUSR1
}

sourceVariables () {
  kill -SIGUSR1 $$
}

setTraps
sourceVariables &
while ! [ $say ] ; do :; done  # <-- careful: cpu-intensive loop, only for demonstration
echo I want you to $say \"hello\" $times times.
printf "$say\n%.0s" $(seq 1 $times)

但我想你的下一个要求将是你的实际的 sourceVariables 函数(不是您到目前为止向我们展示的函数)不得全部在后台运行。

如果是这种情况,您还需要脚本kill 和脚本之间的一些同步机制,以确保一切都在正确的时刻发生。可以有多种方法,最好的方法可能取决于您的实际应用。

一个使用coproc内置函数的简单方法,需要 Bash v4+:

#!/bin/bash

set -x  # <-- set debugging to see what happens

setTraps () {
  trap 'echo trap called in scope ${FUNCNAME[@]}; source ./variables' USR1
}

sourceVariables () {
  # Do interesting stuff

  coproc { read ; kill -USR1 $$ ; }  # run a coprocess in background
  # the coprocess starts by waiting on `read`, which serves as a "go-ahead notification"
  # from the script when this latter is ready to receive the signal

  # Do yet more interesting stuff
}

setTraps
sourceVariables

# do even more stuff not yet ready for USR1

echo >&${COPROC[1]}  # notify the coprocess that we're now ready to receive the signal
while ! [ $say ] ; do :; done  # <-- careful: cpu-intensive loop, only for demonstration
echo I want you to $say \"hello\" $times times.
printf "$say\n%.0s" $(seq 1 $times)

答案3

#!/bin/bash

setTraps () {
    trap 'echo trap called in scope ${FUNCNAME[@]}; say=hello' USR1
}

sendSignal () {
    kill -s USR1 "$$"
}

setTraps
sendSignal
printf 'I want you to %s "hello"\n' "$say"

如果您根本不使用declare,陷阱会将say变量设置为全局范围内的字符串。陷阱依旧被称为虽然在范围之内sendSignal,但我认为没有办法改变这一点。

相关内容