Evan Benn
Evan Benn

Reputation: 1699

Bash ERR trap discards value of args of the failing command

A bash ERR trap can be used to print a stack trace of functions leading to the ERR. However the actual command that failed does not seem to store its ARGS in BASH_ARGV or FUNCNAME. Instead the name of the trap is there.

BASH_COMMAND contains the name and args of the 'currently executing command', not including the trap. So it seems like it would work, however it contains unexpanded variables.

Is there something like BASH_COMMAND with expanded variables, or a BASH_ARGV with the 'currently executing command'?

trace() {
 echo '${FUNCNAME[@]}' ${FUNCNAME[@]}
 echo '${BASH_ARGV[@]}' "${BASH_ARGV[@]}"
 echo '$BASH_COMMAND' "$BASH_COMMAND"
}
trap trace ERR
set -Ee
shopt -s extdebug

f1() {
 false these args are not especially this: $my_arg
}
my_arg=abc
f1 these args are in the trace $my_arg

output:

${FUNCNAME[@]} trace f1 main
${BASH_ARGV[@]} abc trace the in are args these
$BASH_COMMAND false these args are not especially this: $my_arg

In the example line 2 displays the expanded 'abc' of the f1 function call, but not the 'false' call. Line 3 displays the args of 'false', but not the expanded my_arg.

Upvotes: 1

Views: 157

Answers (2)

dimo414
dimo414

Reputation: 48874

Is there something like BASH_COMMAND with expanded variables, or a BASH_ARGV with the 'currently executing command'?

I believe the short answer is no, unfortunately. But depending on your use case there are some options.

The easiest approach would be to use set -x to log important invocations. This obviously only works if you know in advance which calls you're going to care about, but similarly logs the commands as executed. You can use a subshell, i.e. ( set -x; my_command ... ) to log just the command in question.

If you only want to log on ERR, as you're doing here, you can at least inspect BASH_ARGV/BASH_ARGC (with extdebug) to see the functions and other subroutines on the stack. This doesn't support arbitrary commands, but as it contains the parameters as passed to each stack frame all arguments are expanded.

This suggests a potential workaround: make the command you're interested in a function! adding false() { command false "$@"; } to your script should give you the information you need. Of course this is a little tedious, so we can make a general-purpose wrapper for any commands we're interested in, as long as we don't mind a little eval-ing :)

function_wrapper() {
  for cmd in "$@"; do
    eval "${cmd}() { command ${cmd} \"$@\"; }"
  done
}

function_wrapper true false ...

We could decorate all shell builtins like this if we wanted:

while read -r _ cmd; do
  eval "${cmd}() { command ${cmd} \"$@\"; }"
done < <(enable -a)

Whether that's a good idea or not I'll leave up to you :)

Upvotes: 1

John Kugelman
John Kugelman

Reputation: 362107

  • I changed trace() to see if there are any variables that hold the expanded false command.

    trace() {
      declare -p | grep especially
    }
    

    Output:

    
    

    No dice.

  • Changing it to look for abc tells us if anything holds the expanded f1 command:

    trace() {
      declare -p | grep abc
    }
    

    Output:

    declare -a BASH_ARGV=([0]="abc" [1]="trace" [2]="the" [3]="in" [4]="are" [5]="args" [6]="these")
    declare -- _="abc"
    declare -- my_arg="abc"
    

    Just BASH_ARGV. That's it.

  • I thought DEBUG traps might have more information than ERR ones, but changing trap trace ERR to trap trace DEBUG produced the same results.

Upvotes: 1

Related Questions