BASH: использование функций, примеры

Автор: | 11/09/2013
 

terminalОбъявление и вызов функции

Вызов функции с аргументами

Переменные в функциях

Локальные переменные

Математические операции в функциях

Рекурсивные функции

Экспорт функций

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

По сути функция в bash является обычной переменной, но с более широкими  возможностями.

Основное применение — в тех случаях, когда один и тот же код необходимо использовать несколько раз и/или в разных связанных скриптах.

Объявление и вызов функции

Объявляется функция так:

function function_name ()
{
function body
}

Или:

function one {
echo "One"
}

two () {
echo "Two"
}

function three () {
echo "Three"
}

Однако, наиболее правильным вариантом, в целях совместимости скрипта с разными shell-оболочками будет второй:

two () {
echo "Two"
}

И старайтесь никогда не использовать третий вариант:

function three () {
echo "Three"
}

Вызвать функцию можно просто указав её имя в теле скрипта:

#!/bin/bash
function one {
echo "One"
}

one
$ ./example.sh
One

Важно, что бы объявление функции было выполнено до того, как она будет вызвана, иначе будет получена ошибка:

#!/bin/bash
function one {
echo "One"
}

one

two
$ ./example.sh
One
./example.sh: line 7: two: command not found

Вызов функции с аргументами

Перейдём к более сложной функции, и рассмотрим вызов функции с аргументами.

Для примера — возьмём функцию, которая вызывается в том месте кода, где необходимо получить ответ от пользователя:

#!/bin/bash
answer () {
while read response; do
echo
case $response in
[yY][eE][sS]|[yY])
printf "$1n"
$2
break
;;
[nN][oO]|[nN])
printf "$3n"
$4
break
;;
*)
printf "Please, enter Y(yes) or N(no)! "
esac
done
}

В данном случае, функция answer ожидает ответа от пользователя в стиле Yes или No (или любая вариация, заданная в выражении [yY][eE][sS]|[yY] или [nN][oO]|[nN]), и в зависимости от ответа выполняет определённое действие.

К примеру, в случае ответа Yes — будет выполнено действие, заданное в первом аргументе $1, с которым была вызвана функция:

#!/bin/bash
answer () {
while read response; do
echo
case $response in
[yY][eE][sS]|[yY])
printf "$1n"
$2
break
;;
[nN][oO]|[nN])
printf "$3n"
$4
break
;;
*)
printf "Please, enter Y(yes) or N(no)! "
esac
done
}

echo -e "nRun application? (Yes/No) "
answer "Run" "" "Not run" ""

Проверим:

$ ./example.sh

Run application? (Yes/No)
y

Run

Или попробуем ответить No:

$ ./example.sh

Run application? (Yes/No)
no

Not run

Вызов команд непосредственно из аргументов, а тем более из переменных, считается не самым хорошим решением, поэтому — давайте перепишем её и вызовем с операторами && (в случае успешного выполнения, то есть при получении кода 0) и || — в случае ошибки и  получения кода ответа 1:

#!/bin/bash
answer () {
while read response; do
echo
case $response in
[yY][eE][sS]|[yY])
printf "$1n"
return 0
break
;;
[nN][oO]|[nN])
printf "$2n"
return 1
break
;;
*)
printf "Please, enter Y(yes) or N(no)! "
esac
done
}

echo -e "nRun application? (Yes/No) "
answer "Run" "Will not run" && echo "I'm script" || echo "Doing nothing"

Теперь мы первым аргументом передаём функции ответ «Run«, и в случае ответа пользователя Yes — выполняем printf "Run" и echo "I'm script". Если выбран ответ No — то мы печатаем второй аргумент Will not run и выполняем действие echo "Doing nothing":

$ ./example.sh

Run application? (Yes/No)
y

Run
I'm script
$ ./example.sh

Run application? (Yes/No)
n

Will not run
Doing nothing

Соответственно, вместо echo можно выполнить любую другую команду:

#!/bin/bash
answer () {
while read response; do
echo
case $response in
[yY][eE][sS]|[yY])
printf "$1n"
return 0
break
;;
[nN][oO]|[nN])
printf "$2n"
return 1
break
;;
*)
printf "Please, enter Y(yes) or N(no)! "
esac
done
}

echo -e "nKill TOP application? (Yes/No) "
answer "Killing TOP" "Left it alive" && kill 20425 || echo "Doing nothing"
$ ./example.sh

Kill TOP application? (Yes/No)
y

Killing TOP

Важно учитывать, что в случае если первая команда завершится неудачно (в данном примере — не найдёт указанный PID) — то функция вернёт код 1, и будет выполнена вторая часть:

$ ./example.sh

Kill TOP application? (Yes/No)
y

Killing TOP
./example.sh: line 27: kill: (20425) - No such process
Doing nothing

Переменные в функциях

В аргументах так же можно использовать переменные.

Например — можно определить несколько вариантов ответов в разных переменных, и использовать нужную в разных случаях:

#!/bin/bash
answer () {
while read response; do
echo
case $response in
[yY][eE][sS]|[yY])
printf "$1n"
return 0
break
;;
[nN][oO]|[nN])
printf "$2n"
return 1
break
;;
*)
printf "Please, enter Y(yes) or N(no)! "
esac
done
}

replay1="Killing TOP"
replay2="Left it alive"

echo -e "nKill TOP application? (Yes/No) "
answer "$replay1" "$replay2" && echo "I'm script" || echo "Doing nothing"
$ ./example.sh

Kill TOP application? (Yes/No)
y

Killing TOP
I'm script
$ ./example.sh

Kill TOP application? (Yes/No)
n

Left it alive
Doing nothing

Как и с обычными переменными — функции используют «позиционные агрументы», т.е.:

