Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки | ||
---|---|---|
Назад | Вперед |
Подобно "настоящим" языкам программирования, Bash тоже имеет функции, хотя и в несколько ограниченном варианте. Функция -- это подпрограмма, блок кода который реализует набор операций, своего рода "черный ящик", предназначенный для выполнения конкретной задачи. Функции могут использоваться везде, где имеются участки повторяющегося кода.
function function_name {
command...
}
function_name () {
command...
}
Вторая форма записи ближе к сердцу C-программистам (она же более переносимая).
Как и в языке C, скобка, открывающая тело функции, может помещаться на следующей строке.
function_name ()
{
command...
}
Вызов функции осуществляется простым указанием ее имени в тексте сценария.
Пример 22-1. Простая функция
#!/bin/bash funky () { echo "Это обычная функция." } # Функция должна быть объявлена раньше, чем ее можно будет использовать. # Вызов функции. funky 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
Команда shift вполне применима и к аргументам функций (см. Пример 33-10).
В отличие от других языков программирования, в сценариях на языке командной оболочке, в функции передаются аргументы по значению. [1] Если имена переменных (которые фактически являются указателями) передаются функции в виде аргументов, то они интерпретируются как обычные строки символов и не могут быть разыменованы. Функции интерпретируют свои аргументы буквально.
- код завершения
Функции возвращают значение в виде кода завершения. Код завершения может быть задан явно, с помощью команды return, в противном случае будет возвращен код завершения последней команды в функции (0 -- в случае успеха, иначе -- ненулевой код ошибки). Код завершения в сценарии может быть получен через переменную $?.
- return
Завершает исполнение функции. Команда return [2] может иметь необязательный аргумент типа integer, который возвращается в вызывающий сценарий как "код завершения" функции, это значение так же записывается в переменную $?.
Пример 22-3. Наибольшее из двух чисел
#!/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 # Упражнение: # --------------- # Сделайте этот сценарий интерактивным, #+ т.е. заставьте сценарий запрашивать числа для сравнения у пользователя (два числа).
Для случаев, когда функция должна возвращать строку или массив, используйте специальные переменные.
count_lines_in_etc_passwd() { [[ -r /etc/passwd ]] && REPLY=$(echo $(wc -l < /etc/passwd)) # Если файл /etc/passwd доступен на чтение, то в переменную REPLY заносится число строк. # Возвращаются как количество строк, так и код завершения. } if count_lines_in_etc_passwd then echo "В файле /etc/passwd найдено $REPLY строк." else echo "Невозможно подсчитать число строк в файле /etc/passwd." fi # Спасибо S.C.
Пример 22-4. Преобразование чисел в римскую форму записи
#!/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.
Наибольшее положительное целое число, которое может вернуть функция -- 255. Команда return очень тесно связана с понятием код завершения, что объясняет это специфическое ограничение. К счастью существуют различные способы преодоления этого ограничения.
Пример 22-5. Проверка возможности возврата функциями больших значений
#!/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
Пример 22-6. Сравнение двух больших целых чисел
#!/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-7. Настоящее имя пользователя
#!/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.
[1] |
Механизм косвенных ссылок на переменные (см. Пример 34-2) слишком неудобен для передачи аргументов по ссылке. #!/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 |
[2] |
Команда return -- это встроенная команда Bash. |