dimo414
dimo414

Reputation: 48824

Consistently override the DEBUG trap from a sourced script

I have a script that I source in my .bashrc which sets the DEBUG trap. This works fine, normally, but I recently discovered that if the DEBUG trap has previously been set (e.g. earlier in the .bashrc) the sourced trap call doesn't persist - it appears to be re-overwritten by the earlier trap.

Isolated Example:

$ env -i bash --noprofile --norc
bash-4.3$ trap
trap -- '' SIGTSTP
trap -- '' SIGTTIN
trap -- '' SIGTTOU
bash-4.3$ cat /tmp/echotrap.sh 
#!/bin/bash
trap 'echo "DEBUG TRAP TRIGGERED"' DEBUG
bash-4.3$ source /tmp/echotrap.sh 
bash-4.3$ true
DEBUG TRAP TRIGGERED
bash-4.3$ trap
DEBUG TRAP TRIGGERED
trap -- '' SIGTSTP
trap -- '' SIGTTIN
trap -- '' SIGTTOU
trap -- 'echo "DEBUG TRAP TRIGGERED"' DEBUG
bash-4.3$ exit
DEBUG TRAP TRIGGERED
exit

Notice that the DEBUG trap is set by sourcing /tmp/echotrap.sh, as I expected.

However if the DEBUG trap has already been set, this no longer works:

$ env -i bash --noprofile --norc
bash-4.3$ trap
trap -- '' SIGTSTP
trap -- '' SIGTTIN
trap -- '' SIGTTOU
bash-4.3$ trap : DEBUG
bash-4.3$ trap
trap -- '' SIGTSTP
trap -- '' SIGTTIN
trap -- '' SIGTTOU
trap -- ':' DEBUG
bash-4.3$ source /tmp/echotrap.sh 
bash-4.3$ true
bash-4.3$ trap
trap -- '' SIGTSTP
trap -- '' SIGTTIN
trap -- '' SIGTTOU
trap -- ':' DEBUG
bash-4.3$ exit
exit

The initial trap remains.

Is there any workaround for this issue (from within the sourced script)? I'd like to be able to update the DEBUG trap regardless of its current state, not only if it hasn't been set before.

Upvotes: 2

Views: 612

Answers (1)

Zac B
Zac B

Reputation: 4232

From the Bourne Shell Builtins page, in the . section:

If the -T option is enabled, source inherits any trap on DEBUG; if it is not, any DEBUG trap string is saved and restored around the call to source, and source unsets the DEBUG trap while it executes. If -T is not set, and the sourced file changes the DEBUG trap, the new value is retained when source completes.

So, to make the trap stick, in the bash that sources your file (not in the sourced file itself), do set -T before sourceing.

Then your tests should pass.

However, this doesn't meet your stretch goal of being able to ensure that traps get installed within the sourced script, without requiring the invoking user to set an option. Perhaps this is considered to be a security feature.

Regardless, whether or not the sourced file does set -T itself, it only persists for the "scope" of the sourced file. This is true even if the sourced file uses the $BASH_SOURCE variable to re-source itself a second time after setting -T. This implies the presence of some nested scope-like behavior in Bash that I wasn't previously familiar with; if those layered set/not-set behaviors persist to arbitrary depth in sourced files, that could potentially be abused in interesting ways. Regardless, you can at least inform the user sourceing your file that your traps probably won't work, by putting some code like this at the top of the file:

if [[ ! $- =~ T ]]; then
        echo "Can't install traps unless '-T' option is enabled; not loading library"
        return
fi
trap 'echo "DEBUG TRAP TRIGGERED"' DEBUG # Won't run unless -T has been enabled

Another option, if you don't want to place the burden on sourced libraries to check for that sort of thing, is to override source itself with a function which conditionally overrides trap. Since function definitions are available to sourced files, something like this would work:

function source() {
    if [[ ! $- =~ T ]]; then
        function trap() {
            if [[ $@ == *DEBUG ]]; then
                echo "Debug traps won't work in a source/.'d file unless the shell option -T is set before calling source/."
                return 1
            else
                builtin trap $@
            fi
        }
    fi
    builtin source $@
    unset trap
}

The function can't check whether the DEBUG trap is set before deciding to override trap, however; the same behavior that makes DEBUG traps provisionally invisible to sourced code also makes them invisible inside functions. To work around that, you could pack the entire source function into an alias, which does have access to DEBUG traps, at the cost of readability (e.g. alias source='if [[ ! $- =~ T && $(trap -p DEBUG) ]]; then echo SORRY; fi; builtin source').

This override can also be "defeated" by code that uses builtin source or builtin type directly.

Upvotes: 3

Related Questions