12

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.

1
  • This is what I've been using for years in a sudo wrapper, I have never noticed an issue.
    – w00t
    May 23, 2014 at 5:39

2 Answers 2

7

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.

1
  • 1
    The function works great, but unfortunately my command contains redirections and the "excute in background" control operator &. I had to move those to the function to get it to work - not very nice, but I don't think there's a better way. But no more eval, so I've got that going for me, which is nice :) May 23, 2014 at 7:06
5

eval "$BASH_COMMAND" runs the command.

printf '%s\n' "$BASH_COMMAND" prints the exact specified command, plus a newline.

If the command contains variables (i.e. if it's something like cat "$foo"), then printing out the command prints out the variable text. It is impossible to print the variable's value without executing the command — think of commands like variable=$(some_function) other_variable=$variable.

The simplest way to get a trace from executing a shell script is to set the xtrace shell option by running the script as bash -x /path/to/script or invoking set -x inside the shell. The trace is printed to standard error.

2
  • 1
    I know about xtrace, but that doesn't give me very much control. I tried "set -x; ...; set +x", but: 1) the "set +x" command to disable xtrace is printed too 2) I can't prefix the output e.g. with a timestamp 3) I can't log it to syslog. May 23, 2014 at 6:35
  • 3
    "It is impossible to print the variable's value without executing the command" - good example, I hadn't considered such a case. It would be nice though if bash had an additional variable next to BASH_COMMAND that contains the expanded command, because at some point it has to do variable expansion on the command anyway when it's executing it :) May 23, 2014 at 6:38

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .