Если вы работаете с Linux, в какой-то момент вы, вероятно, начнете писать скрипты Bash. Чем больше будет расти ваш код, тем больше вы поймете, насколько полезными могут быть функции Bash.

Что такое функция Bash?

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

В этом уроке мы рассмотрим определение функции Bash, и вы увидите, как использовать функции в своих скриптах для улучшения способа их написания.

Давайте начнем кодировать!

Как определить функцию в скрипте оболочки

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

function <function_name> { 
    <function_code>
}

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

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

В Bash есть также второй способ определения функций:

<function_name>() {
    <function_code>
}

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

  • Удалите слово «function» перед названием функции.
  • Добавьте скобки () после имени функции.

Вы можете выбрать понравившийся вам вариант.

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

Как вызвать функцию Bash

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

Вы можете вызвать функцию Bash, просто указав ее имя и ноль или более параметров.

Например, я определю функцию, которая выводит текущую дату, а затем вызову ее в нашем скрипте:

function show_current_date {
    date
}

show_current_date 

В коде выше я определил функцию show_current_date, которая просто вызывает команду date. Затем я вызываю ее, используя имя функции.

$./bash_function.sh 
Sat 30 Jan 2021 09:14:59 GMT 

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

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

Передача аргумента функции

Точно так же, как вы передаете аргументы в скрипт Bash, вы можете передавать аргументы в функцию.

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

В следующем примере я улучшу функцию show_current_date, чтобы она передавала один аргумент, определяющий формат даты.

function show_current_date {
    echo $1
    date $1
} 

show_current_date "+%Y-%m-%d"

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

Затем я передаю первый аргумент ($1) команде date.

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

Вывод:

$./bash_function.sh 
+%Y-%m-%d
2021-01-30 

Передача нескольких аргументов в функцию

В следующем примере мы рассмотрим, как передать несколько аргументов функции Bash.

Давайте создадим функцию, которая вычисляет сумму трех чисел:

#!/bin/bash
   
function sum_numbers() {
    echo "The numbers received are: $1 $2 $3"
    sum=$(($1+$2+$3))
    echo "The sum of the numbers is $sum"
}

sum_numbers 3 6 22 

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

$./bash_function.sh 
The numbers received are: 3 6 22
The sum of the numbers is 31 

Получить количество аргументов функции в Bash

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

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

function sum_numbers() {
    echo "The number of arguments received is: $#"
    echo "The numbers received are: $1 $2 $3"
    sum=$(($1+$2+$3))
    echo "The sum of the numbers is $sum"
} 

Вывод:

$./bash_function.sh 
The number of arguments received is: 3
The numbers received are: 3 6 22
The sum of the numbers is 31

Как получить последний аргумент функции в Bash

А что, если мы хотим получить только значение последнего аргумента?

Переменная ${@: -1} возвращает значение последнего аргумента, переданного функции.

function sum_numbers() {
    echo "The number of arguments received is: $#"
    echo "The value of the last argument is: ${@: -1}"
    echo "The numbers received are: $1 $2 $3"
    sum=$(($1+$2+$3))
    echo "The sum of the numbers is $sum"
}

Запустите скрипт после обновления функции, вы увидите следующее:

$./bash_function.sh 
The number of arguments received is: 3
The value of the last argument is: 22
The numbers received are: 3 6 22
The sum of the numbers is 31

Имеет ли это смысл?

Как обрабатывать аргументы с пробелами

Что произойдет, если один или несколько аргументов функции содержат пробелы?

Давайте выясним…

Я хочу передать строку «Bash shell» в функцию print_argument, которая выводит значение первого аргумента $1.

#!/bin/bash
   
function print_argument {
    echo $1
} 

print_argument "Bash shell" 

Чтобы убедиться, что передаваемая нами строка интерпретируется как один аргумент, я заключил ее в двойные кавычки.

$./bash_function.sh 
Bash shell 

Вывод правильный.

Как вы думаете, что произойдет, если я уберу двойные кавычки вокруг строки?

Интерпретатор Bash будет видеть Bash и shell как два отдельных параметра, переданных функции. Таким образом, функция выведет только строку Bash ($1).

Установка аргументов со значением по умолчанию

Если вы хотите иметь значения по умолчанию для аргументов функции в случае, если их значение не задано при вызове функции, вы можете сделать следующее…

${<argument_number>:-<default_value>} ${1:-test1} # For argument 1 ${2:-test2} # For argument 2 ... ${N:-testN} # For argument N

Давайте рассмотрим пример вместе…

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

#!/bin/bash
   
function print_arguments {
    ARG1=${1:-default_value1}
    ARG2=${2:-default_value2}
    ARG3=${3:-default_value3} 

    echo "The first argument is: $ARG1"
    echo "The second argument is: $ARG2"
    echo "The third argument is: $ARG3"
}

print_arguments $@ 

Используя переменную $@ в вызове функции, мы передаем функции все аргументы командной строки, переданные нашему скрипту Bash.

Пришло время протестировать эту функцию, передав скрипту другое количество аргументов:

$./default_arguments.sh 
The first argument is: default_value1
The second argument is: default_value2
The third argument is: default_value3 

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

$./default_arguments.sh
The first argument is: default_value1
The second argument is: default_value2
The third argument is: default_value3 

$./default_arguments.sh 1
The first argument is: 1
The second argument is: default_value2
The third argument is: default_value3 

$./default_arguments.sh 1 2
The first argument is: 1
The second argument is: 2
The third argument is: default_value3 

$./default_arguments.sh 1 2 3
The first argument is: 1
The second argument is: 2
The third argument is: 3

Теперь вы также знаете, как использовать значения по умолчанию в функциях Bash 🙂

Передача аргументов в функцию с использованием массива Bash

Мне было любопытно узнать, как передать аргументы функции с помощью массива Bash.

Я должен признать…

…это не самая простая задача в Bash, и я бы не советовал ее использовать, если только у вас нет других альтернатив.

Взгляните на сценарий ниже:

#!/bin/bash
   
function print_arguments {
    local arguments_array=("$@")
    echo "This is the array received by the function: ${arguments_array[@]}"

    ARG1=${arguments_array[0]}
    ARG2=${arguments_array[1]}
    ARG3=${arguments_array[2]}

    echo "The first argument is: $ARG1"
    echo "The second argument is: $ARG2"
    echo "The third argument is: $ARG3"
} 

arguments=("$@")
print_arguments "${arguments[@]}" 

Я объясню этот сценарий шаг за шагом:

  • Сгенерируйте массив из аргументов, переданных скрипту, используя arguments=("$@").
  • Передайте все элементы массива аргументов в функцию print_arguments (print_arguments "${arguments[@]}"). Чтобы лучше это понять, узнайте больше о массивах Bash.
  • В начале функции print_arguments мы создаем локальный массив, содержащий все значения, переданные в функцию.
  • Затем мы выводим все значения в массиве arguments_array, чтобы подтвердить, что аргументы в массиве были получены функцией (очень легко ошибиться при передаче переменных массива).
  • Присвойте первый, второй и третий элементы массива переменным ARG1, ARG2 и ARG3.

Попробуйте написать этот скрипт с нуля — это один из лучших способов запомнить синтаксис, который мы использовали.

А если у вас есть вопросы, пожалуйста, задайте их в комментариях ниже.

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

Как использовать $? в качестве возвращаемого значения

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

По умолчанию функция «возвращает» код выхода последнего оператора, выполненного в функции.

Код выхода сохраняется в переменной $?, и его значение можно проверить после вызова функции, например, с помощью оператора bash if else.

Чтобы увидеть это на практике, начнем с кода, аналогичного приведенному в предыдущем разделе:

function show_current_date {
    date $1
} 

show_current_date "+%Y-%m-%d"
echo $?

Обратите внимание, что мы добавили команду echo, которая выводит значение $? после вызова функции.

Вывод:

$./bash_function.sh 
2021-01-30
0 

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

Давайте посмотрим, что произойдет, если мы обновим команду date с неправильным синтаксисом (остальная часть кода не изменится):

function show_current_date {
    date - $1
}

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

$./bash_function.sh 
date: illegal time format
usage: date [-jnRu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]]... 
             [-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]
 1 

Использование оператора Return в функции Bash

Bash также предоставляет ключевое слово return.

Ключевое слово return завершает функцию и присваивает возвращаемое значение переменной $?.

function show_current_date {
    date $1
    return 3
} 

show_current_date "+%Y-%m-%d"
echo $?

Вывод подтверждает, что значение 3 присвоено $?.

$./bash_function.sh 
2021-01-30
3 

Хотите узнать, что произойдет, если вместо этого мы вернем строку?

function show_current_date {
    date $1
    return "value"
} 

Мы получаем следующую ошибку и код выхода 255.

2021-01-30
./bash_function.sh: line 5: return: value: numeric argument required
255 

Помните, что ненулевые коды возврата указывают на сбой.

Как вернуть строку из функции Bash

Существует способ вернуть строку из функции, хотя это не так просто, как в других языках программирования.

function get_operating_system {
    local result="Linux"
    echo "$result"
}

operating_system=$(get_operating_system)
echo $operating_system

Давайте разберем этот код. У нас есть…

  • Определена функция get_operating_system, которая присваивает значение локальной переменной result, а затем выводит ее значение.
  • Использовала подстановку команд для вызова функции и сохранения значения, выведенного функцией, в переменной operating_system.

Запустите этот скрипт на своем компьютере и убедитесь, что последняя команда echo скрипта выводит строку «Linux».

Теперь у вас есть несколько способов получить значение из функции 🙂

Функции Bash и глобальные переменные

Понимание того, как работает область действия переменных в Bash, важно для предотвращения потенциальных ошибок в ваших скриптах.

По умолчанию переменные в скриптах Bash являются глобальными.

Вот что я имею в виду:

#!/bin/bash
   
MESSAGE="original message" 

update_message() {
    MESSAGE="updated message"
} 

echo "Message before function call: $MESSAGE"
update_message
echo "Message after function call: $MESSAGE"  

Вывод:

Message before function call: original message
Message after function call: updated message 

Вы видите, что у нас есть:

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

Внутри функции нам удалось обновить глобальную переменную MESSAGE, это подтверждает, что переменные по умолчанию являются глобальными в Bash.

В следующем разделе мы рассмотрим, какие еще возможности существуют в отношении области действия переменных в Bash…

Локальные переменные в функциях Bash

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

…особенно если вам приходится отслеживать сотни строк кода.

Использование ключевого слова local в Bash позволяет устанавливать переменные, которые являются локальными по отношению к функции.

Давайте сделаем переменную MESSAGE локальной в функции, которую мы определили в предыдущем скрипте, и посмотрим, что произойдет:

#!/bin/bash
   
MESSAGE="original message" 

update_message() {
    local MESSAGE="updated message"
} 

echo "Message before function call: $MESSAGE"
update_message
echo "Message after function call: $MESSAGE"

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

Message before function call: original message
Message after function call: original message 

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

Ошибка «Команда не найдена» для функций Bash

В начале этого урока я упомянул, что в Bash важно определить функцию перед ее использованием.

Обратите внимание на порядок вызова функции и определения функции в приведенном ниже скрипте:

show_current_date "+%Y-%m-%d"

function show_current_date {
    echo $1
    date $1
}

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

./bash_function_order.sh: line 3: show_current_date: command not found 

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

Существует также другой тип ошибок, который вы можете увидеть при определении функций с помощью ключевого слова function (помните, что в начале этого урока мы видели, что вы можете определять функции двумя разными способами).

Использование функций с Korn Shell и C Shell

Ключевое слово function применимо не ко всем типам оболочек.

Посмотрите, что произойдет, если мы попытаемся выполнить один и тот же скрипт с помощью Korn Shell (ksh) и C Shell (csh).

Использование оболочки Korn

$ ksh bash_function_order.sh 
+%Y-%m-%d
2021-01-31 

Скрипт успешно выполнен.

Использование оболочки C

$ csh bash_function_order.sh 
function: Command not found. 

Sun 31 Jan 2021 13:41:02 GMT
}: Command not found.
show_current_date: Command not found. 

При запуске скрипта с помощью оболочки C мы получаем следующую ошибку:

function: Command not found.

Это происходит потому, что оболочка C не понимает ключевое слово function.

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

show_curent_date() {
    echo $1
    date $1
} 

show_current_date "+%Y-%m-%d" 

На этот раз мы получаем другую ошибку:

$ csh bash_function_order.sh 
Badly placed ()'s. 

Причина этих двух ошибок при использовании функции в скрипте csh заключается в том, что оболочка C не поддерживает функции.

Определить функцию в одной строке

Возьмем следующую функцию, которую мы определили в одном из разделов выше:

function show_current_date {
    echo $1
    date $1
} 

Я хочу посмотреть, сможем ли мы написать это в одну строку. Это не обязательно то, что мы сделали бы в реальном скрипте, но это полезно, чтобы понять как можно больше о том, как работает Bash.

Сначала мы просто записываем две команды в теле функции рядом друг с другом и разделяем их пробелом:

show_current_date() { echo $1 date $1 }

Функция больше не работает:

$./bash_function.sh 
./bash_function.sh: line 6: syntax error: unexpected end of file 

Почему мы видим сообщение «Bash Syntax Error Near Unexpected Token»?

Чтобы это заработало, нам нужно добавить точку с запятой после каждой команды внутри функции (включая последнюю):

show_current_date() { echo $1; date $1; } 

Обновите функцию на вашем компьютере и убедитесь, что она работает нормально.

Документация по функциям

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

Это может помочь как тому, кто написал эту функцию много лет назад, так и тому, кто совсем новичок в коде.

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

  • Что делает функция.
  • Ожидаемые аргументы и их формат (например, числа, строки, массивы).
  • Значение, которое функция возвращает основному скрипту.

Возьмем в качестве примера следующую функцию:

function get_operating_system {
    local result="Linux"
    echo "$result"
}

operating_system=$(get_operating_system)
echo $operating_system

Мы можем добавить три комментария в верхней части функции, чтобы объяснить три приведенных выше пункта.

# Description: Function that returns the current Operating System
# Arguments: No arguments
# Returns: Name of the Operating System
function get_operating_system {
....

Вы можете увидеть, как наш код становится намного проще для чтения, если просто добавить эту документацию в начало функции.

Мы также могли бы добавить четвертую команду, которая показывает, как ее вызывать…

# Usage: operating_system=$(get_operating_system)

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

Обработка ошибок функций в Bash

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

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

Два подхода к обработке ошибок в функциях:

  • Проверка количества аргументов, переданных функции, для подтверждения того, что это ожидаемое число.
  • Проверка значения переменной $? после выполнения операторов в вашей функции.

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

function sum_numbers() {
    echo "The number of arguments received is: $#"
    echo "The numbers received are: $1 $2 $3"
    sum=$(($1+$2+$3))
    echo "The sum of the numbers is $sum"
}

Во-первых, я хочу убедиться, что функция получает ровно три числа. Если это не так, выполнение нашего скрипта останавливается.

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

#!/bin/bash
   
function sum_numbers() {
    if [ $# -ne 3 ]; then
        echo "The number of arguments received is $# instead of 3"
        exit 1
    fi

    echo "The numbers received are: $1 $2 $3"
    sum=$(($1+$2+$3))
    echo "The sum of the numbers is $sum"
}

sum_numbers "$@" 

Посмотрите, что произойдет, если я запущу этот скрипт, передав ноль или три аргумента:

$./function_error_handing.sh 
The number of arguments received is 0 instead of 3

$./function_error_handing.sh 2 7 24
The numbers received are: 2 7 24
The sum of the numbers is 33 

Когда мы передаем скрипту ноль аргументов, условие оператора if в функции становится истинным, и выполнение скрипта останавливается.

Использование $? для обработки ошибок в функции

В следующем примере показано, как можно использовать переменную $? для обработки ошибок в функции.

Я создал простой скрипт, который принимает имя файла в качестве входных данных и выводит сообщение, содержащее количество строк в файле.

#!/bin/bash
   
function count_lines {
    local count=$(wc -l $1 | awk '{print $1}')
    echo "$count"
} 

number_of_lines=$(count_lines $1)
echo "The number of lines is $number_of_lines" 

Вот вывод скрипта (я создал файл с именем testfile в том же каталоге скрипта. Этот файл содержит три строки):

$./function_error_handing.sh testfile
The number of lines is 3 

Давайте посмотрим, что произойдет, если я передам имя несуществующего файла:

$./function_error_handing.sh testfile1
wc: testfile1: open: No such file or directory
The number of lines is  

Скрипт с этим не очень хорошо справляется. Он показывает ошибку от команды wc, а затем частичное сообщение из-за того, что функция возвращает пустое значение.

Это немного грязно!

Что мы можем сделать для улучшения пользовательского опыта?

Вот скрипт обновления, мы рассмотрим все обновления шаг за шагом:

#!/bin/bash
   
function count_lines {
    ls $1 > /dev/null 2>&1 

    if [ $? -ne 0 ]; then
        exit 1
    fi

    local count=$(wc -l $1 | awk '{print $1}')
    echo "$count"
} 

number_of_lines=$(count_lines $1)

if [ $? -eq 0 ]; then
    echo "The number of lines is $number_of_lines"
elif [ $? -eq 1 ]; then
    echo "Unable to detect the number of lines, the file $1 does not exist"
else
    echo "Unable to detect the number of lines"
fi 

Существует множество различных вариантов обнаружения ошибок в этом сценарии. Вот один из вариантов:

  • Используйте команду ls в начале функции, чтобы проверить, существует ли имя файла, переданное в качестве первого аргумента.
  • Скройте вывод команды ls, перенаправив стандартный вывод и стандартную ошибку в /dev/null.
  • Проверьте успешность выполнения команды ls, используя переменную $? (0 = успех, 1 = неудача).
  • В случае сбоя используйте выход 1, чтобы вернуть код выхода, который мы можем использовать в основном скрипте, чтобы предоставить пользователю сведения о типе ошибки (таким образом, код выхода 1 относится к файлу, который не существует, и мы можем использовать другие коды выхода для других типов ошибок).
  • Основной скрипт выводит правильное сообщение в зависимости от значения $?, содержащего статус оператора number_of_lines=$(count_lines $1).

Это всего лишь один из способов сделать это, главное, чтобы вы понимали ход мыслей, лежащий в основе этого кода.

Может ли функция быть пустой?

Давайте рассмотрим, что может произойти, если вы определите функцию в большом скрипте и по какой-то причине забудете реализовать тело функции.

Мы определим пустую функцию…

…функция, не содержащая никакого кода:

empty_function() {
   
} 

Я даже не вызываю эту функцию в своем скрипте.

Вот ошибка, которую мы получаем при выполнении скрипта Bash:

$./bash_function.sh 
./bash_function.sh: line 3: syntax error near unexpected token `}'
./bash_function.sh: line 3: `}' 

Просто о чем следует знать, если при работе с функциями вы видите сообщение «Bash Syntax Error Near Unexpected Token».

Заключение

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

Взгляните на свои скрипты и подумайте: «Где можно уменьшить дублирование и сложность с помощью функций?»

Written by Иван Васильков

Системный администратор и DevOps с опытом 10+ лет.