Функции

Глава 22. Функции

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

function function_name {
command...
}

или

function_name () {
command...
}



Вторая форма записи ближе к сердцу C-программистам (она же более переносимая).

Как и в языке C, скобка, открывающая тело функции, может помещаться на следующей строке.

function_name ()
{
command...
}



Вызов функции осуществляется простым указанием ее имени в тексте сценария.

Пример 22-1. Простые функции

#!/bin/bash

JUST_A_SECOND=1

funky ()
{ # Это пример простейшей функции, которая выводит сообщение.
  echo "Это функция funky."
} # Объявление функции должно предшествовать ее вызову.


fun ()
{ # Более сложная функция.
  i=0
  REPEATS=30

  echo
  echo "Теперь запускается функция fun."
  echo

  sleep $JUST_A_SECOND    # Эй! Погодите секундочку!
  while [ $i -lt $REPEATS ]
  do
    echo "------------------РАБОТА------------------->"
    echo "<---------------С ФУНКЦИЯМИ----------------"
    echo "<------------ОЧЕНЬ УВЛЕКАТЕЛЬНА------------>"
    echo
    let "i+=1"
  done
}

  # А теперь собственно вызов функций.

funky
fun

exit 0

Функция должна быть объявлена раньше, чем ее можно будет использовать. К сожалению, в Bash нет возможности "опережающего объявления" функции, как например в C.

f1
# Эта строка вызовет сообщение об ошибке, поскольку функция "f1" еще не определена.

declare -f f1      # Это не поможет.
f1                 # По прежнему -- сообщение об ошибке.

# Однако...


f1 ()
{
  echo "Вызов функции \"f2\" из функции \"f1\"."
  f2
}

f2 ()
{
  echo "Функция \"f2\"."
}

f1  #  Функция "f2", фактически, не вызывается выше этой строки,
    #+ хотя ссылка на нее встречается выше, до ее объявления.
    #  Это допускается.

    # Спасибо S.C.


Допускается даже создание вложенных функций, хотя пользы от этого немного.

f1 ()
{

  f2 () # вложенная
  {
    echo "Функция \"f2\", вложенная в \"f1\"."
  }

}

f2  #  Вызывает сообщение об ошибке.
    #  Даже "declare -f f2" не поможет.

echo

f1  #  Ничего не происходит, простой вызов "f1", не означает автоматический вызов "f2".
f2  #  Теперь все нормально, вызов "f2" не приводит к появлению ошибки,
    #+ поскольку функция "f2" была определена в процессе вызова "f1".

    # Спасибо S.C.


Объявление функции может размещаться в самых неожиданных местах.

ls -l | foo() { echo "foo"; }  # Допустимо, но бесполезно.



if [ "$USER" = bozo ]
then
  bozo_greet ()   # Объявление функции размещено в условном операторе.
  {
    echo "Привет, Bozo!"
  }
fi

bozo_greet        # Работает только у пользователя bozo, другие получат сообщение об ошибке.



# Нечто подобное можно использовать с определеной пользой для себя.
NO_EXIT=1   # Will enable function definition below.

[[ $NO_EXIT -eq 1 ]] && exit() { true; }     # Определение функции в последовательности "И-список".
# Если $NO_EXIT равна 1, то объявляется "exit ()".
# Тем самым, функция "exit" подменяет встроенную команду "exit".

exit  # Вызывается функция "exit ()", а не встроенная команда "exit".

# Спасибо S.C.


22.1. Сложные функции и сложности с функциями

Функции могут принимать входные аргументы и возвращать код завершения.

function_name $arg1 $arg2

Доступ к входным аргументам, в функциях, производится посредством позиционных параметров, т.е. $1, $2 и так далее.

Пример 22-2. Функция с аргументами

#!/bin/bash
# Функции и аргументы

DEFAULT=default                             # Значение аргумента по-умолчанию.

func2 () {
   if [ -z "$1" ]                           # Длина аргумента #1 равна нулю?
   then
     echo "-Аргумент #1 имеет нулевую длину.-"  # Или аргумент не был передан функции.
   else
     echo "-Аргумент #1: \"$1\".-"
   fi

   variable=${1-$DEFAULT}                   #  Что делает
   echo "variable = $variable"              #+ показанная подстановка параметра?
                                            #  ---------------------------
                                            #  Она различает отсутствующий аргумент
                                            #+ от "пустого" аргумента.

   if [ "$2" ]
   then
     echo "-Аргумент #2: \"$2\".-"
   fi

   return 0
}

echo

echo "Вызов функции без аргументов."
func2
echo


echo "Вызов функции с \"пустым\" аргументом."
func2 ""
echo

echo "Вызов функции с неинициализированным аргументом."
func2 "$uninitialized_param"
echo

echo "Вызов функции с одним аргументом."
func2 first
echo

echo "Вызов функции с двумя аргументами."
func2 first second
echo

echo "Вызов функции с аргументами \"\" \"second\"."
func2 "" second       # Первый параметр "пустой"
echo                  # и второй параметр -- ASCII-строка.

exit 0
Important

Команда shift вполне применима и к аргументам функций (см. Пример 33-11).

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

Механизм косвенных ссылок на переменные (см. Пример 34-2) слишком неудобен для передачи аргументов по ссылке.

Пример 22-3. Передача косвенных ссылок в функцию

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

echo_var ()
{
echo "$1"
}

message=Hello
Hello=Goodbye

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

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

# Что произойдет, если изменить содержимое переменной "Hello"?
Hello="Hello, again!"
echo_var "$message"        # Hello
echo_var "${!message}"     # Hello, again!

exit 0

Тут же возникает вопрос: "Возможно ли изменить значение переменной, которая была передана по ссылке?"

Пример 22-4. Изменение значения переменной, переданной в функцию по ссылке.

#!/bin/bash
# dereference.sh
# Изменение значения переменной, переданной в функцию по ссылке.
# Автор: Bruce W. Clare.

