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?

mesr
  • 399
  • 1
  • 12
  • 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 '22 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 https://dev.to/philgibbs/avoiding-temporary-files-in-shell-scripts). – mesr Nov 17 '22 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. – Kamil Maciorowski Nov 17 '22 at 16:33
  • Ideally, this should also be a generic script-wide solution where I don't need to fiddle with individual commands (openssl here is only a simplified example). – mesr Nov 17 '22 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 '22 at 16:44
  • Thanks for the information @kamil-maciorowski, this points me in the right direction. – mesr Nov 17 '22 at 16:47
  • And the trap is triggered after the error stream occur – nezabudka Nov 17 '22 at 16:49
  • Idem @nezabudka – mesr Nov 17 '22 at 16:50
  • What do you want happening to the standard output stream? Should it also be silent unless the utility exists with a non-zero exit status? – Kusalananda Nov 17 '22 at 16:51
  • 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 '22 at 16:53
  • Ok, that means you can't use chronic from the moreutils package. Also, please add clarifications to the text of the question. Don't just leave them in comments. – Kusalananda Nov 17 '22 at 16:53

2 Answers2

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

  • 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 '23 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 '23 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.

mesr
  • 399
  • 1
  • 12