BASH: використання функцій, приклади

Автор |  15/01/2023
 

terminal

Переклад поста 2013 року з деякими правками, але все ще актуальний для вивчення BASH.

По суті функція в 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]

А тепер – спробуємо додати одну “зайву” функцію до виклику isDefined() :

#!/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, що викликало спрацювання оператора ||.