1

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.

Cognitive Hazard
  • 1,072
  • 10
  • 25
  • Just a quick thought: have you tried printing a Here Document that contains $cmd? – Nitzan Shaked Jan 16 '13 at 03:45
  • Nitzan, assuming I interpretted your suggestion the way you meant: this won't work, because by the time run() sees $cmd, the redirection part has already happened, and been stripped out of $cmd (this per Gordon's explanation, below.) – Cognitive Hazard Jan 16 '13 at 17:55

2 Answers2

2

run which bash >quiet

This silences output from STDOUT, but not STDERR

So you could try this

echo "### RUN: '$cmd'" >/dev/stderr

or simply

echo "### RUN: '$cmd'" >&2

ref

Community
  • 1
  • 1
Zombo
  • 1
  • 62
  • 391
  • 407
2

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 was run touch "foo bar" or run 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:

run ()
{
  if [ -z "$TESTMODE" ]; then
    "$@"
  else
    echo "### RUN: '$*'" >&2
  fi
}

...but this has a few issues: it still executes the redirect rather than printing it (run which bash >quiet will 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 echo command, 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:

echo "### RUN:" >&2
printf " %q" "$@" >&2
echo >&2

printf's %q format will use an unambiguous (and shell-executable) format for the command and its arguments.

Gordon Davisson
  • 118,432
  • 16
  • 123
  • 151
  • Very insightful, thank you. With regards to redirection, I keep forgetting to think of every statement in a bash script as though it was being typed directly into an interactive bash shell. For my use-case, I don't particularly care if I lose the breaks between the arguments; either I will be echoing the full $cmd, or having bash evaluate it--in full--as a complete command. In either case, as far as my script is concerned, I think it might as well be a single string of text, because I won't be parsing it. I am curious about your choice of $* in the else block, vs the $@ in the if clause? – Cognitive Hazard Jan 16 '13 at 17:50
  • Also, can you think of any disadvantages to the >/dev/tty variant? Would it always work as expected? – Cognitive Hazard Jan 16 '13 at 18:00
  • Also: I suddenly understand the import of your point about cmd="$@" now. – Cognitive Hazard Jan 16 '13 at 18:02
  • 1
    @RyanV.Bissell: I've edited in more info to cover your questions. – Gordon Davisson Jan 16 '13 at 23:16