25

The following command will resolve google ip

> ip=`dig +short google.com`
> echo $ip
> 216.58.210.238

Sometimes (especially when internet connection is lost) this command fail with this error

> ;; connection timed out; no servers could be reached

When the command fail and I use $# the output is 0 for the assigment

> ip=`dig +short google.com`
> echo $#
> 0
> echo $ip     # Command failed
> ;; connection timed out; no servers could be reached

How can I save the output of command in variable, and also check if the command succeeded?

oguz ismail
  • 1
  • 16
  • 47
  • 69
Nasr
  • 2,482
  • 4
  • 26
  • 31

5 Answers5

35

You can avoid accessing $?, and simply:

if ip=$(dig +short google.com); then
    # Success.
else
    # Failure.
fi

Example:

The following function will print "fail" and return 1.

print_and_fail() { printf '%s' fail; return 1; }

Thus, if we do the following:

if foo=$(print_and_fail); then printf '%s\n' "$foo";fi

We'll get no output, yet store print_and_fail output to $foo - in this case, "fail".

But, take a look at the following function, which will print "success" and return 0.

print_and_succeed() { printf '%s' success; return 0; }

Let's see what happens now:

$ if foo=$(print_and_succeed); then printf '%s\n' "$foo";fi
$ success
Rany Albeg Wein
  • 3,304
  • 3
  • 16
  • 26
12

You should use $? instead of $#.

  • $? contains the return value from the last script.
  • $# contains the total number of arguments passed to a script or function.

Do something like below :

ip=$(dig +short google.com)
if [ $? -eq 0 ]; then
  echo "Success" # Do something here
else
  echo "Fail" # Fallback mode
fi
oguz ismail
  • 1
  • 16
  • 47
  • 69
sjsam
  • 21,411
  • 5
  • 55
  • 102
5

I recommend against the use of $? as much as possible, as it is fragile and easy to overlook when refactoring. If someone adds a line in between the command and $?, the script will break in non-obvious ways.

If you want to both assign the output to a variable and check the exit code at the same time, you can just do both in your conditional block:

if foo="$(echo "foo"; true)"; then
    echo "$foo"
fi

echo "$foo"
Swiss
  • 5,556
  • 1
  • 28
  • 42
  • Very neat solution I've been looking for. – arudzinska Nov 24 '20 at 13:58
  • awesome! just what I was looking for to avoid using `$?`!! If I'm not mistaken, I can remove the `true` inside, and also make use of the called command's exit code, right? After all, in this case if the command fails, the `true` after it would kill the error and return exit 0 all the same, making the `if` always go for the positive path, correct? – João Ciocca Aug 14 '23 at 22:09
  • 1
    @JoãoCiocca I just put `true` as a placeholder for a command that exits with true - you definitely don't need it. – Swiss Aug 14 '23 at 22:23
1

You can check the return or use command-substitution and check the resulting variable. e.g.

$ ip=$(dig +short google.com)
$ [ -n "$ip" ] && echo "all good, ip = $ip"