dereference ()
{
     y=\$"$1"   # Имя переменной.
     echo $y    # $Junk

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

Junk="Некий текст"
echo $Junk "до того как..."    # Некий текст до того как...

dereference Junk
echo $Junk "после того как..." # Некий другой текст после того как...

exit 0

Пример 22-5. Еще один пример разыменования параметров функции, передаваемых по ссылке.

#!/bin/bash

ITERATIONS=3  # Количество вводимых значений.
icount=1

my_read () {
  # При вызове my_read varname,
  # выводит предыдущее значение в квадратных скобках,
  # затем просит ввести новое значение.

  local local_var

  echo -n "Введите говое значение переменной "
  eval 'echo -n "[$'$1'] "'  # Прежнее значение.
  read local_var
  [ -n "$local_var" ] && eval $1=\$local_var

  # Последовательность "And-list": если "local_var" не пуста, то ее значение переписывается в "$1".
}

echo

while [ "$icount" -le "$ITERATIONS" ]
do
  my_read var
  echo "Значение #$icount = $var"
  let "icount += 1"
  echo
done


# Спасибо Stephane Chazelas за этот поучительный пример.

exit 0

Exit и Return

код завершения

Функции возвращают значение в виде кода завершения. Код завершения может быть задан явно, с помощью команды return, в противном случае будет возвращен код завершения последней команды в функции (0 -- в случае успеха, иначе -- ненулевой код ошибки). Код завершения в сценарии может быть получен через переменную $?.

return

Завершает исполнение функции. Команда return [1] может иметь необязательный аргумент типа integer, который возвращается в вызывающий сценарий как "код завершения" функции, это значение так же записывается в переменную $?.

Пример 22-6. Наибольшее из двух чисел

#!/bin/bash
# max.sh: Наибольшее из двух целых чисел.

E_PARAM_ERR=-198    # Если функции передано меньше двух параметров.
EQUAL=-199          # Возвращаемое значение, если числа равны.

max2 ()             # Возвращает наибольшее из двух чисел.
{                   # Внимание: сравниваемые числа должны быть меньше 257.
if [ -z "$2" ]
then
  return $E_PARAM_ERR
fi

if [ "$1" -eq "$2" ]
then
  return $EQUAL
else
  if [ "$1" -gt "$2" ]
  then
    return $1
  else
    return $2
  fi
fi
}

max2 33 34
return_val=$?

if [ "$return_val" -eq $E_PARAM_ERR ]
then
  echo "Функции должно быть передано два аргумента."
elif [ "$return_val" -eq $EQUAL ]
  then
    echo "Числа равны."
else
    echo "Наибольшее из двух чисел: $return_val."
fi


exit 0

#  Упражнение:
#  ---------------
#  Сделайте этот сценарий интерактивным,
#+ т.е. заставьте сценарий запрашивать числа для сравнения у пользователя (два числа).
Tip

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

count_lines_in_etc_passwd()
{
  [[ -r /etc/passwd ]] && REPLY=$(echo $(wc -l < /etc/passwd))
  # Если файл /etc/passwd доступен на чтение, то в переменную REPLY заносится число строк.
  # Возвращаются как количество строк, так и код завершения.
  # Команда 'echo' может показаться ненужной, но . . .
  #+ она предотвращает вывод лишних пробелов.
}

if count_lines_in_etc_passwd
then
  echo "В файле /etc/passwd найдено $REPLY строк."
else
  echo "Невозможно подсчитать число строк в файле /etc/passwd."
fi

# Спасибо S.C.


Пример 22-7. Преобразование чисел в римскую форму записи

#!/bin/bash

# Преобразование чисел из арабской формы записи в римскую
# Диапазон: 0 - 200

# Расширение диапазона представляемых чисел и улучшение сценария
# оставляю вам, в качестве упражнения.

# Порядок использования: roman number-to-convert

LIMIT=200
E_ARG_ERR=65
E_OUT_OF_RANGE=66

if [ -z "$1" ]
then
  echo "Порядок использования: `basename $0` number-to-convert"
  exit $E_ARG_ERR
fi

num=$1
if [ "$num" -gt $LIMIT ]
then
  echo "Выход за границы диапазона!"
  exit $E_OUT_OF_RANGE
fi

to_roman ()   # Функция должна быть объявлена до того как она будет вызвана.
{
number=$1
factor=$2
rchar=$3
let "remainder = number - factor"
while [ "$remainder" -ge 0 ]
do
  echo -n $rchar
  let "number -= factor"
  let "remainder = number - factor"
done

return $number
       # Упражнение:
       # --------
       # Объясните -- как работает функция.
       # Подсказка: деление последовательным вычитанием.
}


to_roman $num 100 C
num=$?
to_roman $num 90 LXXXX
num=$?
to_roman $num 50 L
num=$?
to_roman $num 40 XL
num=$?
to_roman $num 10 X
num=$?
to_roman $num 9 IX
num=$?
to_roman $num 5 V
num=$?
to_roman $num 4 IV
num=$?
to_roman $num 1 I

echo

exit 0

См. также Пример 10-28.

Important

Наибольшее положительное целое число, которое может вернуть функция -- 255. Команда return очень тесно связана с понятием код завершения, что объясняет это специфическое ограничение. К счастью существуют различные способы преодоления этого ограничения.

Пример 22-8. Проверка возможности возврата функциями больших значений

#!/bin/bash
# return-test.sh

# Наибольшее целое число, которое может вернуть функция, не может превышать 256.

return_test ()         # Просто возвращает то, что ей передали.
{
  return $1
}

return_test 27         # o.k.
echo $?                # Возвращено число 27.

return_test 255        # o.k.
echo $?                # Возвращено число 255.

return_test 257        # Ошибка!
echo $?                # Возвращено число 1.

return_test -151896    # Как бы то ни было, но для больших отрицательных чисел проходит!
echo $?                # Возвращено число -151896.

exit 0

Самый простой способ вернуть из функции большое положительное число -- это присвоить "возвращаемое значение" глобальной переменной.

Return_Val=   # Глобальная переменная, которая хранит значение, возвращаемое функцией.

alt_return_test ()
{
  fvar=$1
  Return_Val=$fvar
  return   # Возвратить 0 (успешное завершение).
}

alt_return_test 1
echo $?                                  # 0
echo "Функция вернула число $Return_Val" # 1

alt_return_test 255
echo "Функция вернула число $Return_Val" # 255

alt_return_test 257
echo "Функция вернула число $Return_Val" # 257

alt_return_test 25701
echo "Функция вернула число $Return_Val" #25701


Еще более элегантный способ заключается в передаче возвращаемого значания команде echo, для вывода на stdout, которое затем снимается со стандартного вывода конструкцией подстановки команд. См. обсуждение этого приема в Section 33.7.

Пример 22-9. Сравнение двух больших целых чисел

#!/bin/bash
# max2.sh: Наибольшее из двух БОЛЬШИХ целых чисел.

# Это модификация предыдущего примера "max.sh",
# которая позволяет выполнять сравнение больших целых чисел.

EQUAL=0             # Если числа равны.
MAXRETVAL=255       # Максимально возможное положительное число, которое может вернуть функция.
E_PARAM_ERR=-99999  # Код ошибки в параметрах.
E_NPARAM_ERR=99999  # "Нормализованный" код ошибки в параметрах.

max2 ()             # Возвращает наибольшее из двух больших целых чисел.
{
if [ -z "$2" ]
then
  return $E_PARAM_ERR
fi

if [ "$1" -eq "$2" ]
then
  return $EQUAL
else
  if [ "$1" -gt "$2" ]
  then
    retval=$1
  else
    retval=$2
  fi
fi

# -------------------------------------------------------------- #
# Следующие строки позволяют "обойти" ограничение
if [ "$retval" -gt "$MAXRETVAL" ]    # Если больше предельного значения,
then                                 # то
  let "retval = (( 0 - $retval ))"   # изменение знака числа.
  # (( 0 - $VALUE )) изменяет знак числа.
fi
# Функции имеют возможность возвращать большие *отрицательные* числа.
# -------------------------------------------------------------- #

return $retval
}

max2 33001 33997
return_val=$?

# -------------------------------------------------------------------------- #
if [ "$return_val" -lt 0 ]                  # Если число отрицательное,
then                                        # то
  let "return_val = (( 0 - $return_val ))"  # опять изменить его знак.
fi                                          # "Абсолютное значение" переменной $return_val.
# -------------------------------------------------------------------------- #


if [ "$return_val" -eq "$E_NPARAM_ERR" ]
then                   # Признак ошибки в параметрах, при выходе из функции так же поменял знак.
  echo "Ошибка: Недостаточно аргументов."
elif [ "$return_val" -eq "$EQUAL" ]
  then
    echo "Числа равны."
else
    echo "Наиболшее число: $return_val."
fi

exit 0

См. также Пример A-8.

Упражнение: Используя только что полученные знания, добавьте в предыдущий пример, преобразования чисел в римскую форму записи, возможность обрабатывать большие числа.

Перенаправление

Перенаправление ввода для функций

Функции -- суть есть блок кода, а это означает, что устройство stdin для функций может быть переопределено (перенаправление stdin) (как в Пример 3-1).

Пример 22-10. Настоящее имя пользователя

#!/bin/bash

# По имени пользователя получить его "настоящее имя" из /etc/passwd.

ARGCOUNT=1  # Ожидается один аргумент.
E_WRONGARGS=65

file=/etc/passwd
pattern=$1

if [ $# -ne "$ARGCOUNT" ]
then
  echo "Порядок использования: `basename $0` USERNAME"
  exit $E_WRONGARGS
fi

file_excerpt ()  # Производит поиск в файле по заданному шаблону, выводит требуемую часть строки.
{
while read line
do
  echo "$line" | grep $1 | awk -F":" '{ print $5 }'  # Указывет awk использовать ":" как разделитель полей.
done
} <$file  # Подменить stdin для функции.

file_excerpt $pattern

# Да, этот сценарий можно уменьшить до
#       grep PATTERN /etc/passwd | awk -F":" '{ print $5 }'
# или
#       awk -F: '/PATTERN/ {print $5}'
# или
#       awk -F: '($1 == "username") { print $5 }'
# Однако, это было бы не так поучительно.

exit 0

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

# Вместо:
Function ()
{
 ...
 } < file

# Попробуйте так:
Function ()
{
  {
    ...
   } < file
}

# Похожий вариант,

Function ()  # Тоже работает.
{
  {
   echo $*
  } | tr a b
}

Function ()  # Этот вариант не работает.
{
  echo $*
} | tr a b   # Наличие вложенного блока кода -- обязательное условие.


# Спасибо S.C.


Notes

[1]

Команда return -- это встроенная команда Bash.