0

Is there a way to get stdout and stderr into 2 different variables?

Pseudocode:

#!/bin/bash
A=1
B=0

echo -n "Attempting to divide......"

#This will cause a Division by zero error
#Also, the below is pseudocode ONLY. It is not valid bash syntax
RESULT,ERR_MSG=$((A/B))

if [[ $? == 0 ]]
then
    echo "[SUCCESS]"
else
    echo "[FAIL]"
    echo "$ERROR_MSG"
fi
puk
  • 16,318
  • 29
  • 119
  • 199
  • Does this answer your question? [How to store standard error in a variable](https://stackoverflow.com/questions/962255/how-to-store-standard-error-in-a-variable) – racraman Sep 05 '20 at 21:50
  • Are the commands you are executing outputting text to both the standard output and standard error? If not, you could just capture all output to a single variable and switch on the exit code like you have here. – Mitchell P Sep 05 '20 at 22:04
  • Note that `RESULT,ERR_MSG` is an invalid env variable name. If you're hoping to populate 2 variables from one `$((...))` output, it won't work. You might save a few cycles by tmpVar=( $((...)) );res=${tmpVar[1]}; errMsg=${tmpVar[2]}`, but not really clear from your description. Good luck. – shellter Sep 05 '20 at 22:52
  • @racraman it is similar, but doesn't quite answer my question. Thanks though – puk Sep 05 '20 at 23:53
  • @MitchellP, it is a SQL query. I am capturing the output into a variable (`ID=$(echo "SELECT id from table_name"|sudo -u postgres psql -d tablename)`) I want to capture errors to a different variable – puk Sep 05 '20 at 23:54
  • @shellter I understand it is wrong. I will add a comment RE this – puk Sep 05 '20 at 23:54
  • 1
    And of course `ERR_MSG` != `ERROR_MSG` ..... Now that you have clarified your problem (in comments), my experience is to capture err Msgs to a `/tmp` file, inspect the file and then delete it. (that is ugly too!), but not as fraught with issues as errors in env_vars. (IMHO). Good luck. – shellter Sep 06 '20 at 01:23
  • I believe a possible solution is provided here: https://stackoverflow.com/a/59592881/10693476 – LuckyDams Apr 02 '23 at 20:18

1 Answers1

1

It's possible but it's not pretty.

Probably the best solution is to use a temporary file as suggested in a comment by @shellter. That requires a little care to ensure that the temporary file doesn't overwrite anything else and is properly cleaned up, but it should at least be reliable.

However, it is possible to do it in bash, although I can't honestly say that I'm 100% certain that I've covered all possible corner cases [Note 1]. The following function runs a command and records its stdout and stderr into two environment variables whose names are supplied as arguments. It has two modes of operation:

  • If less than three arguments are provided, the first arguments name the variables which will receive stdout and stderr. The arguments default to out and err, respectively. The command must be provided to the function's stdin, and will be run in a separate bash shell.

  • If there are at least three arguments, then the first two name the variables as above, and the remaining arguments are executed as an external command. (This version cannot be used if you want to run a pipeline or a bash function.)

Reading the command from stdin will be the solution if you want to be able to do this with arbitrary bash commands, but a child bash process is not the same as a subshell; you'll need to make sure you export any variables and functions which you need to use in the commands (and unfortunately you cannot export bash arrays). You can supply the input either as a here-string or as a here-doc; for commands which require quoting characters, you'll probably find it simpler to use a quoted here-doc, as shown below.

For most purposes, you will probably want to use the version which runs the specified command, but remember that expansions in the command are executed before the function is called, and so resulting errors will not be captured in the error variable.

Here's the function:

bsplit () { 
    local -a cmd=("${@:3}")
    if ! ((${#cmd})); then cmd=("$SHELL"); fi
    local outerr=$(
        "${cmd[@]}" 2>&1 > >(
             IFS= read -rd '' s
             printf '%s|%d' "$s" ${#s} 1>&2
        )
    )
    local n=${outerr##*|}
    outerr=${outerr%|*}
    declare -n out_=${1:-out} err_=${2:-err}
    if ((n)); then
        out_=${outerr: -$n} err_=${outerr:0:-$n}
    else
        out_="" err_=$outerr
    fi
}

The heart of the function is the invocation of the command:

    local outerr=$(
        "${cmd[@]}" 2>&1 > >(
             IFS= read -rd '' s
             printf '%s|%d' "$s" ${#s} 1>&2
        )
    )

The first redirect (2>&1) causes the stderr output to be sent directly to the command substitution where it will be captured in the environment variable. The second redirect (> >(...)) passes the stdout output to a subshell which reads the entire output and appends its length in bytes before sending that on to stderr. Note that the stderr it sends it to is the one which was redirected earlier, which will go to the command substituion. This dance ensures that the regular output is sent after the error output; the printf does not execute until the original stdout is closed by the termination of the command being run, at which point all of the command's error output will already have been sent.

The rest of the function just splits the concatenated output apart into two pieces, using the length indicator at the end to find the split point.

Some examples:

# Execute bash arithmetic substitutions using a here-string
$ bsplit <<<'echo $((1/1)); echo $((1/0))'
$ echo "$out"
1

$ echo "$err"
/bin/bash: line 1: 1/0: division by 0 (error token is "0")

# Here's a script being executed in a quoted here-doc to avoid quoting issues:
$ bsplit <<"EOS"
> cat <(printf %s "Hello,") <(printf '%s!' " world")
> not a command
> EOS
$ echo "$out"
Hello, world!
$ echo "$err"
/bin/bash: line 2: not: command not found

# And a more typical case where it just runs a command:
$ bsplit out err ls -l a_file a_nonexistent_file
$ echo "$out"
-rw-rw-r-- 1 rici rici 0 Sep  6 13:04 a_file

$ echo "$err"
ls: cannot access 'a_nonexistent_file': No such file or directory


Notes:

  1. If you find a corner case not covered, let me know in a comment. No promises, though :-)
rici
  • 234,347
  • 28
  • 237
  • 341
  • Sainthood for rici ! . One clarification please: Are you saying subshells are different than child processes and DO inherit all env-vars? (no export required). And to have our definitions lined up, a child process is calling a separate program, `ffmpeg` for example? Great answer! I hope the O.P. can use it. Good luck to all! – shellter Sep 10 '20 at 18:43
  • @shellter: The subshell created for command substitution has a copy of all variables. – rici Sep 10 '20 at 19:26