Идиомы для возврата нескольких значений в сценарии оболочки

Существуют ли какие-либо идиомы для возврата нескольких значений из функции bash в script?

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}"

Я понимаю, что если мне нужна перезагрузка в оболочке script, я, вероятно, ошибаюсь, но мне по-прежнему очень неудобно бросать глобальные переменные только для того, чтобы удерживать возвращаемые значения.

Есть ли лучший способ? Я бы предпочел переносимость, но это, вероятно, не является реальным ограничением, если я должен указать #!/bin/bash.

Ответы

Ответ 1

Как я люблю оболочку, возможно, это так, что, как только вы производите произвольные структурированные данные, Unix bourne/posix shell не является правильным выбором.

Если есть символы, которые не встречаются внутри полей, разделите их одним из них. Классический пример: /etc/passwd, /etc/group и различные другие файлы, которые используют двоеточие в качестве разделителя полей.

Если вы используете оболочку, которая может обрабатывать символ NUL внутри строк, то объединение в NUL и разделение на нее (через IFS или что-то еще) могут работать хорошо. Но несколько общих оболочек, включая bash, ломают NUL. Тест был бы старым .sig:

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

Даже если это сработает для вас, вы только что достигли одного из предупреждающих знаков о том, что пришло время переключиться на более структурированный язык (Python, Perl, Ruby, Lua, Javascript... выберите предпочтительный яд). Ваш код, вероятно, будет трудно поддерживать; даже если вы можете, есть меньший пул людей, которые поймут это достаточно хорошо, чтобы сохранить его.

Ответ 2

В специальном случае, когда ваши значения никогда не содержат пробелов, этот трюк read может быть простым решением:

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

read var1 var2 < <(get_vars)
echo "var1='$var1', var2='$var2'"

Но, разумеется, он разбивается, как только в одном из значений есть пробел. Вы можете изменить IFS и использовать специальный разделитель в своей функции echo, но тогда результат не намного проще, чем другие предлагаемые решения.

Ответ 3

Этот вопрос был опубликован 5 лет назад, но у меня есть интересный ответ. Я сам только начал изучать bash, и я также сталкиваюсь с той же проблемой, что и вы. Я думаю, что этот трюк может быть полезным:

#!/bin/sh

foo=""
bar=""

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

print_result(){
    echo $1 $2
}

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

Если вы не хотите использовать слишком много глобальных переменных, попробуйте следующее:

#!/bin/sh

my_func(){
    echo 'print_it "a" "b"'
}

print_result(){
    echo $1 $2
}

eval $(my_func)
# result: a b

Этот трюк также полезен для решения проблемы, когда дочерний процесс не может отправить значение обратно в глобальную переменную родительского процесса, например.

#!/bin/sh

msg="" #global variable
stat=""

say_hello(){
    msg="hello" # doesn't work at all!
    echo "success"
}

output=$(say_hello) # child process $(...) return "success"

решить:

#!/bin/sh

msg="" #global variable
stat=""

say_hello(){
    echo 'msg="hello"; stat"success"'
}

eval $(say_hello) # this line evaluates to msg="hello"; stat"success"

Ответ 4

вы можете использовать ассоциативные массивы, у вас есть bash 4 eg

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"

Ответ 5

Я бы выбрал решение я здесь, но вместо этого использовал переменную массива. Старые bash: es не поддерживают ассоциативные массивы. Например.,

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
}

Ответ 6

Более поздняя версия Bash поддерживает nameref. Используйте declare -n var_name, чтобы дать var_name атрибут nameref. nameref дает вашей функции возможность "передавать по ссылке", которая обычно используется в функциях С++ для возврата нескольких значений. Согласно Bash странице man:

переменной можно присвоить атрибут nameref с помощью параметра -n для встроенных команд declare или local для создания nameref или ссылка на другую переменную. Это позволяет косвенно манипулировать переменными. Всякий раз, когда переменная nameref ссылается или назначается, операция фактически выполняется для переменной, указанной значением переменной nameref. Nameref обычно используется в функциях оболочки для ссылки на переменную, имя которой передается как аргумент функции.

Ниже приведены примеры интерактивной командной строки.

Пример 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'

Ответ 7

Для версии Bash, которая не поддерживает nameref (введена в Bash 4.3-alpha), я могу определить вспомогательную функцию, в которой возвращаемое значение присваивается данной переменной. Это похоже на использование 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

Ответ 8

Функции оболочки script могут возвращать только статус выхода последней выполненной команды или статус выхода этой функции, явно указанный оператором 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

Это может уменьшить ваш дискомфорт, хотя он добавляет накладные расходы на создание новой оболочки и, следовательно, будет незначительно медленнее.

Ответ 9

Еще один способ:

function get_tuple()
{
  echo -e "Value1\nValue2"
}

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