Cogitatio materialis est

Как вернуть значение из bash функции?

8th Oct 2013 Tags: #bash #development

Как известно, в bash все функции и команды возвращают лишь код возврата (ноль - успешное завершение, не-ноль - ошибка). Для того, чтобы вернуть нечто отличное от числа, нужно использовать один из следующих приёмов:

  • установка глобальной переменной
  • использование подстановки (чтение вывода команды)
  • передача косвенной ссылки в функцию

Рассмотрим подробнее, как же использовать данные подходы.


Установка глобальной переменной

Что может быть проще, чем загадить глобальную область видимости в функции, и затем считать из неё данные в основной программе?

  function makedirty()
  {
      myresult='oh, sic...!'
  }

  makedirty
  echo $myresult  # oh, sic...!

Оно и понятно, ведь все переменные в bash по-умолчанию - глобальные.

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

Иcпользование подстановки, aka $(function)

Довольно распространённый механизм. Вы уже, наверняка, сами не зная того, неоднократно им пользовались. Идея в том, что мы сохраняем в переменную всё, что выводит некая команда (функция -- тоже команда). Ну и конечно, что бы без необходимости не засорять глобальную область видимости, рекомендуется использовать локальные переменные.

  function makecoffe()
  {
      local  myresult='done done done!'
      echo "$myresult"
  }

  result=$(makecoffe)   # или result=`myfunc`
  echo $result          # done done done!
+ Не засоряется глобальная область видимости
- Можно вернуть лишь 1 значение (или несколько разделённых неким разделителем)

Передача косвенной ссылки (Indirect References) в функцию

Самый интересный, функциональный и неуклюжий механизм :)
Это проще 1 раз увидеть, чем 100 раз услышать:

  #!/bin/bash
  # return-val-func-3.sh: Передача косвенной ссылки в функцию.

  echo_var ()
  {
      echo "$1"
  }

  message=Hello
  Hello=Goodbye

  echo_var "$message"        # Hello
  # Теперь давайте передадим в функцию косвенную ссылку.
  # Т.е. дважды разыменуем переменную message
  echo_var "${!message}"     # Goodbye

  echo "-------------"

  # И что же произойдёт, если мы поменяем содержимое переменной "Hello" ?
  Hello="Hello, again!"
  echo_var "$message"        # Hello
  echo_var "${!message}"     # Hello, again!

  exit 0

А теперь по-честному и по порядку

В отличии от некоторых других языков программирования, в bash сценариях параметры в функцию обычно передаются по значению. Если имена переменных (которые фактически являются указателями) будут переданы в функцию в качестве параметров, то они будут обработаны, как строковые литералы.
Функции обрабатывают свои аргументы, как литералы.

Переменные - это способ представления данных в различных языках программирования. Переменная не более чем метка - имя, назначенное области или группе областей памяти компьютера, где содержатся данные.

Имя переменной - это "контейнер" для хранения значения данных. Обращение к значению (получение значения) переменной называется подстановкой переменной.

Отмечу, что надо различать имя переменной и ее значение. Если variable1 - имя переменной, то $variable1 - обращение к значению, которое она содержит.

Имя переменной - в действительности, ссылка/указатель на область(ти) памяти, где фактические данные связаны со значением, которое они хранят.

  bash$ variable1=23
  bash$ echo variable1
  variable1
  bash$ echo $variable1
  23

Как сказано выше, после подстановки переменной (referencing a variable, $var), мы получаем значение, на которую указывает переменная. А как насчёт значения, на которое укзывает само значение? $$var ? Вы же помните, что с переменными мы работаем просто как с литералами? :) Ну вот мы и пробуем получить значение, на которое указывает переменная, имя которой тоже является значением другой переменной. Немного запутанно, но, надеюсь, уловили.

Настоящая нотация выглядит как \$$var (или, c версии bash 2, как ${!variable}) и обычно используется вместе с eval. Данный механизм и называется косвенная ссылка (indirect reference).

Пример 1. Для понимания

  #!/bin/bash
  # dereference.sh
  # Разыменовывание параметра переданного в функцию.
  # Сценарий от Bruce W. Clare.

  dereference ()
  {
       y=\$"$1"   # Сохранили имя переменной
       echo $y    # $Junk     == echo \$$y

       x=`eval "expr \"$y\" "`    # $x == Junk
       echo $1=$x
       eval "$1=\"Некоторый Другой Текст \""  # Присваиваем новое значение.
  }

  Junk="Некоторый Текст"
  echo $Junk "до"         # Некоторый Текст до

  dereference Junk
  echo $Junk "после"     # Некоторый Другой Текст после

  exit 0

Пример 2. Практичный

  function myfunc()
  {
      local  __resultvar=$1
      local  myresult='some value'
      eval $__resultvar="'$myresult'"
  }

  myfunc result
  echo $result

Так как имя переменной само хранится в переменной, мы не можем установить его напрямую. Для этого используют eval. Если в кратце, eval сообщает bash-интерпретатору, что необходимо дважды проинтерпретировать строку. В первый раз он установит result='some value', а во второй - искомую переменную (которую передали как аргумент).

При сохранении имени переменной, которую передали как аргумент командной строки, убедитесь, что сохраняете её локально и в переменную с отличным (по возможности) именем, нежели использовалась в командной строке. Если этого не сделать, и имя переменной, переданной как параметр командной строки, совпадёт с именем локальной переменной, в которую Вы сохраняете значение, то ничего не будет работать :D

Пример 3. Не работающий!

  function myfunc()
  {
      local  result=$1
      local  myresult='some value'
      eval $result="'$myresult'"
  }

  myfunc result
  echo $result

Почему? Ну... в тот момент когда eval совершает вторую подстановку в result='some value', result - уже локальная переменная функции, поэтому мы установим её ещё разок, а не искомую переменную.

Для большей гибкости, можно комбинировать разные подходы:

Пример 4. Универсальный

  function myfunc()
  {
      local  __resultvar=$1
      local  myresult='some value'
      if [[ "$__resultvar" ]]; then
          eval $__resultvar="'$myresult'"
      else
          echo "$myresult"
      fi
  }

  myfunc result
  echo $result        # some value
  result2=$(myfunc)
  echo $result2       # some value

Т.е. если в функцию не передано ни одного параметра, значение просто выводится в stdout.


Использованные источники: