Shell脚本中用于返回多个值的习惯用语

37

有没有用于在bash脚本中从函数返回多个值的习惯用语?

http://tldp.org/LDP/abs/html/assortedtips.html描述了如何回显多个值并处理结果(例如,示例35-17),但是如果某些返回值是带空格的字符串,则会变得棘手。

一种更结构化的返回方式是分配给全局变量,例如:

foo () {
    FOO_RV1="bob"
    FOO_RV2="bill"
}

foo
echo "foo returned ${FOO_RV1} and ${FOO_RV2}"

我意识到如果在shell脚本中需要可重入性,那么我可能做错了什么,但是我仍然感到很不舒服,因为只是为了保存返回值就需要使用全局变量。

有更好的方法吗?我更喜欢可移植性,但如果必须指定#!/bin/bash,那么这可能不是一个真正的限制。


新答案:declare -n。请参见https://dev59.com/b_UXpYgB1922wOYJe49p#76127851。 - guettli
10个回答

34

如果你的值从不包含空格,那么这个read技巧可以是一个简单的解决方案:

get_vars () {
  #...
  echo "value1" "value2"
}

read var1 var2 < <(get_vars)
echo "var1='$var1', var2='$var2'"
当然,一旦其中一个值中有空格,它就会中断。您可以修改 IFS 并在函数的 echo 中使用特殊分隔符,但这样的结果并不比其他建议的解决方案更简单。

请注意,当与“\n”分隔符一起使用时,read命令可能会返回退出代码1,即使它实际上已经成功执行,除非找不到行终止符。参见 - Ninh Pham

28

这个问题是5年前发布的,但我有一些有趣的答案要发表。我刚开始学习bash,也遇到了你当时遇到的同样的问题。我认为这个技巧可能会有所帮助:

#!/bin/sh

foo=""
bar=""

my_func(){
    echo 'foo="a"; bar="b"'
}

eval $(my_func)
echo $foo $bar
# result: a b

当子进程无法向其父进程发送值时,这个技巧也很有用。


1
虽然它不完美,但我觉得它通常适用。 - Yu Shen
如何为foo设置变量? - Pramodya Mendis

11

虽然我很喜欢shell,但当你需要处理任意结构化数据时,Unix bourne/posix shell可能并不是最合适的选择。

如果有一些字符没有出现在字段中,那么就用这些字符来分隔。经典的例子是/etc/passwd/etc/group和其他使用冒号作为字段分隔符的文件。

如果你使用的shell可以处理字符串中的NUL字符,则可以使用NUL连接并以其为分隔符(通过$IFS或其他方式)。但几个常见的shell,包括bash,在NUL上会出错。我的一个旧.sig的测试就出错了:

