1

I see that many questions have been asked and answered on SE about redirections in Bash using exec, but none seem to answer my question.

What I'm trying to accomplish is redirect all output to stderr in a script to a temporary fd and restitute it back to stderr only in case of non-successful script termination. The ability to restore the contents of stderr is needed in case of a non-successful termination, for returning to the caller information about the error. Redirecting stderr to /dev/null would discard both irrelevant noise and useful information. As much as possible, explicit temporary files should be avoided, as they add a number of other issues (see https://dev.to/philgibbs/avoiding-temporary-files-in-shell-scripts). The contents of stdout should be returned transparently to the caller. It will be ignored by the caller in case of non-successful termination of the script, but the script shouldn't need to care about that.

The reason for doing this is that the script is called from another program that considers any output to stderr an error, instead of testing for a non-zero exit code. In the example below, openssl echoes a progress indicator to stderr which does not indicate an error and can be ignored upon successful completion of the command.

Below is a simplified version of my script (note that openssl here is only a placeholder for arbitrary and possibly more complex instructions):

#!/bin/bash
set -euo pipefail

# Redirect stderr to temp fd 3
exec 3>&2

# In case of error, copy contents of fd 3 to stderr
trap 'cat <&3 >&2' ERR

# Upon exit, restore original stderr and close fd 3
trap 'exec 2>&3 3>&-' EXIT

# This nonetheless outputs the progress indicator to stderr
openssl genpkey -algorithm RSA

But I'm obviously missing something as the script keeps printing out the content of stderr upon successful termination, but now blocks upon failure. I guess I'm confused with how redirections with exec work. What am I getting wrong?

11
  • Can't you just redirect standard error as usual when invoking the command, either to /dev/null or to a file? Doing either would prevent the script from producing any output on that stream, at least not when running the command that you have redirected.
    – Kusalananda
    Nov 17, 2022 at 16:17
  • Not really. I need the ability to restore the contents of stderr in case of a non-successful termination, for returning to the caller information about the error. Redirecting stderr to /dev/nul would discard both the progress indicator and useful information. As much as possible I'm also trying to avoid explicit temp files, which add a number of other issues (see dev.to/philgibbs/avoiding-temporary-files-in-shell-scripts).
    – mesr
    Nov 17, 2022 at 16:27
  • 2
    (1) exec 3>&2 makes the descriptor number 3 point to the same open file description as 2, but it does not prevent programs from using their inherited 2. (2) "Restore the contents" cannot be done without some kind of buffer (a temporary file may be such buffer) and supporting code. I don't think simple redirections can do this. Nov 17, 2022 at 16:33
  • 1
    One nuance. If you have a universal descriptor 3 open, then in order to read it from the beginning cat <&3 after writing to it, you need to reopen the descriptor 3, then the pointer (seek) will be located at the beginning and not at the end of a stream.
    – nezabudka
    Nov 17, 2022 at 16:44
  • 1
    stdout should be returned transparently to the caller. It will be ignored in case of non-successful termination of the script, but the script doesn't need to care about that.
    – mesr
    Nov 17, 2022 at 16:53

2 Answers 2

2

chronic from moreutils does almost what you want:

chronic cmd

Will discard cmd's stdout and stderr unless cmd fails or is killed, in which case chronic writes the stdout output on stdout and then the stderr output on stderr which it has saved in memory.

chronic is written in perl which can deal with arbitrary data and can read from two streams independently. Shells other than zsh on the other hand cannot store arbitrary output in their variables as they choke or NUL characters. Also, command substitution, even in zsh, strips trailing newline characters.

Here, you can still use a temp file and remove most of the problems associated with it if you delete it from the start (that's what most shells do for their here-documents for instance):

#! /bin/bash -
set -o nounset -o errexit -o pipefail

tmp=$(mktemp)
exec 3>&2 2> "$tmp" 4< "$tmp"
rm -f -- "$tmp"
trap '[ "$?" -eq 0 ] || cat <&4 >&3' EXIT

cmd1
cmd2
2
  • The problem with chronic, as you mentioned, is that it discards stdout. The other issue I see is that openssl here is a placeholder for possibly more complex series of commands, which are then subject to the same limitations. Also, my question states that temp files should be avoided. That being said, the part of your answer about choking on NUL characters and stripping trailing NL are indeed limitations to keep in mind. The approach I've favored where output is binary or must be verbatim is to encode it with either xxd -plain or base64, depending on which one makes sense in context.
    – mesr
    Jan 17 at 17:04
  • That being said, thanks for your relevant input. I'll edit my answer and include the caveats that you pointed out.
    – mesr
    Jan 17 at 17:11
0

Thanks to all in the comments above who've helped pointing me in the right direction. This below seems to be the answer that I was looking for, heavily inspired from this other SE answer:

#!/bin/bash
set -euo pipefail
shopt -s inherit_errexit

trap 'printf "%s\n" "$_ERR" >&2' ERR

{ _ERR=$({

# Arbitrary commands go here
openssl genpkey -algorithm RSA

} 2>&1 >&3 3>&-); } 3>&1

Ideas for further improvements are welcomed.

Thanks to @StéphaneChazelas for his suggestions.

2
  • 1
    You'd also likely want shopt -s inherit_errexit without which your set -e (more legible as set -o errexit) would be defeated. Jan 17 at 17:48
  • 1
    You'd also want printf "%s\n" "$_ERR" instead of echo $_ERR Jan 17 at 17:50

You must log in to answer this question.

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