$# — вывод количества переданных аргументов;
$* — вывод списка всех переданных аргументов;
$@ — то же, что и $* — но каждый аргумент считается как простое слово (строка);
$1 - $9 — нумерованные аргументы, в зависимости от позиции в списке.

Например — создадим такой скрипт с функцией, которая должна вывести количество переданных аргументов:

#!/bin/bash
example () {
echo $#
shift
}

example $*
$ ./example.sh 1 2 3 4
4

Или — просто вывести на экран все переданные ей аргументы:

#!/bin/bash
example () {
echo $*
shift
}

example $*
$ ./example.sh 1 2 3 4
1 2 3 4

А можно аргументы передавать прямо при вызове функции, а не при вызове скрипта как в примере выше:

#!/bin/bash
example () {
echo $*
shift
}

example 1 2 3 4
$ ./example.sh
1 2 3 4

Локальные переменные

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

#!/bin/bash
example () {
local ex1=1
local ex2=2
echo "$ex1, $ex2"
}

example

echo $ex1 && echo "Can't find $ex1 variable!"
$ ./example.sh
1, 2

Can't find $ex1 variable!

Математические операции в функциях

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

К примеру такая функция:

#!/bin/bash
mat () {
a=1
(( a++ ))
echo $a
}

mat

В результате получаем значение переменной $a + единица:

$ ./mat.sh
2

Более сложный вариант — с использованием нескольких переменных и вычислением их значения:

#!/bin/bash
mat () {
a=1
b=2
c=$(( a + b ))
echo $c
}

mat
$ ./mat.sh
3

Ещё вариант — с использованием аргументов:

#!/bin/bash
mat () {
a=$1
b=$2
c=$(( a + b ))
echo $c
}

mat $1 $2
$ ./mat.sh 1 1
2

Рекурсивные функции

Рекурсивная функция — функция, которая при вызове вызывает сама себя.

Например:

#!/bin/bash
recursion () {
count=$(( $count + 1 ))
echo $count
recursion
}

recursion

Такая функция будет бесконечно вызывать сама себя, пока её выполнение не будет прервано вручную:

$ ./example.sh
...
913
914
915

Для большей наглядности — добавим цикл, который проверяет условие — если переменная $count превысит значение переменной $recursions — функция остановит своё выполнение:

#!/bin/bash

count=0
recursions=4

recursion () {
count=$(( $count + 1 ))
echo $count

while [ $count -le $recursions ]
do
recursion
done
}

recursion
$ ./example.sh
1
2
3
4
5

Для упрощения скрипта — можно заменить выражение count=$(( $count + 1 )) на (( count++ )):

#!/bin/bash

count=0
recursions=4

recursion () {
(( count++ ))
echo $count

while [ $count -le $recursions ]
do
recursion
done
}

recursion
$ ./example.sh
1
2
3
4
5

Экспорт функций

Что бы передать функцию в следующий скрипт, вызываемом в новом (дочернем) экземпляре shell — её необходимо экспортировать.

Для примера возьмём два файла — в файле 1.sh мы объявим функцию, а в файле 2.sh — попробуем её выполнить:

#!/bin/bash
one () {
echo "one"
}

./2.sh
$ ./1.sh
./2.sh: line 1: one: command not found

Теперь — экспортируем функцию с помощью опции export и ключа -f:

#!/bin/bash
one () {
echo "one"
}

export -f one

./2.sh
$ ./1.sh
one

Другой вариант — вызывать следующий скрипт в том же экземпляре шела:

#!/bin/bash
one () {
echo "one"
}

source 2.sh

Или так:

#!/bin/bash
one () {
echo "one"
}

. ./2.sh

Оба варианта равнозначны и дадут один результат:

$ ./1.sh
one

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

Иногда необходимо проверить имеется ли функция, перед её выполнением. Для этого удобно использовать команду declare.

Вызванная с ключём -f и без аргументов — declare выведет описание всех имеющихся функций:

#!/bin/bash
one () {
echo "one"
}

two () {
echo "two"
}

declare -f
$ ./1.sh
one ()
{
echo "one"
}
two ()
{
echo "two"
}

С ключём -F — только названия:

#!/bin/bash
one () {
echo "one"
}

two () {
echo "two"
}

declare -F
$ ./1.sh
declare -f one
declare -f two

Если задать имена функций в качестве аргументов — declare просто выведет их мена:

#!/bin/bash
one () {
echo "one"
}

two () {
echo "two"
}

declare -F one two
$ ./1.sh
one
two

Можно задать ключ -f и имя функции, тогда будет выведено описание только указанной функции:

#!/bin/bash
one () {
echo "one"
}

two () {
echo "two"
}

declare -f one
$ ./1.sh
one () {
echo "one"
}

Проверить наличие функций перед их выполнением можно с помощью дополнительной функции, которой передаются имена проверяемых функций:

#!/bin/bash
one () {
echo "one"
}

two () {
echo "two"
}

isDefined() {
declare -f "$@" > /dev/null && echo "Functions exist" || echo "There is no some functions!"
}

isDefined one two

Обратите внимание на использование «$@» — как писалось выше, именно такой параметр выводит аргумент «как есть», то есть простым словом, без каких-либо интерпретаций bash-ем.

Запустим скрипт для проверки:

$ ./1.sh
Functions exist

А теперь — попробуем добавить одну «лишнюю» функцию:

$ cat 1.sh | grep is
isDefined() {
declare -f "$@" > /dev/null && echo "Functions exist" || echo "There is no some functions!"
isDefined one two three
$ ./1.sh
There is no some functions!

declare обнаружил отсутствие функции с именем three и вернул код 1, что вызвало срабатывание оператора ||.