Jack Dinkel Jack Dinkel - 5 months ago 8
Linux Question

Supporting both FD-number and filename parameterized redirections

I am writing a shell script that redirects the

STDOUT
and
STDERR
output of a program. The destination of the redirect is a variable. Usually, the destination is a file in which case the following works fine.

simple_redirect()
{
myfunc=$1
myfile=$2

myfunc &>myfile
}
simple_redirect ls text.txt


However, sometimes I don't want to redirect to a file, but want to print to
STDOUT
like normal.
If
myfile
is
&1
for
STDOUT
, I get the following error:
$myfile: ambiguous redirect


If I run the command
ls &>&1
, I see:
parse error near '&'
.

I have thought of a workaround by redirecting
STDOUT
and
STDERR
separately:
ls 1>&1 2>&1
. However, I like this option less because I have to reuse the variable and it is less readable. Is there a simpler way to do this?

Answer

If you're using bash 4.1 or newer, the following will work:

redirect_to() {
  local myfile=$1; shift
  case $myfile in
    -)    "$@" ;;
    "&"*) "$@" >&${myfile#"&"} 2>&1 ;;
    *)    "$@" >"$myfile"      2>&1 ;;
  esac
done

redirect_to -           ls -l foo.d/ # no redirection
redirect_to &2          ls -l foo.d/ # redirection to FD 2
redirect_to foo.list    ls -l foo.d/ # redirection to foo.list
redirect_to /dev/stdout ls -l foo.d/ # consider doing this anyhow

>&$foo is a bash 4.1 extension which allows a variable to be used to provide a file descriptor number -- prior to this, you'd need to use >/dev/fd/${foo#"&"} instead (if your operating system provides them; given the tag on the question, this should be a generally safe assumption).


If you don't have bash 4.1 or newer (and particularly if you're targeting POSIX sh rather than bash altogether), you can't implement the >&$foo option without eval -- a primitive which has substantial security caveats -- and are better off using /dev/stdout, /dev/stderr, /dev/fd/1, etc.

To do so safely with older versions of bash might look something like this, using printf %q to sanitize literal arguments and %d to ensure that the file descriptor can only be numeric:

"&"*) printf -v cmd_str '%q ' "$@"
      printf -v redir_suffix '>&%d' "${myfile#"&"}"
      eval "$cmd_str $redir_suffix 2>&1" ;;
Comments