Bash – Is it safe to eval $BASH_COMMAND

bash

I am working on a shell script that constructs a complex command from variables, e.g. like this (with a technique that I've learned from the Bash FAQ):

#!/bin/bash

SOME_ARG="abc"
ANOTHER_ARG="def"

some_complex_command \
  ${SOME_ARG:+--do-something "$SOME_ARG"} \
  ${ANOTHER_ARG:+--with "$ANOTHER_ARG"}

This script dynamically adds the parameters --do-something "$SOME_ARG" and --with "$ANOTHER_ARG" to some_complex_command if these variables are defined. So far this is working fine.

But now I also want to be able to print or log the command when I'm running it, for example when my script is run in a debug mode. So when my script runs some_complex_command --do-something abc --with def, I also want to have this command inside a variable so I can e.g. log it to the syslog.

The Bash FAQ demonstrates a technique to use the DEBUG trap and the $BASH_COMMAND variable (for example for debugging purposes) for this purpose. I've tried that with the following code:

#!/bin/bash

ARG="test string"

trap 'COMMAND="$BASH_COMMAND"; trap - DEBUG' DEBUG
echo "$ARG"

echo "Command was: ${COMMAND}"

This works, but it doesn't expand the variables in the command:

host ~ # ./test.sh
test string
Command was: echo "$ARG"

I guess I have to use eval to expand echo "$ARG" to echo test string (at least I haven't found a way without eval yet). The following does work:

eval echo "Command was: ${COMMAND}"

It produces the following output:

host ~ # ./test.sh
test string
Command was: echo "$ARG"
Command was: echo test string

But I'm not really certain if I can use eval safely like this. I've unsuccessfully tried to exploit some things:

#!/bin/bash

ARG="test string; touch /x"
DANGER='$(touch /y; cat /etc/shadow)'

trap 'COMMAND="$BASH_COMMAND"; trap - DEBUG' DEBUG
echo "$ARG" $DANGER

echo "Command was: ${COMMAND}"
eval echo "Command was: ${COMMAND}"

It seems to handle this well, but I'm curious if someone else sees an issue that I've missed.

Best Answer

One possibility is to make a wrapper function that will at the same time print the command and execute it, as follows:

debug() {
    # This function prints (to stdout) its arguments and executes them
    local args=() idx=0 IFS=' ' c
    for c; do printf -v args[idx++] '%q' "$c"; done
    printf "%s\n" "${args[*]}"
    # Execute!
    "$@"
}

So that in your script you can do:

debug echo "$ARG"

No need to fiddle with the trap. The drawback is that it adds some debug keywords all over your code (but that should be fine, it's common to have such stuff, like asserts, etc.).

You can even add a global variable DEBUG and modify the debug function like so:

debug() {
    # This function prints (to stdout) its arguments if DEBUG is non-null
    # and executes them
    if [[ $DEBUG ]]; then
        local args=() idx=0 IFS=' ' c
        for c; do printf -v args[idx++] '%q' "$c"; done
        printf "%s\n" "${args[*]}"
    fi
    # Execute!
    "$@"
}

Then you can call your script as:

$ DEBUG=yes ./myscript

or

$ DEBUG= ./myscript

or just

$ ./myscript

depending whether you want to have the debug info or not.

I capitalized the DEBUG variable because it should be treated as an environment variable. DEBUG is a trivial and common name, so this might clash with other commands. Maybe call it GNIOURF_DEBUG or MARTIN_VON_WITTICH_DEBUG or UNICORN_DEBUG if you like unicorns (and then you probably like ponies too).

Note. In the debug function, I carefully formatted each argument with printf '%q' so that the output will be correctly escaped and quoted so as to be reusable verbatim with a direct copy and paste. It will also show you exactly what the shell saw as you'll be able to figure out each argument (in case of spaces or other funny symbols). This function also uses direct assignment with the -v switch of printf so as to avoid unnecessary subshells.

Related Question