foo=$'a\0b'; [ ${#foo} -eq 3 ] && echo "$0 rocks"

即使这对你有用,你刚刚达到了需要切换到更加结构化的语言(Python、Perl、Ruby、Lua、Javascript...选择你喜欢的毒药)的警告信号之一。你的代码可能变得难以维护;即使你能维护,理解它足够好以便维护它的人员数量也会更少。


3
另一种方法:
function get_tuple()
{
  echo -e "Value1\nValue2"
}

IFS=$'\n' read -d '' -ra VALUES < <(get_tuple)
echo "${VALUES[0]}" # Value1
echo "${VALUES[1]}" # Value2

3
在不支持nameref(Bash 4.3-alpha中引入的)的旧版Bash中,我可以定义一个帮助函数,将返回值分配给给定的变量。这有点像使用eval来进行相同类型的变量分配。
示例1:
##  Add two complex numbers and returns it.
##  re: real part, im: imaginary part.
##
##  Helper function named by the 5th positional parameter
##  have to have been defined before the function is called.
complexAdd()
{
    local re1="$1" im1="$2" re2="$3" im2="$4" fnName="$5" sumRe sumIm

    sumRe=$(($re1 + $re2))
    sumIm=$(($im1 + $im2))

    ##  Call the function and return 2 values.
    "$fnName" "$sumRe" "$sumIm"
}

main()
{
    local fooRe='101' fooIm='37' barRe='55' barIm='123' bazRe bazIm quxRe quxIm

    ##  Define the function to receive mutiple return values
    ##  before calling complexAdd().
    retValAssign() { bazRe="$1"; bazIm="$2"; }
    ##  Call comlexAdd() for the first time.
    complexAdd "$fooRe" "$fooIm" "$barRe" "$barIm" 'retValAssign'

    ##  Redefine the function to receive mutiple return values.
    retValAssign() { quxRe="$1"; quxIm="$2"; }
    ##  Call comlexAdd() for the second time.
    complexAdd "$barRe" "$barIm" "$bazRe" "$bazIm" 'retValAssign'

    echo "foo = $fooRe + $fooIm i"
    echo "bar = $barRe + $barIm i"
    echo "baz = foo + bar = $bazRe + $bazIm i"
    echo "qux = bar + baz = $quxRe + $quxIm i"
}

main

例子2

##  Add two complex numbers and returns it.
##  re: real part, im: imaginary part.
##
##  Helper functions
##      getRetRe(), getRetIm(), setRetRe() and setRetIm()
##  have to have been defined before the function is called.
complexAdd()
{
    local re1="$1" im1="$2" re2="$3" im2="$4"

    setRetRe "$re1"
    setRetRe $(($(getRetRe) + $re2))

    setRetIm $(($im1 + $im2))
}

main()
{
    local fooRe='101' fooIm='37' barRe='55' barIm='123' bazRe bazIm quxRe quxIm

    ##  Define getter and setter functions before calling complexAdd().
    getRetRe() { echo "$bazRe"; }
    getRetIm() { echo "$bazIm"; }
    setRetRe() { bazRe="$1"; }
    setRetIm() { bazIm="$1"; }
    ##  Call comlexAdd() for the first time.
    complexAdd "$fooRe" "$fooIm" "$barRe" "$barIm"

    ##  Redefine getter and setter functions.
    getRetRe() { echo "$quxRe"; }
    getRetIm() { echo "$quxIm"; }
    setRetRe() { quxRe="$1"; }
    setRetIm() { quxIm="$1"; }
    ##  Call comlexAdd() for the second time.
    complexAdd "$barRe" "$barIm" "$bazRe" "$bazIm"

    echo "foo = $fooRe + $fooIm i"
    echo "bar = $barRe + $barIm i"
    echo "baz = foo + bar = $bazRe + $bazIm i"
    echo "qux = bar + baz = $quxRe + $quxIm i"
}

main

3
较新版本的Bash支持nameref。使用declare -n var_name将var_name赋予nameref属性。nameref使您的函数具有“按引用传递”的能力,这在C++函数中常用于返回多个值。根据Bash man页面:变量可以使用declare或local内置命令的-n选项分配nameref属性来创建nameref或对另一个变量的引用。这允许间接操作变量。每当引用或分配nameref变量时,实际上是在由nameref变量的值指定的变量上执行操作。nameref通常在shell函数中使用,以引用作为参数传递给函数的变量名称。以下是一些交互式命令行示例。
示例1:
$ unset xx yy
$ xx=16
$ yy=xx
$ echo "[$xx] [$yy]"
[16] [xx]
$ declare -n yy
$ echo "[$xx] [$yy]"
[16] [16]
$ xx=80
$ echo "[$xx] [$yy]"
[80] [80]
$ yy=2016
$ echo "[$xx] [$yy]"
[2016] [2016]
$ declare +n yy # Use -n to add and +n to remove nameref attribute.
$ echo "[$xx] [$yy]"
[2016] [xx]

示例2:
$ func()
> {
>     local arg1="$1" arg2="$2"
>     local -n arg3ref="$3" arg4ref="$4"
> 
>     echo ''
>     echo 'Local variables:'
>     echo "    arg1='$arg1'"
>     echo "    arg2='$arg2'"
>     echo "    arg3ref='$arg3ref'"
>     echo "    arg4ref='$arg4ref'"
>     echo ''
> 
>     arg1='1st value of local assignment'
>     arg2='2st value of local assignment'
>     arg3ref='1st return value'
>     arg4ref='2nd return value'
> }
$ 
$ unset foo bar baz qux
$ 
$ foo='value of foo'
$ bar='value of bar'
$ baz='value of baz'
$ qux='value of qux'
$ 
$ func foo bar baz qux

Local variables:
    arg1='foo'
    arg2='bar'
    arg3ref='value of baz'
    arg4ref='value of qux'

$ 
$ {
>     echo ''
>     echo '2 values are returned after the function call:'
>     echo "    foo='$foo'"
>     echo "    bar='$bar'"
>     echo "    baz='$baz'"
>     echo "    qux='$qux'"
> }

2 values are returned after the function call:
    foo='value of foo'
    bar='value of bar'
    baz='1st return value'
    qux='2nd return value'

1

如果你使用的是Bash 4,你可以利用关联数组。

declare -A ARR
function foo(){
  ...
  ARR["foo_return_value_1"]="VAR1"
  ARR["foo_return_value_2"]="VAR2"
}

你可以将它们作为字符串组合。

function foo(){
  ...
  echo "$var1|$var2|$var3"
}

那么每当您需要使用这些返回值时,

ret="$(foo)"
IFS="|"
set -- $ret
echo "var1 one is: $1"
echo "var2 one is: $2"
echo "var3 one is: $3"

就在昨天,我读到了另一个用户的评论,与另一个问题的类似答案有关。她指出像 set -- $ret 这样的东西是危险的,因为 $ret 在这样的结构中会经历文件名扩展/模式匹配,至少在 bash 中是这样(我不知道其他 shell)。如果 $ret 恰好包含相应的通配符,并且存在匹配的路径或文件名,则可能导致错误的结果和非常难以找到的问题。 - Binarus

1

我会采用我在这里提出的解决方案,但是使用一个数组变量代替。旧版本的bash不支持关联数组。 例如,

function some_func() # ARRVAR args...
{
    local _retvar=$1 # I use underscore to avoid clashes with return variable names
    local -a _out
    # ... some processing ... (_out[2]=xxx etc.)
    eval $_retvar='("${_out[@]}")'
}

呼叫站点:
function caller()
{
    local -a tuple_ret # Do not use leading '_' here.
    # ...
    some_func tuple_ret "arg1"
    printf "  %s\n" "${tuple_ret[@]}" # Print tuple members on separate lines
}

1

我是bash新手,但发现这段代码很有帮助。

function return_multiple_values() {
        eval "$1='What is your name'"
        eval "$2='my name is: BASH'"
    }
    
    return_var=''
    res2=''
    return_multiple_values return_var res2
    echo $return_var
    echo $res2

0
Shell脚本函数只能返回最后一个执行的命令的退出状态或由return语句明确指定的该函数的退出状态。
要返回某个字符串,一种方法可能是这样的:
function fun()
{
  echo "a+b"
}

var=`fun` # Invoke the function in a new child shell and capture the results
echo $var # use the stored result

尽管这样做会增加创建新shell的开销,因此稍微慢一些,但可以减轻您的不适感。


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