Search code examples
bashpipestdoutstderr

Bash - getting return code, stdout and stderr form piped invocations


I made a simple logger which has method logMETHOD. It's job is to

  1. Put stderr and stdout to a variable log (and later to my global _LOG variable)
  2. Print stderr of invoked method on stderr and stdout on stdout so I can see it in a console.
  3. Return the return code of a invoked function.

It's invocation looks like this:

logMETHOD myMethod arg1 arg2 arg3

I figured out how to put standard and error output to both log variable and a console but I cannot get the right return code.

My code so far:

function logMETHOD {
    exec 5>&1
    local log
    log="$($1 ${@:2} 2>&1 | tee /dev/fd/5)"
    local retVal=$?
    _LOG+=$log$'\n'
    return $retVal
}

Unfortunately the return code I get comes from (probably) assigning a value (or from tee maybe).

BONUS QUESTION: Is there a possibility to achieve my goals without 2>&1 which connects stdout with stderr also for console?

I tested solution with 'PIPESTATUS' but the code is still 0.

function main {
    logMETHOD alwaysError
}

function logMETHOD {
    exec 5>&1
    local log
    local retVal
    log="$( "$@" 2>&1 | tee /dev/fd/5 )"
    retVal=${PIPESTATUS[0]}
    echo "RETVAL: $retVal"
    echo "LOG: $log"
    _LOG+=$log$'\n'
    return $retVal
}

function alwaysError {
    return 1
}

main $@

Solution

  • PIPESTATUS would be a good solution, but here it already holds the return value of the log=... assignment. If you want the return value of the "$@"... you have to write it like this:

    log="$( "$@" 2>&1 | tee /dev/fd/5; echo ${PIPESTATUS[0]}>/tmp/retval )"
    retVal=$(</tmp/retval)
    

    Assigning it to a variable would not work, because its scope would not extend to the calling shell, so you have to resort to using a tempfile.

    As for stderr, $() can only extract stdout, therefore you have to use a tempfile for that too, if you want to handle it separately.

    log="$( "$@" 2>/tmp/stderr | tee /dev/fd/5; echo ${PIPESTATUS[0]}>/tmp/retval )"
    stderr_log=$(</tmp/stderr)
    retVal=$(</tmp/retval)
    

    If you just want to spare the redirection:

    log="$( "$@" |& tee /dev/fd/5; echo ${PIPESTATUS[0]}>/tmp/retval )"
    

    From man bash:

    If |& is used, command's standard error, in addition to its standard output, is connected to command2's standard input through the pipe; it is shorthand for 2>&1 |. This implicit redirection of the standard error to the standard output is performed after any redirections specified by the command.