0

using #!/bin/bash under Ubuntu 16.04 I want to an eval'ed command to...

  • deliver direct output
  • get the output to a variable
  • check on the result of the eval
# valid command
foo=$(eval "ls" | tee /dev/tty);
echo ${PIPESTATUS[@]}
echo $foo

# invalid command
foo=$(eval "ls -ßnonsense" | tee /dev/tty);
echo ${PIPESTATUS[@]}
echo $foo

My problem is, that I only get a meaningful PIPESTATUS, when

  • I completely take out the tee /dev/tty (otherwise, yes, it covers up any eval-failure as part of the first pipe command)
  • or put it outside the bracket foo=$(eval "ls" ) | tee /dev/tty; – ...but in that case, I no longer get neither direct output nor $foo output.
1
  • Why the use of eval? when you can run just ls?
    – Inian
    Aug 17 '20 at 10:49
2

Use zsh instead:

shell_code='ls' # or ls -ßnonsense...
{ foo=$(eval " $shell_code" >&1 >&3 3>&-); } 3>&1
print -r status=$? output=$foo

Here, we're duplicating the original stdout (which would only be the controlling tty if that script was run unredirected from a terminal) onto fd 3, and redirect eval's output to both the command substitution and that fd 3 (using zsh's own teeing mechanism when a fd is redirected more than once for output).

With bash and other shells supporting pipefail (though not ksh93 if on Linux/Cygwin) on systems with /dev/fd/<n>, you can do something similar with:

shell_code='ls' # or ls -ßnonsense...
{
  foo=$(
    set -o pipefail
    eval " $shell_code" 3>&- | 
      tee 4>&1 >&3 3>&- /dev/fd/4
  )
} 3>&1

printf 'status=%s output=%s\n' "$?" "$foo"

With pipefail on, the exit status of a pipeline is that of the rightmost command in the pipeline that failed. So here it will be that of eval unless tee fails.

To get the exit status of eval unconditionaly, with bash, you can do:

shell_code='ls' # or ls -ßnonsense...
{
  foo=$(
    eval " $shell_code" 3>&- |
      tee 4>&1 >&3 3>&- /dev/fd/4
    exit "$PIPESTATUS"
  )
} 3>&1

printf 'status=%s output=%s\n' "$?" "$foo"

In those approaches, tee's stdout is the original stdout, while the command substitution pipe is fed by way of /dev/fd/4. We're doing that rather than the other way round (tee's stdout to command substitution pipe, writing to original stdout via some /dev/fd/<n>) even though that makes the code a bit more complicated, to work around the fact that the latter wouldn't work on Linux or Cygwin systems, where opening /dev/fd/<n> is not the same as duplicating fd <n> (but doing it for a fd that points to the writing end of a pipe, like for that command substitution (not ksh93 that uses socketpairs instead) is functionally equivalent)

1

Here is a portable solution that should work with most if not all Bourne syntax shell; tested with bash, dash, ksh93, ksh88, ash, (Bourne) sh and zsh:

for command in "ls -ßnonsense" "ls -ld /tmp"; do
    foo=`{ eval "$command"; echo $? > /tmp/ps.$$; } | tee /dev/tty;`
    st=`cat /tmp/ps.$$;rm /tmp/ps.$$`
    echo "foo=$foo status=$st"
done
2
  • I need eval because the script bit actually come in a variable (I write some kind of execute-and-assert-function)). I need the pipestatus in a variable, non on screen. And $foo needs to get the eval'ed commands stdout), which would not "pipe through" your echo pipestatus“. (I assume you mean to have a closing ) at the very end?) Aug 17 '20 at 13:35
  • There was a typo. Answer modified to avoid bashisms or whatever dependence on specific shells features.
    – jlliagre
    Aug 17 '20 at 20:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.