(you can do the reverse check for failure with -z

David C. Rankin
  • 81,885
  • 6
  • 58
  • 85
  • 3
    Who is our downvoter who lacks the integrity to leave a comment as to why? – David C. Rankin Apr 13 '18 at 02:55
  • 1
    This answer doesn't check the exit status of `dig`, which the question specifically mentions. – Slaiyer Dec 18 '19 at 14:07
  • @Slaiyer the `+short` ("terse") option only returns a value on success. The check on `dig` exit status is provided by `[ -n "$ip" ]`. On failure nothing is returned by `dig` and is caught by the conditional. – David C. Rankin Dec 18 '19 at 22:24
  • 1
    While that works in this particular case, exit codes are an out of band reporting mechanism whose usage is a convention for good reasons, the prime being uniformity of implementation (non-zero for failure), specificity (error codes mapped to specific errors), and widespread adoption. A given CLI tool is more likely to have already written something to stdout by the time an error occurs, than it is to return an unexpected code. – Slaiyer Dec 19 '19 at 08:29
  • I get that point, and if there were a difference in directly checking `"$?"` here being the subsequent result of the *command substitution* instead of checking the return by checking whether the variable assigned the result of the *command substitution* it would make sense.. I note other answers here also took this approach. – David C. Rankin Dec 19 '19 at 08:34
  • Agreed, the syntax is not my favourite either. – Slaiyer Dec 19 '19 at 08:38
1

Since you are using Bash, you can use something like the following script which can capture stdout, stderr and the return code https://gist.github.com/jmmitchell/c4369acb8e9ea1f984541f8819c4c87b

For easy reference I have copied the script here:

# #!/bin/bash
# 
# based on posts from stackexchange:
# http://stackoverflow.com/a/26827443/171475
# http://stackoverflow.com/a/18086548/171475
# http://stackoverflow.com/a/28796214/171475

function example_function {
    printf 'example output to stdout %d\n' {1..10}
    echo >&2 'example output to stderr'
    return 42
}



##############################
### using the dot operator ###

if [ "${BASH_VERSINFO}" -lt 4 ]; then
    printf '%s\n' "The source version of this script requires Bash v4 or higher."
else

    # stdout & stderr only
    source <({ cmd_err=$({ mapfile -t cmd_out < <(example_function); } 2>&1; declare -p cmd_out >&2); declare -p cmd_err; } 2>&1)

    printf "\n%s\n" "SOURCE VERSION : STDOUT & STDERR ONLY"
    printf "%s\n" "${cmd_out[@]}"
    printf "%s\n" "${cmd_err}"

    unset cmd_out
    unset cmd_err


    # stdout & stderr only as well as return code:
    source <({ cmd_err=$({ mapfile -t cmd_out< <( \
        example_function \
      ; cmd_rtn=$?; declare -p cmd_rtn >&3); } 3>&2 2>&1; declare -p cmd_out >&2); declare -p cmd_err; } 2>&1)


    printf "\n%s\n" "SOURCE VERSION : STDOUT, STDERR & RETURN CODE"
    printf '%s\n' "${cmd_out[@]}"
    # alternative version
    # declare -p cmd_out 
    printf '%s\n' "${cmd_err}"
    printf '%s\n' "${cmd_rtn}"

    unset cmd_out
    unset cmd_err
    unset cmd_rtn

fi

##############################
######### using exec #########

# stdout & stderr only
eval "$({ cmd_err=$({ cmd_out=$( \
    example_function \
  ); } 2>&1; declare -p cmd_out >&2); declare -p cmd_err; } 2>&1)"

printf "\n%s\n" "EVAL VERSION : STDOUT & STDERR ONLY"
printf '%s\n' "${cmd_out}"
printf '%s\n' "${cmd_err}"
printf '%s\n' "${cmd_rtn}"

unset cmd_out
unset cmd_err

# stdout & stderr only as well as return code:
eval "$({ cmd_err=$({ cmd_out=$( \
    example_function \
  ); cmd_rtn=$?; } 2>&1; declare -p cmd_out cmd_rtn >&2); declare -p cmd_err; } 2>&1)"


printf "\n%s\n" "EVAL VERSION : STDOUT, STDERR & RETURN CODE"
printf '%s\n' "${cmd_out}"
printf '%s\n' "${cmd_err}"
printf '%s\n' "${cmd_rtn}"


unset cmd_out
unset cmd_err
unset cmd_rtn
John Mark Mitchell
  • 4,522
  • 4
  • 28
  • 29
  • 4
    If someone is going to down vote my answer, I would appreciate understanding how I was wrong or unhelpful in my post. – John Mark Mitchell May 06 '16 at 04:14
  • It does not look easy to use, but I think it is mainly due to formatting the code as only one snippet. (I did not downvote though) – Lenormju Jul 19 '23 at 09:38