I have a shell function called “run”, that normally just treats its parameters as a command line (i.e., as though ‘run()’ had not been used.) However, if the script is run in a $TESTMODE, ‘run()’ will echo its parameters, instead:
run ()
{
cmd="$@"
if [ -z "$TESTMODE" ]; then
$cmd
else
echo "### RUN: '$cmd'"
fi
}
The idea is, the command run rm -Rf / would normally attempt to delete your entire root filesystem. But, if $TESTMODE is defined, then it instead echos
### RUN: rm -Rf /
It works pretty well, until you attempt to use run() on a command-line that includes redirection:
run which which
run which bash >quiet
In which case, when $TESTMODE is defined, the desired echo will never be seen by human eyes:
$ rm quiet
$ TESTMODE=yes ./runtest.sh
### RUN: 'which which'
$ cat quiet
### RUN: 'which bash'
I attempted to correct for this with string replacement:
run ()
{
cmd="$@"
if [ -z "$TESTMODE" ]; then
$cmd
else
cmd=${cmd//\>/\\>}
cmd=${cmd//\</\\<}
echo "### RUN: '$cmd'"
fi
}
… which seemed promising in a command-line test:
$ test1="foo >bar"
$ echo $test1
foo >bar
$ test2=${test1//\>/\\>}
$ echo $test2
foo \>bar
… but failed miserably (no apparent change in behavior) from within my bash script.
Help? I’m sure this has something to do with quoting, but I’m not sure exactly how to address it.
It’s not a matter of escaping; the redirection happens before your function even starts, and is outside of the function’s control. For example, when bash sees the command
run which bash >quiet, bash first redirects output to the file “quiet”, then executes the “run” function with the arguments “which” and “bash”. Anything your function sends to stdout will naturally go into the file “quiet”.There’s also another problem with your script. When you store the command in a variable (
cmd="$@"), it looses the breaks between arguments. For example, you can’t tell if the command wasrun touch "foo bar"orrun touch "foo" "bar"— in either case, $cmd is set to “touch foo bar”. This might be ok for printing the command, but since you’re executing it in that form it’s likely to cause trouble. To solve this, either avoid storing it in a variable, or store it as an array (cmd=("$@"); then execute it as"${cmd[@]}").There are a couple of ways around the output redirection problem. You could send output to stderr instead of stdout:
…but this has a few issues: it still executes the redirect rather than printing it (
run which bash >quietwill create an empty file named “quiet”, then print “### RUN: ‘which bash'”).Also, if stderr has been redirected, it will print to that instead of the terminal. This might be either good or bad, depending on your intent. Another possibility would be to send directly to the terminal with
echo "### RUN: '$*'" >/dev/tty. Possible problem here: it’ll fail if there’s no tty (e.g. in a cron job).Final note: in the
echocommand, I used$*instead of$@because I wanted the entire command treated as a single string with spaces instead of a series of strings. This makes the printout ambiguous (e.g.run touch "foo bar"vs.run touch "foo" "bar"— both would print### RUN: 'touch foo bar'), but that isn’t nearly as important as executing the command correctly. If you want unambiguous logging, you have to do something more complex like this:printf’s
%qformat will use an unambiguous (and shell-executable) format for the command and its arguments.