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

Автор: | 09/11/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

[simterm]

$ ./example.sh
One

[/simterm]

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

#!/bin/bash

function one {
  echo "One"
}

one

two

function two {
  echo "Two"
}

[simterm]

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

[/simterm]

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

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

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

#!/bin/bash

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

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

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

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

Проверим:

[simterm]

$ bash test.sh 
Run application? (Yes/No) 
y

Run

[/simterm]

С ответом No:

[simterm]

$ ./example.sh

Run application? (Yes/No)
no

Not run

[/simterm]

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

#!/bin/bash

answer () {
  while read response; do
    echo
      case $response in
        [yY][eE][sS]|[yY])
          printf "$1\n"
          return 0
          break
          ;;
        [nN][oO]|[nN])
          printf "$2\n"
          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":

[simterm]

$ bash test.sh 

Run application? (Yes/No) 
y

Run
I'm script

$ bash test.sh 

Run application? (Yes/No) 
no

Will not run
Doing nothing

[/simterm]

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

#!/bin/bash

answer () {
  while read response; do
    echo
      case $response in
        [yY][eE][sS]|[yY])
          printf "$1\n"
          return 0
          break
          ;;
        [nN][oO]|[nN])
          printf "$2\n"
          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" && pkill top || echo "Doing nothing"

[simterm]

$ ./example.sh

Kill TOP application? (Yes/No)
y

Killing TOP

[/simterm]

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

[simterm]

$ ./example.sh

Kill TOP application? (Yes/No)
y

Killing TOP
Doing nothing

[/simterm]

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

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

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

#!/bin/bash

answer () {
  while read response; do
    echo
      case $response in
        [yY][eE][sS]|[yY])
          printf "$1\n"
          return 0
          break
          ;;
        [nN][oO]|[nN])
          printf "$2\n"
          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"

[simterm]

$ ./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

[/simterm]

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

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

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

#!/bin/bash

example () {
  echo $#
  shift
}

example $*

[simterm]

$ ./example.sh 1 2 3 4
4

[/simterm]

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

#!/bin/bash

example () {
  echo $*
  shift
}

example $*

[simterm]

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

[/simterm]

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

#!/bin/bash

example () {
  echo $*
  shift
}

example 1 2 3 4

[simterm]

$ ./example.sh
1 2 3 4

[/simterm]

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

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

Пример:

#!/bin/bash

ex0=0

example () {
  local ex1=1

  echo "$ex1"
}

example

[[ $ex0 ]] && echo "Variable found" || echo "Can't find variable!"
[[ $ex1 ]] && echo "Variable found" || echo "Can't find variable!"

Проверяем:

[simterm]

$ bash test.sh
1
Variable found
Can't find variable!

[/simterm]

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

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

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

#!/bin/bash

mat () {
  a=1
  (( a++ ))
  echo $a
}

mat

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

[simterm]

$ ./mat.sh
2

[/simterm]

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

#!/bin/bash

mat () {
  a=1
  b=2
  c=$(( a + b ))
  echo $c
}

mat

Результат:

[simterm]

$ ./mat.sh
3

[/simterm]

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

#!/bin/bash

mat () {
  a=$1
  b=$2
  c=$(( a + b ))
  echo $c
}

mat $1 $2

Выполняем:

[simterm]

$ ./mat.sh 1 1
2

[/simterm]

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

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

Например:

#!/bin/bash

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

recursion

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

[simterm]

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

[/simterm]

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

#!/bin/bash

count=0
recursions=4

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

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

recursion

Выполнение:

[simterm]

$ ./example.sh
1
2
3
4
5

[/simterm]

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

#!/bin/bash

count=0
recursions=4

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

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

recursion

Проверяем:

[simterm]

$ ./example.sh
1
2
3
4
5

[/simterm]

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

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

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

#!/bin/bash

one () {
  echo "one"
}

bash 2.sh

А в файле 2.sh – попробуем эту функцию вызвать:

#!/bin/bash

one

Проверяем:

[simterm]

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

[/simterm]

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

#!/bin/bash

one () {
  echo "one"
}

export -f one

bash 2.sh

Выполняем:

[simterm]

$ ./1.sh 
one

[/simterm]

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

#!/bin/bash

one () {
  echo "one"
}

source 2.sh

Или так:

#!/bin/bash

one () {
  echo "one"
}

. 2.sh

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

[simterm]

$ ./1.sh
one

[/simterm]

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

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

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

#!/bin/bash

one () {
  echo "one"
}

two () {
  echo "two"
}

declare -f

Результат:

[simterm]

$ ./test.sh 
one () 
{ 
    echo "one"
}
two () 
{ 
    echo "two"
}

[/simterm]

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

#!/bin/bash

one () {
  echo "one"
}

two () {
  echo "two"
}

declare -F

И:

[simterm]

$ ./test.sh
declare -f one
declare -f two

[/simterm]

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

#!/bin/bash

one () {
  echo "one"
}

two () {
  echo "two"
}

declare -F one two

Проверяем:

[simterm]

$ ./test.sh
one
two

[/simterm]

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

#!/bin/bash

one () {
  echo "one"
}

two () {
  echo "two"
}

declare -f one

Запускаем:

[simterm]

$ ./test.sh
one () {
echo "one"
}

[/simterm]

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

#!/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-ем.

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

[simterm]

$ ./test.sh
Functions exist

[/simterm]

А теперь – попробуем добавить одну “лишнюю” функцию:

#!/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 three

Результат:

[simterm]

$ ./test.sh
There is no some functions!

[/simterm]

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