49 KiB
Функции
Bash относится к процедурным языкам программирования. Процедурные языки позволяют разделить программу на логические части — подпрограммы. Подпрограмма — это самостоятельный блок кода, который решает конкретную задачу. Подпрограммы вызываются из основной программы.
В современных языках подпрограммы называются функциями. Мы уже сталкивались с ними, когда знакомились с командой declare. Теперь рассмотрим подробнее, как функции устроены и для чего нужны.
Парадигмы программирования
Для начала разберёмся с терминологией. Она поможет понять, зачем вообще нужны функции.
Что такое процедурное программирование? Это одна из парадигм разработки ПО. Парадигма — это набор идей, методов и принципов, которые определяют способ написания программ.
Современные языки следуют одной из двух доминирующих сегодня парадигм:
-
Императивное программирование. Разработчик явно указывает машине, как ей изменять своё состояние. Другими словами он задаёт полный алгоритм вычисления результата.
-
Декларативное программирование. Разработчик указывает свойства желаемого результата, но не алгоритм его вычисления.
Bash следует первой парадигме. Это императивный язык.
Императивная и декларативная парадигмы определяют общие принципы написания программы. В рамках одной парадигмы есть различные методологии (подходы). Методология предлагает конкретные приёмы программирования. Так у императивной парадигмы есть две основных методологии:
-
Процедурное программирование.
-
Объектно-ориентированное программирование.
Эти методологии предлагают по-разному структурировать исходный код программы. Bash следует первой методологии.
Рассмотрим процедурное программирование подробнее. Процедурный язык предоставляет средства для объединения наборов команд в независимые блоки кода. Эти блоки кода называются процедурами или функциями. Функцию можно вызвать из любого места программы. На вход она принимает параметры. Этот механизм похож на передачу параметров командной строки в скрипт. Поэтому функцию иногда называют программой в программе или подпрограммой.
Основная задача функций — управление сложностью программ. Чем больше объём исходного кода, тем сложнее его сопровождать и поддерживать в рабочем состоянии. Ситуацию усугубляют повторяющиеся фрагменты кода. Они разбросаны по всей программе и могут содержать ошибки. После исправления ошибки в одном таком фрагменте, надо найти и исправить все остальные. Если фрагмент вынести в функцию, то достаточно исправить ошибку только в ней.
Рассмотрим пример повторяющегося фрагмента кода. Представьте, что вы пишете большую программу. Чтобы обработать ошибки, программа выводит в поток ошибок текстовые сообщения. Тогда в исходном коде появится много мест с вызовом команды echo. Например, таких: {line-numbers: false, format: Bash}
>&2 echo "Произошла ошибка N"
В какой-то момент вы решаете, что лучше записывать все ошибки в файл. Тогда анализировать их станет легче. Пользователи вашей программы могут перенаправить поток ошибок в лог-файл сами. Но, предположим, что не все умеют пользоваться перенаправлением. Поэтому программа должна сама писать сообщения в лог-файл.
Внесём изменение в программу. Для этого нужно пройти по всем местам обработки ошибок. Каждый вызов команды echo надо заменить на следующий: {line-numbers: false, format: Bash}
echo "Произошла ошибка N" >> debug.log
Если по невнимательности пропустить и не исправить какой-то вызов echo, его вывод не попадёт в лог-файл. Этот вывод может оказаться важным. Без него вы не поймёте, почему программа не работает у пользователя.
Мы рассмотрели одну из сложностей сопровождения программ. Она часто встречается при изменении кода, написанного ранее. В нашем примере проблема возникла из-за нарушения принципа разработки "не повторяйся" (don’t repeat yourself или DRY). Один и тот же код вывода ошибок копировался снова и снова в разные места программы. Так делать нельзя.
Функции решают проблему дублирования кода. Чем-то это решение напоминает циклы. Отличие в том, что цикл многократно исполняет набор команд в одном месте программы. В отличие от цикла функция позволяет исполнять одни и те же команды в разных местах программы.
Функция улучшит читаемость кода программы. Она объединяет набор команд в единый блок. Если дать блоку говорящее имя, станет очевидна решаемая им задача. В программе функция вызывается по своему имени. Благодаря этому, программу станет легче читать. Вместо десятка строк тела функции, будет стоять её имя. Оно объяснит читателю, что происходит в функции.
Функции в командном интерпретаторе
Функции доступны в обоих режимах Bash: командный интерпретатор и исполнение скриптов. Начнём с командного интерпретатора.
В общем виде функция объявляется так: {line-numbers: true, format: Bash}
ИМЯ_ФУНКЦИИ()
{
ДЕЙСТВИЕ
}
В одну строку функцию можно объявить так: {line-numbers: false, format: Bash}
ИМЯ_ФУНКЦИИ() { ДЕЙСТВИЕ ; }
Обратите внимание на обязательную точку с запятой перед закрывающей фигурной скобкой }.
Тело функции ДЕЙСТВИЕ может быть одной командой или блоком команд.
На имена функций в Bash накладываются те же ограничения, что и на имена переменных. В них допустимы только символы латинского алфавита, числа и знак подчёркивания _. Имя не должно начинаться с числа.
Рассмотрим, как объявлять и использовать функции в командном интерпретаторе. Предположим, вам нужна статистика использования оперативной памяти. Эта информация доступна через файловую систему proc или procfs. Через proc можно узнать список работающих процессов, состояние ОС и оборудования компьютера. Эта информация доступна через файлы, находящихся по системному пути /proc.
Статистика использования оперативной памяти доступна в файле /proc/meminfo. Прочитаем его с помощью утилиты cat:
{line-numbers: false, format: Bash}
cat /proc/meminfo
Вывод команды зависит от вашей системы. Для окружения MSYS2 он даст меньше информации, для Linux-системы — больше.
Для MSYS2 содержимое файла meminfo будет примерно таким:
{line-numbers: true, format: Bash}
MemTotal: 6811124 kB
MemFree: 3550692 kB
HighTotal: 0 kB
HighFree: 0 kB
LowTotal: 6811124 kB
LowFree: 3550692 kB
SwapTotal: 1769472 kB
SwapFree: 1636168 kB
Таблица 3-22 объясняет значение каждого поля.
{caption: "Таблица 3-22. Поля в файле meminfo", width: "100%"}
| Поле | Значение |
|---|---|
| MemTotal | Объём доступной в системе RAM. |
| MemFree | Объём не используемой в данный момент RAM. Считается как LowFree + HighFree. |
| HighTotal | Объём доступной памяти в области RAM выше 860 мегабайтов. |
| HighFree | Объём не используемой памяти в области RAM выше 860 мегабайтов. |
| LowTotal | Объём доступной памяти в области RAM ниже 860 мегабайтов. |
| LowFree | Объём не используемой памяти в области RAM ниже 860 мегабайтов. |
| SwapTotal | Объём доступной памяти в области подкачки на жёстком диске. |
| SwapFree | Объём не используемой памяти в области подкачки. |
Подробнее значения полей файла meminfo рассматриваются в статье.
Чтобы не набирать команду чтения файла meminfo каждый раз, объявим функцию с коротким именем. Например, так:
{line-numbers: false, format: Bash}
mem() { cat /proc/meminfo; }
Это однострочное объявление функции с именем mem. Её можно вызвать так же, как любую Bash-команду. Например:
{line-numbers: false, format: Bash}
mem
Функция выведет статистику использования памяти.
Команда unset удаляет объявленную ранее функцию. Удалим нашу функцию mem следующей командой:
{line-numbers: false, format: Bash}
unset mem
Предположим, что переменная и функция объявлены с одинаковыми именами. Чтобы удалить именно функцию, используйте опцию -f команды unset. Например, так:
{line-numbers: false, format: Bash}
unset -f mem
Объявление функции можно добавить в файл ~/.bashrc. Тогда функция будет доступна при каждом запуске командной оболочки.
В командной строке мы объявили функцию mem в однострочном формате. Его удобнее и быстрее набирать. В файле ~/.bashrc важна наглядность. Там функцию mem лучше объявить в стандартном виде. Например, так:
{line-numbers: true, format: Bash}
mem()
{
cat /proc/meminfo
}
Отличие функций от псевдонимов
Мы объявили функцию mem для вывода статистики использования оперативной памяти. То же поведение даст следующий псевдоним:
{line-numbers: false, format: Bash}
alias mem="cat /proc/meminfo"
Если функции и псевдонимы работают одинаково, что выбрать?
Функции и псевдонимы похожи в одном — это встроенные механизмы Bash. С точки зрения пользователя они сокращают ввод длинных команд. Но принцип работы этих механизмов принципиально различается.
Псевдоним заменяет один текст на другой во введённой пользователем команде. Другими словами Bash находит в команде текст, который совпадает с именем alias. Затем заменяет этот текст на значение псевдонима и исполняет получившуюся команду.
Предположим, вы определили псевдоним для утилиты cat. Он добавляет опцию -n в вызов утилиты. Благодаря опции, в вывод добавляются номера строк. Псевдоним выглядит так:
{line-numbers: false, format: Bash}
alias cat="cat -n"
Теперь каждый раз когда в команде встречается слово "cat", Bash подставит вместо него "cat -n". Например, вы вводите команду: {line-numbers: false, format: Bash}
cat ~/.bashrc
После подстановки псевдонима она выглядит так: {line-numbers: false, format: Bash}
cat -n ~/.bashrc
Подстановка заменила только слово "cat" на "cat -n". Следующий далее путь до файла не изменился.
Теперь рассмотрим, как работают функции. В отличие от псевдонима тело функции не подставляется в команду. Когда Bash встречает имя функции в команде, он исполняет её тело.
Пример. Попробуем с помощью функции получить то же поведение, как у псевдонима для утилиты cat. Если бы функции работали как alias, такое определение решило бы задачу: {line-numbers: false, format: Bash}
cat() { cat -n; }
Тогда в следующей команде Bash просто добавит опцию -n.
{line-numbers: false, format: Bash}
cat ~/.bashrc
Но это не сработает. Bash не подставляет тело функции в команду. Bash его исполняет и подставляет в команду результат.
В нашем случае утилита cat будет вызвана с опцией -n, но без параметра ~/.bashrc. Это совершенно не то что нужно.
Чтобы решить задачу, передадим имя файла в функцию в качестве параметра. Это работает так же, как передача параметра в команду или скрипт. После вызова функции укажите список параметров, разделённых пробелами.
В общем виде вызов функции и передача в неё параметров выглядит так: {line-numbers: false, format: Bash}
ИМЯ_ФУНКЦИИ ПАРАМЕТР1 ПАРАМЕТР2 ПАРАМЕТР3
Чтобы прочитать параметры в теле функции, используйте переменные $1, $2, $3 и т.д. Прочитать сразу все параметры можно через переменную $@.
Исправим объявление функции cat. Все её входные параметры передадим в утилиту cat:
{line-numbers: false, format: Bash}
cat() { cat -n $@; }
Такая функция тоже не заработает. Дело в том, что при её выполнении произойдёт рекурсия. Рекурсией называется вызов функции из неё же самой.
Перед выполнением команды "cat -n $@" Bash проверит список объявленных функций. В списке будет функция с именем cat. Её тело выполняется в данный момент, но это не важно. Поэтому вместо вызова утилиты Bash вызовет функцию cat. Этот вызов повторится снова и снова. Возникнет бесконечная рекурсия, которая похожа на бесконечный цикл.
Рекурсия — вовсе не ошибка в поведении интерпретатора. Это мощный механизм, который значительно упрощает сложные алгоритмы (например, обход графа или дерева).
Ошибка в нашем объявлении функции cat. Рекурсивный вызов произошел случайно и привел к зацикливанию. Решить эту проблему можно двумя способами:
-
Использовать встроенную команду command.
-
Переименовать функцию так, чтобы её имя отличалось от имени утилиты.
Рассмотрим первое решение. В качестве параметров command получает команду. Если в команде встречаются имена псевдонимов и функций, Bash не станет их обрабатывать. Тело псевдонима не подставится. Функция не вызовется.
Применим команду command в объявлении функции cat. Получим следующее:
{line-numbers: false, format: Bash}
cat() { command cat -n "$@"; }
Второе решение — просто переименовать функцию. Такой вариант сработает: {line-numbers: false, format: Bash}
cat_func() { cat -n "$@"; }
Всегда помните о проблеме случайной рекурсии. Не давайте функциям имена, совпадающие с именами команд интерпретатора и GNU-утилит.
Подведём итоги сравнения функций и псевдонимов в командном интерпретаторе. Если нужно просто сократить длинную команду, используйте alias.
Функция нужна только в следующих случаях:
-
Для выполнения действия нужны условные операторы, циклы или блок команд.
-
Параметры команды находятся не в конце.
Рассмотрим пример второго случая — команду, которую нельзя заменить псевдонимом. Сократим вызов утилиты find для поиска файлов в указанном каталоге. Поиск в домашнем каталоге выглядит так: {line-numbers: false, format: Bash}
find ~ -type f
С помощью псевдонима для этой команды параметризовать путь не получится. Следующий вариант не заработает: {line-numbers: false, format: Bash}
alias="find -type f"
Проблема в том, что путь должен идти до опции -type.
Заменим псевдоним на функцию. В её теле можно выбрать позицию для подстановки параметра в вызов find. Например, так: {line-numbers: false, format: Bash}
find_func() { find $1 -type f; }
Функции в скриптах
В скриптах функции объявляются точно так же, как в командном интерпретаторе. Допускаются оба варианта объявления: стандартный и однострочный.
Для примера вернёмся к проблеме обработки ошибок в большой программе. Объявим следующую функцию для вывода сообщений об ошибках: {line-numbers: true, format: Bash}
print_error()
{
>&2 echo "Произошла ошибка: $@"
}
Текст, объясняющий причину ошибки, передаётся в функцию через параметр. Допустим, наша программа читает файл на диске. Но файл оказался недоступен. Тогда сообщить о проблеме можно так: {line-numbers: false, format: Bash}
print_error "файл readme.txt не найден"
Предположим, что требования к программе изменились. Теперь сообщения об ошибках нужно выводить в лог-файл. Для этого достаточно исправить объявление функции print_error. Команда echo изменится на следующую:
{line-numbers: true, format: Bash}
print_error()
{
echo "Произошла ошибка: $@" >> debug.log
}
После изменения функции все сообщения об ошибках выводятся в файл debug.log. Менять что-либо в местах вызова функции не нужно.
Встречаются ситуация, когда одна функция должна вызвать другую. Это допустимо в Bash. В общем случае функцию можно вызвать из любого места программы.
Рассмотрим пример. Предположим, интерфейс программы надо перевести на другой язык. Такая процедура называется локализацией. Сообщения об ошибках лучше выводить на понятном пользователю языке. Для этого продублируем текст всех сообщений на всех языках, поддерживаемых программой. Как это сделать?
Самое простое решение — присвоить каждой ошибке уникальный код. Такая практика часто встречается в системном программировании. Применим этот подход в нашей программе. Тогда функция print_error в качестве параметра будет принимать код ошибки.
Код ошибки можно выводить прямо в лог-файл. Но тогда пользователю понадобится информация о значениях кодов. Удобнее выводить текст сообщения, как и раньше. Для этого код ошибки надо конвертировать в текст на нужном языке. Для этой задачи объявим специальную функцию.
Напишем функцию для конвертирования кода ошибки в сообщение. Для конвертирования применим конструкцию case. Каждый блок case соответствует определённому коду ошибки. Объявление функции выглядит так: {line-numbers: true, format: Bash}
code_to_error()
{
case $1 in
1)
echo "Не найден файл"
;;
2)
echo "Нет прав для чтения файла"
;;
esac
}
Теперь перепишем объявление функции print_error так:
{line-numbers: true, format: Bash}
print_error()
{
echo "$(code_to_error $1) $2" >> debug.log
}
Вызов функции print_error выглядит, например, так:
{line-numbers: false, format: Bash}
print_error 1 "readme.txt"
В результате вызова в лог-файл запишется строка: {line-numbers: false, format: text}
Не найден файл readme.txt
Первым параметром в функцию передаётся код ошибки. Вторым параметром — имя файла, который привёл к проблеме.
Сопровождать механизм вывода сообщений об ошибках в нашей программе стало проще. Предположим, надо добавить вывод ошибок на другом языке. Для этого достаточно объявить две функции:
-
code_to_error_ruдля сообщений на русском. -
code_to_error_enдля сообщений на английском.
Чтобы выбрать правильную функцию, можно проверить значение переменной LANGUAGE в функции print_error.
I> Если переменная LANGUAGE недоступна в вашей системе, используйте переменную LANG.
Наше решение с конвертированием кода ошибок — это учебный пример. Для локализации скриптов у Bash есть специальный механизм. В нём используются PO-файлы с текстами на разных языках. Подробнее об этом механизме читайте в статье BashFAQ.
{caption: "Упражнение 3-13. Использование функций", format: text, line-numbers: false}
Для вывода сообщений об ошибках на русском и английском языках напишите следующие функции:
* print_error
* code_to_error_ru
* code_to_error_en
Реализуйте функции code_to_error двумя способами:
* с конструкцией case.
* с ассоциативным массивом.
Возврат значения из функции
Чтобы вернуть значение из функции, процедурные языки имеют встроенную команду. Обычно она называется return. В Bash эта команда тоже есть. Но её поведение отличается. Команда return в Bash не возвращает значение. Она передаёт код возврата, то есть целое число от 0 до 255.
Полный алгоритм вызова и выполнения функции выглядит так:
-
При выполнении команды встречается имя функции.
-
Интерпретатор переходит в тело функции и исполняет его с первой команды.
-
Если в теле функции встречается команда return, выполнение функции прекращается. Bash переходит в место её вызова. В специальный параметр
$?записывается код возврата функции. Это параметр команды return. -
Если в теле функции нет return, Bash выполняет его до последней команды. После этого интерпретатор переходит в место вызова функции.
В других процедурных языках команда return возвращает переменную любого типа: число, строку или массив. Такое же поведение можно получить и в Bash. Для этого есть три способа:
-
Подстановка команд.
-
Глобальная переменная.
-
Вызывающая сторона указывает глобальную переменную.
Рассмотрим пример каждого из трёх способов.
В прошлом разделе мы написали функции code_to_error и print_error для вывода сообщений об ошибках. Они выглядят так:
{line-numbers: true, format: Bash}
code_to_error()
{
case $1 in
1)
echo "Не найден файл"
;;
2)
echo "Нет прав для чтения файла"
;;
esac
}
print_error()
{
echo "$(code_to_error $1) $2" >> debug.log
}
Здесь работает первый способ возврата значения. Вызов функции code_to_error помещается в подстановку команды. Благодаря этому, Bash подставит в место вызова функции всё, что она выведет на консоль.
В нашем примере функция code_to_error выводит сообщение об ошибке через команду echo. Далее Bash подставляет этот вывод в тело функции print_error. В результате получается команда echo, состоящая из двух частей:
-
Вывод функции
code_to_error. Это сообщение об ошибке. -
Входной параметр
$2функцииprint_error. Это имя файла, доступ к которому вызвал ошибку.
Составная команда в функции print_error выводит полное сообщение об ошибке в лог-файл.
Второй способ вернуть значение из функции — записать его в глобальную переменную. Такая переменная доступна в любом месте скрипта. То есть и в теле функции, и в месте её вызова.
I> Все объявленные в скрипте переменные являются глобальными по умолчанию. У этого правила есть одно исключение. Его мы рассмотрим далее.
Перепишем функции code_to_error и print_error. Сохраним результат code_to_error в глобальной переменной. Затем прочитаем эту переменную в функции print_error. Получится следующее:
{line-numbers: true, format: Bash}
code_to_error()
{
case $1 in
1)
error_text="Не найден файл"
;;
2)
error_text="Нет прав для чтения файла"
;;
esac
}
print_error()
{
code_to_error $1
echo "$error_text $2" >> debug.log
}
Результат функции code_to_error записывается в переменную error_text. Затем значения параметра $2 и error_text подставляются в команду echo в функции print_error. Так получается сообщение для вывода в лог-файл.
Возвращать значение из функции через глобальную переменную опасно. Это чревато конфликтом имён. Для примера предположим, что в скрипте есть другая переменная error_text. Она никак не связана с выводом в лог-файл. Тогда любой вызов функции code_to_error перезапишет значение этой переменной. Это приведёт к ошибкам во всех местах использования error_text вне функции.
Решить проблему конфликта имён поможет соглашение об именовании переменных. Такое соглашение — это один из пунктов стандарта оформления кода (coding style). Любой крупный программный проект должен иметь такой стандарт.
Вот пример соглашения об именовании переменных:
- Все глобальные переменные, через которые функции возвращают значения, имеют префикс знак подчёркивания _.
Будем следовать этому соглашению. Тогда переменная для возврата значения из функции code_to_error должна называться _error_text. Проблема решится, но лишь отчасти. Предположим, одна функция вызывает другую (вложенный вызов). Случайно они возвращают свои значения через переменные с одинаковыми именами. Это приведёт к ошибке.
Третий способ возврата значения из функции решает проблему конфликта имён. Вызывающая сторона задаёт имя глобальной переменной. Функция записывает свой результат в переменную с этим именем.
Как работает передача имени переменной в функцию? Имя передаётся через параметр функции, как и любое другое значение. Дальше, функция использует команду eval. Эта команда конвертирует текст в Bash-команду. Имя переменной хранится в виде текста. Поэтому без eval обратиться к переменной не получится.
Перепишем функцию code_to_error. Вместо одного параметра будем передавать в неё два:
-
Код ошибки в
$1. -
Имя глобальной переменной для возврата значения в
$2.
Получится такой код: {line-numbers: true, format: Bash}
code_to_error()
{
local _result_variable=$2
case $1 in
1)
eval $_result_variable="'Не найден файл'"
;;
2)
eval $_result_variable="'Нет прав для чтения файла'"
;;
esac
}
print_error()
{
code_to_error $1 "error_text"
echo "$error_text $2" >> debug.log
}
На первый взгляд код мало отличается от предыдущего варианта. Но это не так. Мы получили дополнительную гибкость поведения. Теперь вызывающая сторона выбирает имя глобальной переменной для возврата значения. Это имя явно указывается в коде вызова. Поэтому обнаружить конфликт и решить его стало проще.
Область видимости переменных
Конфликт имён — это серьёзная проблема. Она возникает в Bash, когда функции объявляют переменные в глобальном пространстве имён. В результате имена двух переменных могу совпасть. Тогда к ним обращаются разные функции в разные моменты времени. Это приводит к путанице и потере данных.
Чтобы решить конфликт имён в Bash, ограничивайте область видимости переменных. Рассмотрим этот механизм подробнее.
Если объявить переменную с ключевым словом local, её область видимости ограничится телом функции. Другими словами, переменная будет доступна только в теле функции.
Наш последний вариант функции code_to_error выглядит так:
{line-numbers: true, format: Bash}
code_to_error()
{
local _result_variable=$2
case $1 in
1)
eval $_result_variable="'Не найден файл'"
;;
2)
eval $_result_variable="'Нет прав для чтения файла'"
;;
esac
}
Здесь переменная _result_variable объявлена как локальная. Это значит, что она доступна для чтения и записи только в теле code_to_error и любых вызываемых ею функциях.
В Bash область видимости локальной переменной ограничена временем исполнения функции, в которой она объявлена. Такая область видимости называется динамической. В современных языках чаще встречается лексическая область видимости. При этом подходе переменная доступна только в теле функции, но не за его пределами (например, в вызываемых функциях).
Локальные переменные не попадают в глобальную область видимости. Это гарантирует, что никакая функция не перезапишет их случайно.
{caption: "Упражнение 3-14. Область видимости переменных", format: text, line-numbers: false}
Какой текст выведет на консоль скрипт из листинга 3-37 после выполнения?
{caption: "Листинг 3-37. Скрипт для тестирования области видимости переменной", line-numbers: true, format: Bash}
Неосторожное обращение с локальными переменными приводит к ошибкам. Проблема в том, что локальная переменная скрывает глобальную с тем же именем. Рассмотрим пример.
Предположим, вы пишете функцию для обработки файла. Например, она с помощью утилиты grep ищет шаблон в файле. Функция выглядит так: {line-numbers: true, format: Bash}
check_license()
{
local filename="$1"
grep "General Public License" "$filename"
}
Теперь допустим, что в начале скрипта объявлена глобальная переменная с именем filename. Например:
{line-numbers: true, format: Bash}
#!/bin/bash
filename="$1"
Выполнится ли функция check_license корректно? Да выполнится, благодаря сокрытию глобальной переменной. Сокрытие работает так. При обращении к имени filename в теле функции Bash подставит локальную переменную, а не глобальную. Это происходит потому, что локальная переменная объявлена позже глобальной. Из-за сокрытия в теле функции нельзя получить доступ к глобальной переменной filename.
Случайное сокрытие переменных приводит к ошибкам. Старайтесь исключить саму возможность такой ситуации. Для этого добавляйте префикс или постфикс для имён локальных переменных. Например, символ подчёркивания в конец имени.
Глобальная переменная становится недоступна в теле функции только после объявления локальной переменной с тем же именем. Рассмотрим следующий вариант функции check_license:
{line-numbers: true, format: Bash}
#!/bin/bash
filename="$1"
check_license()
{
local filename="$filename"
grep "General Public License" "$filename"
}
Здесь локальная переменная filename инициализируется значением глобальной переменной с тем же именем. Причина в том, что подстановка переменных выполняется до операции присваивания. То есть в момент присваивания подставляется значение параметра скрипта $1. Например, если в скрипт передать имя файла README, то присваивание выглядит так:
{line-numbers: false, format: Bash}
local filename="README"
В Bash начиная с версии 4.2 изменилось ограничение области видимости массивов. Если объявить индексируемый и ассоциативный массив в функции, он по умолчанию попадёт в локальную область видимости. Чтобы объявить массив глобальным, используйте опцию -g команды declare.
Вот пример объявления локального массива files:
{line-numbers: true, format: Bash}
check_license()
{
declare files=(Documents/*.txt)
grep "General Public License" "$files"
}
В следующем примере массив files попадёт в глобальную область видимости:
{line-numbers: true, format: Bash}
check_license()
{
declare -g files=(Documents/*.txt)
grep "General Public License" "$files"
}
Мы познакомились с функциями в Bash. Вот общие рекомендации по их использованию:
-
Тщательно выбирайте имена для функций. Каждое имя сообщает читателю кода, что делает функция.
-
В функциях объявляйте только локальные переменные. Используйте соглашение об их именовании. Так вы решите конфликт имён локальных и глобальных переменных.
-
Не используйте глобальные переменные в функциях. Вместо этого передавайте значение глобальной переменной в функцию через параметр.
-
Не используйте ключевое слово function при объявлении переменных. Оно есть в Bash, но отсутствует в POSIX-стандарте.
Рассмотрим подробнее последний совет. Следующий вариант объявления функции не рекомендуется: {line-numbers: true, format: Bash}
function check_license()
{
declare files=(Documents/*.txt)
grep "General Public License" "$files"
}
Ключевое слово function полезно только в одном случае. Оно решает конфликт между именем функции и псевдонимом (alias).
Например, следующее объявление функции не заработает без слова function: {line-numbers: true, format: Bash}
alias check_license="grep 'General Public License'"
function check_license()
{
declare files=(Documents/*.txt)
grep "General Public License" "$files"
}
После такого объявления функцию можно вызвать, если поставить перед ней слэш . Например, так: {line-numbers: false, format: Bash}
\check_license
Без слэша Bash подставит значение псевдонима: {line-numbers: false, format: Bash}
check_license
В скриптах имена псевдонимов и функций конфликтуют редко. Каждый скрипт запускается в новом процессе Bash. В нём нет пользовательских alias из файла .bashrc. Конфликт имён может произойти по ошибке в режиме командного интерпретатора.