将命令输出保存在变量中并检查退出状态

25

以下命令将解析谷歌的IP地址

> ip=`dig +short google.com`
> echo $ip
> 216.58.210.238
有时候(尤其是在网络连接中断时),这个命令会因为这个错误而失败。
> ;; connection timed out; no servers could be reached

当命令失败时,我使用 $# 作为赋值操作的输出为0。

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

如何将命令的输出保存到变量中,并检查命令是否成功?

5个回答

36

你可以避免使用$?,只需要:

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

例子:

以下函数将打印“fail”并返回1。

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

因此,如果我们按照以下步骤执行:

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

我们将不会得到任何输出,但是会将print_and_fail函数的输出存储在变量$foo中 - 在本例中为"fail"。

但是,请看下面的函数,它将打印"success"并返回0。

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

现在让我们看看会发生什么:

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

12

你应该使用$?而不是$#

  • $? 包含上一个脚本的返回值。
  • $# 包含传递给脚本或函数的参数总数。

请参考以下示例:

ip=$(dig +short google.com)
if [ $? -eq 0 ]; then
  echo "Success" # Do something here
else
  echo "Fail" # Fallback mode
fi

7

我建议尽可能避免使用$?,因为它很容易在重构时被忽视而导致代码脆弱。如果有人在命令和$?之间添加一行,脚本将以不明显的方式中断。

如果您想同时将输出分配给变量并检查退出代码,可以在条件块中同时执行这两个操作:

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

echo "$foo"

我一直在寻找的非常整洁的解决方案。 - arudzinska
太棒了!正是我找的来避免使用 $? 的东西!如果我没错的话,我可以把里面的 true 移除,并且利用调用命令的退出代码,对吗?毕竟,在这种情况下,如果命令失败,后面的 true 会屏蔽错误并返回退出码 0,使得 if 总是走积极路径,对吗? - João Ciocca
1
@JoãoCiocca 我只是将 true 作为一个占位符,用于表示一个以 true 退出的命令 - 你绝对不需要它。 - Swiss

1
您可以检查返回值,或使用“命令替换”并检查结果变量。例如:

您可以检查返回值,或使用“命令替换”并检查结果变量。例如:

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

(您可以使用-z反向检查失败)


3
我们的踩贴者是谁?他缺乏诚信,无法留下评论说明原因? - David C. Rankin
1
这个答案没有检查dig的退出状态,而问题明确提到了这一点。 - Slaiyer
@Slaiyer +short(“简洁”)选项仅在成功时返回值。dig退出状态的检查由[ -n "$ip" ]提供。如果失败,dig不会返回任何内容,并被条件捕获。 - David C. Rankin
1
虽然在这种特定情况下这样做可行,但退出代码是一种带外报告机制,其使用习惯是有很好的理由的,其中主要原因是实现的统一性(非零表示失败),特定性(错误代码映射到具体错误)以及广泛采用。在错误发生时,给定的CLI工具更有可能已经将某些东西写入了stdout,而不是返回意外的代码。 - Slaiyer
我理解这一点,如果直接检查"$?"在这里是命令替换的结果,而不是通过检查分配给命令替换结果的变量来检查返回值,那么这将是有意义的。我注意到其他答案也采用了这种方法。 - David C. Rankin
同意,语法也不是我最喜欢的。 - Slaiyer

1

由于您正在使用Bash,您可以使用类似以下脚本的东西,它可以捕获stdout、stderr和返回代码https://gist.github.com/jmmitchell/c4369acb8e9ea1f984541f8819c4c87b

为了方便参考,我在这里复制了脚本:

# #!/bin/bash
# 
# based on posts from stackexchange:
# https://dev59.com/SGYr5IYBdhLWcg3wRoSW#26827443
# https://dev59.com/NGgu5IYBdhLWcg3w2alm#18086548
# https://dev59.com/NGgu5IYBdhLWcg3w2alm#28796214

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

4
如果有人要对我的回答进行负面评价,我希望能够了解我在帖子中做错或没有帮助的地方。请指出我的错误或不足之处,感激不尽。 - John Mark Mitchell
这看起来不容易使用,但我认为主要是由于将代码格式化为一个片段造成的。(虽然我没有投反对票) - Lenormju

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接