2

I want to execute a trap command in "global" scope, but the signal will come from within function. Of course it is possible to declare the variable globally beforehand or use the -g declare option. But in cases where I want to source on trap that's not very practicable as shown below:

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

and the actual script:

#!/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)

But without declare -g NAME, thank you.

EDIT:

Is it possible to detach the kill -s USR1 $$ part completely from the current process, so that the signal comes from 'outside'?

I tried nohup and disown without success so far.

PS: @LL2 offered a solution using background process or coproc here: https://unix.stackexchange.com/a/518720/154557 But I am afraid the time sync issues pop up and make the code disproportionately complicated.

PPS: Actually @LL2 solution even works great with IPC, so I consider the question solved. A downside is that function arguments HAVE TO be processed within the sourced file if needed for further use in main scope, because of course symbols in the subshell are lost. Have a look into EDIT 2 for another solution coping this issue.

#!/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[@]}"

EDIT 2 (Proposal 3 Work in Progress):

Has anyone enough experience with bash custom builtins to point into a direction where a true solution can be achieved?

PS: Bash Loadable Builtins are super powerful and can most prabably be used to receive dbus method calls in main scope.

SOLUTION II (without bg nor coproc):

As it seems the signal always is triggered inside the scope of current execution line. Most probably there is another more sophisticated solution using custom bash builtins (loadable builtins which are pretty powerfull). I can even think of bash-dbus-builtins for IPC. Anyways, this solution using aliases can be usefull if one wants to tell self/another PID sleeping in the background to source some script on signal, without having to set the -g global flag in the sourced script ./variables, so the ./variables script can also be used exclusively within function scope also if necessary. As aliases cannot be exported to subshells, one would have to include the alias into an environment script referenced by BASH_ENV variable for example. Here I extended the original example with an IPC mechanism to make the objective of the use case clearer.

The variable store now also receives arguments

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

First I prepare an environment script

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

and ensure to make the alias available in every bash script.

$> export BASH_ENV=./env

furthermore I need the actual IPC send- and reveive-mechanism using fifo

#!/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

plus ipc receive

#!/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

Now I can transparently trigger the source-signal across multiple processes

#!/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

From 'outside' the process namespace the signal can be triggered as follows. I assume that the Alias was already source by BASH_ENV reference.

$> ./trapsource & TSPID=$!; sleep 1; source_ipc $TSPID ./variables arg1 arg2; sleep 2; kill $TSPID
2
trap 'echo trap called in scope ${FUNCNAME[@]}; declare say=hello; declare -ri times=3' SIGUSR1

Instead of using declare -i within the trap, do that beforehand, and just assign the new value in the trap:

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

I think you could even use readonly times within the trap, since readonly in itself doesn't make the variable local.

E.g. this prints 1, 3, 3 and then an error modifying a readonly variable.

#!/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

Then again, if it's the global variable you want to modify, why not use declare -g?

Is it possible to detach the kill -s USR1 $$ part completely from the current process, so that the signal comes from 'outside'?

I don't think there's a difference between a signal sent by (the builtin) kill from the script itself, and one sent by another process. As you saw, Bash seems to run the trap code in the context of the running function.

3
  • you have a good point with declaring the variable beforehand. but there are cases when thats not practicable. For example sourcing a new file on trap. I'll clarify my question of course.
    – domson
    Feb 17 '19 at 17:42
  • @DominikKummer, I'm not really sure I see the utility of that, and I can't really see why you wouldn't use declare -g, since that appears to do what you want. But then I suppose one possibility would be to just set a flag in the trap, and then do the actual processing in the main level of the script.
    – ilkkachu
    Feb 17 '19 at 17:59
  • @ikkachu I wrote a custom Source function which does some additional lookups, caches and finally sources the files. But within the sourced files I have to declare all variables global with -g option. which works of course, you are right. It is actually a syntactical detail to be forced to declare a variable as global which is meant to be global in the first place. I hoped that I can work around with trapping the source mechanism to make the sourced variables global without -g option.
    – domson
    Feb 17 '19 at 18:17
1

Is it possible to detach the kill -s USR1 $$ part completely from the current process, so that the signal comes from 'outside'?

The problem is not that your signal comes from "inside", it is rather that your main script receives that signal while it is still running within the function scope. It is not enough to send signal from "outside", otherwise a simple subshell like (kill -SIGUSR1 $$) would suffice. You also need to send it in a way to give the main script a chance to return from the sourceVariables function and enter whatever other scope you want your trap to be run within. Supposedly the main scope, if you want to make your variables "global" without marking them explicitly so.

For your sample code, I'd say: just run the sourceVariables function in background. That way the trap will certainly run in main scope.

For instance, the following code does as (I think) you intend:

#!/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)

But I guess your next piece of requirement will be that your actual sourceVariables function (not the one you showed to us so far) must not be run all in background.

If that's the case, you also need some synchronization mechanism between the kill and the script to make sure everything happens at the right moment. There can be various ways and the best one may depend on your actual application.

A simple one using the coproc builtin, which requires 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)
2
  • OMG time flies, and I have not recognized your elegant approach. Nevertheless my main objective originally was to remotely invoke a sourcing mechanism into main scope. But also to be able to use various declare commands in variable source without having to exlicitely set them gobal. You solved everything elegantly by running the function in background or as coprocess. But I am afraid that this solution can get pretty complicated with time sync. Of course my requirement to use a function is due to having the possibility to pass arguments and extend its functionality.
    – domson
    May 28 at 13:11
  • I offer a first solution sourcing code snippets and pass arguments to them, to have a clean interface using aliases but stay within main scope without background or coproc. I'll have to test its practicability. If it fails, your solution will be accepted.
    – domson
    May 28 at 13:16
1
#!/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"

If you don't use declare at all, the trap will set the say variable to the string in global scope. The trap is still called within the scope of sendSignal scope though, but I don't think there's a way of changing that.

2
  • even if the best answer is the simplest, I have to admit that I also would like to make use of $ say=hello; declare -i times=3; within the trap.
    – domson
    Feb 16 '19 at 12:59
  • regarding impossibility, that unfortunately forces me into bash source code, because it would be pointless. Is it possible to detach the kill -s USR1 "$$" completely from the process, so that the signal would come from "outside"?
    – domson
    Feb 16 '19 at 13:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.