How to store standard error in a variable

后端 未结 18 2142
难免孤独
难免孤独 2020-11-22 12:46

Let\'s say I have a script like the following:

useless.sh

echo \"This Is Error\" 1>&2
echo \"This Is Output\" 

And I have an

相关标签:
18条回答
  • 2020-11-22 13:18

    There are a lot of duplicates for this question, many of which have a slightly simpler usage scenario where you don't want to capture stderr and stdout and the exit code all at the same time.

    if result=$(useless.sh 2>&1); then
        stdout=$result
    else
        rc=$?
        stderr=$result
    fi
    

    works for the common scenario where you expect either proper output in the case of success, or a diagnostic message on stderr in the case of failure.

    Note that the shell's control statements already examine $? under the hood; so anything which looks like

    cmd
    if [ $? -eq 0 ], then ...
    

    is just a clumsy, unidiomatic way of saying

    if cmd; then ...
    
    0 讨论(0)
  • 2020-11-22 13:21

    For the benefit of the reader, this recipe here

    • can be re-used as oneliner to catch stderr into a variable
    • still gives access to the return code of the command
    • Sacrifices a temporary file descriptor 3 (which can be changed by you of course)
    • And does not expose this temporary file descriptors to the inner command

    If you want to catch stderr of some command into var you can do

    { var="$( { command; } 2>&1 1>&3 3>&- )"; } 3>&1;
    

    Afterwards you have it all:

    echo "command gives $? and stderr '$var'";
    

    If command is simple (not something like a | b) you can leave the inner {} away:

    { var="$(command 2>&1 1>&3 3>&-)"; } 3>&1;
    

    Wrapped into an easy reusable bash-function (probably needs version 3 and above for local -n):

    : catch-stderr var cmd [args..]
    catch-stderr() { local -n v="$1"; shift && { v="$("$@" 2>&1 1>&3 3>&-)"; } 3>&1; }
    

    Explained:

    • local -n aliases "$1" (which is the variable for catch-stderr)
    • 3>&1 uses file descriptor 3 to save there stdout points
    • { command; } (or "$@") then executes the command within the output capturing $(..)
    • Please note that the exact order is important here (doing it the wrong way shuffles the file descriptors wrongly):
      • 2>&1 redirects stderr to the output capturing $(..)
      • 1>&3 redirects stdout away from the output capturing $(..) back to the "outer" stdout which was saved in file descriptor 3. Note that stderr still refers to where FD 1 pointed before: To the output capturing $(..)
      • 3>&- then closes the file descriptor 3 as it is no more needed, such that command does not suddenly has some unknown open file descriptor showing up. Note that the outer shell still has FD 3 open, but command will not see it.
      • The latter is important, because some programs like lvm complain about unexpected file descriptors. And lvm complains to stderr - just what we are going to capture!

    You can catch any other file descriptor with this recipe, if you adapt accordingly. Except file descriptor 1 of course (here the redirection logic would be wrong, but for file descriptor 1 you can just use var=$(command) as usual).

    Note that this sacrifices file descriptor 3. If you happen to need that file descriptor, feel free to change the number. But be aware, that some shells (from the 1980s) might understand 99>&1 as argument 9 followed by 9>&1 (this is no problem for bash).

    Also note that it is not particluar easy to make this FD 3 configurable through a variable. This makes things very unreadable:

    : catch-var-from-fd-by-fd variable fd-to-catch fd-to-sacrifice command [args..]
    catch-var-from-fd-by-fd()
    {
    local -n v="$1";
    local fd1="$2" fd2="$3";
    shift 3 || return;
    
    eval exec "$fd2>&1";
    v="$(eval '"$@"' "$fd1>&1" "1>&$fd2" "$fd2>&-")";
    eval exec "$fd2>&-";
    }
    

    Security note: The first 3 arguments to catch-var-from-fd-by-fd must not be taken from a 3rd party. Always give them explicitly in a "static" fashion.

    So no-no-no catch-var-from-fd-by-fd $var $fda $fdb $command, never do this!

    If you happen to pass in a variable variable name, at least do it as follows: local -n var="$var"; catch-var-from-fd-by-fd var 3 5 $command

    This still will not protect you against every exploit, but at least helps to detect and avoid common scripting errors.

    Notes:

    • catch-var-from-fd-by-fd var 2 3 cmd.. is the same as catch-stderr var cmd..
    • shift || return is just some way to prevent ugly errors in case you forget to give the correct number of arguments. Perhaps terminating the shell would be another way (but this makes it hard to test from commandline).
    • The routine was written such, that it is more easy to understand. One can rewrite the function such that it does not need exec, but then it gets really ugly.
    • This routine can be rewritten for non-bash as well such that there is no need for local -n. However then you cannot use local variables and it gets extremely ugly!
    • Also note that the evals are used in a safe fashion. Usually eval is considerered dangerous. However in this case it is no more evil than using "$@" (to execute arbitrary commands). However please be sure to use the exact and correct quoting as shown here (else it becomes very very dangerous).
    0 讨论(0)
  • 2020-11-22 13:22

    Improving on YellowApple's answer:

    This is a Bash function to capture stderr into any variable

    stderr_capture_example.sh:

    #!/usr/bin/env bash
    
    # Capture stderr from a command to a variable while maintaining stdout
    # @Args:
    # $1: The variable name to store the stderr output
    # $2: Vararg command and arguments
    # @Return:
    # The Command's Returnn-Code or 2 if missing arguments
    function capture_stderr {
      [ $# -lt 2 ] && return 2
      local stderr="$1"
      shift
      {
        printf -v "$stderr" '%s' "$({ "$@" 1>&3; } 2>&1)"
      } 3>&1
    }
    
    # Testing with a call to erroring ls
    LANG=C capture_stderr my_stderr ls "$0" ''
    
    printf '\nmy_stderr contains:\n%s' "$my_stderr"
    

    Testing:

    bash stderr_capture_example.sh
    

    Output:

     stderr_capture_example.sh
    
    my_stderr contains:
    ls: cannot access '': No such file or directory
    

    This function can be used to capture the returned choice of a dialog command.

    0 讨论(0)
  • 2020-11-22 13:23

    Iterating a bit on Tom Hale's answer, I've found it possible to wrap the redirection yoga into a function for easier reuse. For example:

    #!/bin/sh
    
    capture () {
        { captured=$( { { "$@" ; } 1>&3 ; } 2>&1); } 3>&1
    }
    
    # Example usage; capturing dialog's output without resorting to temp files
    # was what motivated me to search for this particular SO question
    capture dialog --menu "Pick one!" 0 0 0 \
            "FOO" "Foo" \
            "BAR" "Bar" \
            "BAZ" "Baz"
    choice=$captured
    
    clear; echo $choice
    

    It's almost certainly possible to simplify this further. Haven't tested especially-thoroughly, but it does appear to work with both bash and ksh.


    EDIT: an alternative version of the capture function which stores the captured STDERR output into a user-specified variable (instead of relying on a global $captured), taking inspiration from Léa Gris's answer while preserving the ksh (and zsh) compatibility of the above implementation:

    capture () {
        if [ "$#" -lt 2 ]; then
            echo "Usage: capture varname command [arg ...]"
            return 1
        fi
        typeset var captured; captured="$1"; shift
        { read $captured <<<$( { { "$@" ; } 1>&3 ; } 2>&1); } 3>&1
    }
    

    And usage:

    capture choice dialog --menu "Pick one!" 0 0 0 \
            "FOO" "Foo" \
            "BAR" "Bar" \
            "BAZ" "Baz"
    
    clear; echo $choice
    
    0 讨论(0)
  • 2020-11-22 13:23

    For error proofing your commands:

    execute [INVOKING-FUNCTION] [COMMAND]
    

    execute () {
        function="${1}"
        command="${2}"
        error=$(eval "${command}" 2>&1 >"/dev/null")
    
        if [ ${?} -ne 0 ]; then
            echo "${function}: ${error}"
            exit 1
        fi
    }
    

    Inspired in Lean manufacturing:

    • Make errors impossible by design
    • Make steps the smallest
    • Finish items one by one
    • Make it obvious to anyone
    0 讨论(0)
  • 2020-11-22 13:25

    Redirected stderr to stdout, stdout to /dev/null, and then use the backticks or $() to capture the redirected stderr:

    ERROR=$(./useless.sh 2>&1 >/dev/null)
    
    0 讨论(0)
提交回复
热议问题