Проектирование функций с локальной областью и кодами возврата
Пишите функции с локальными переменными, статусами завершения и возвращаемыми значениями на основе printf вместо ненадёжных глобальных переменных.
«Проектирование функций с локальной областью и кодами возврата» — бесплатный урок DevOps Bootcamp на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DevOps Bootcamp, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DevOps Bootcamp содержит 4 уроков всего.
Почему важна область видимости функции
В Bash переменные по умолчанию глобальны. Переменная, заданная внутри функции, выходит в область видимости вызывающего кода, если явно не объявить её с помощью local. Это распространённый источник трудно обнаружимых ошибок в сценариях оболочки.
- Функции без
localмогут незаметно перезаписывать переменные вызывающего кода. - Переменные, объявленные через
local, уничтожаются после возврата функции. - Чёткие границы области видимости делают функции повторно используемыми и пригодными для изолированного тестирования.
В этом уроке Вы научитесь писать самодостаточные функции: они используют переменные local, передают результаты через коды завершения и printf и никогда не зависят от неявного глобального состояния.
Проблема утечки глобальных переменных
Вот конкретный пример утечки глобальной переменной. Функция set_name задаёт переменную с именем result, незаметно перезаписывая собственную переменную result вызывающего кода.
Запустите этот скрипт и обратите внимание на неожиданный результат — после вызова функции переменная result вызывающего кода исчезает.
#!/usr/bin/env bash
set_name() {
result="Alice" # No 'local' — this is GLOBAL
}
result="important data"
echo "Before: $result"
set_name
echo "After: $result" # Prints 'Alice', not 'important data'Объявление локальных переменных с помощью 'local'
Встроенная команда local ограничивает область видимости переменной объемлющей функцией и всеми функциями, которые она вызывает. За пределами функции переменная либо не задана, либо сохраняет предыдущее значение.
local varname— объявляет переменную без присваивания значения.local varname="value"— объявляет переменную и присваивает ей значение за один шаг.local -i count=0— объявляет локальную переменную целочисленного типа.local -r PI=3.14159— объявляет локальную константу, доступную только для чтения.
Рекомендуемый подход: объявляйте каждую переменную внутри функции как local, если только Вам намеренно не требуется сделать её глобальной.
#!/usr/bin/env bash
greet() {
local name="$1" # local — safe
local greeting="Hello, ${name}!"
echo "$greeting"
} # 'name' and 'greeting' vanish here
name="global value"
greet "Bob"
echo "name is still: $name" # Prints 'global value'Коды завершения как возвращаемые значения
Функции Bash не могут возвращать строки с помощью return — команда return задаёт только целочисленный статус завершения (0–255). По соглашению:
return 0— успехreturn 1(или любое ненулевое значение) — ошибка
Вызывающий код получает статус завершения через $? сразу после вызова или использует функцию непосредственно в условии if. Коды завершения — стандартный способ сообщить об успехе или ошибке функции.
#!/usr/bin/env bash
is_even() {
local -i n="$1"
(( n % 2 == 0 )) # arithmetic command: exits 0 if true, 1 if false
}
for num in 2 3 7 10; do
if is_even "$num"; then
echo "$num is even"
else
echo "$num is odd"
fi
doneПередача строковых результатов через printf
Когда функция должна вернуть строковый результат, обычно его выводят в стандартный вывод и перехватывают с помощью подстановки команды $(). Команда printf предпочтительнее echo, потому что:
printfпо умолчанию не добавляет завершающий перевод строки (если только Вы не укажете\n).- Поведение
printfединообразно и определено стандартом POSIX, аechoведёт себя по-разному в разных оболочках. - Подстановка команды удаляет завершающие переводы строк, поэтому
printf '%s' "$value"обеспечивает точный результат.
#!/usr/bin/env bash
to_uppercase() {
local input="$1"
printf '%s' "${input^^}" # Bash 4+ parameter expansion
}
word="hello"
upper=$(to_uppercase "$word")
echo "Original: $word"
echo "Upper: $upper"Сочетание кодов завершения и вывода в stdout
Хорошо спроектированная функция может одновременно вывести результат (при успехе) и сообщить об ошибке (с помощью кода завершения). Вызывающий код сначала проверяет код завершения и только после этого доверяет выводу.
Приведённый ниже шаблон широко используется в настоящих библиотеках Bash:
- При успехе: вывести результат с помощью
printfи выполнитьreturn 0. - При ошибке: записать диагностическое сообщение в stderr (а не в stdout) и выполнить
return 1. - Запись ошибок в stderr оставляет stdout чистым для передачи данных по конвейеру.
#!/usr/bin/env bash
divide() {
local -i numerator="$1"
local -i denominator="$2"
if (( denominator == 0 )); then
printf 'Error: division by zero\n' >&2
return 1
fi
printf '%d' $(( numerator / denominator ))
return 0
}
if result=$(divide 20 4); then
echo "20 / 4 = $result"
else
echo "Division failed."
fi
if result=$(divide 10 0); then
echo "10 / 0 = $result"
else
echo "Division failed (caught the error)."
fiИспользование 'local' для защиты рекурсивных функций
Рекурсия — один из самых наглядных примеров того, почему local необходима. Каждый рекурсивный вызов получает собственную независимую копию каждой local-переменной в стеке вызовов. Без local каждый вызов перезаписывал бы одну и ту же глобальную переменную, что приводило бы к неправильным результатам.
Приведённая ниже функция вычисления факториала безопасна, потому что n и sub являются локальными для каждого кадра стека.
#!/usr/bin/env bash
factorial() {
local -i n="$1"
local -i sub
if (( n <= 1 )); then
printf '1'
return 0
fi
sub=$(factorial $(( n - 1 )))
printf '%d' $(( n * sub ))
}
for i in 1 2 3 4 5 6; do
echo "${i}! = $(factorial $i)"
doneКак избежать ловушки подоболочки с помощью local -n (ссылки на переменные)
Подстановка команды $() выполняется в подоболочке. Любые присваивания переменным внутри неё невидимы для родительской оболочки. Если Вам нужно, чтобы функция записывала значение в переменную, предоставленную вызывающим кодом, без подоболочки, используйте ссылку на переменную (local -n), доступную в Bash 4.3 и более новых версиях.
local -n ref="$1"делаетrefпсевдонимом переменной, имя которой хранится в$1.- Присваивание значения
refвнутри функции напрямую изменяет переменную вызывающего кода. - Это позволяет избежать подоболочки, сохраняя детали реализации локальными.
#!/usr/bin/env bash
# Fills caller's array by reference — no subshell needed
read_csv_line() {
local -n _out="$1" # nameref to caller's variable
local line="$2"
local IFS=','
read -ra _out <<< "$line"
}
declare -a fields
read_csv_line fields "alice,30,engineer"
echo "Name: ${fields[0]}"
echo "Age: ${fields[1]}"
echo "Role: ${fields[2]}"Создание небольшой библиотеки функций
В настоящих проектах на Bash повторно используемые функции разделяют по файлам библиотек, которые подключаются скриптами с помощью source (или оператора точки .). Основные правила хорошего проектирования библиотек:
- Каждая переменная внутри функции библиотеки должна быть объявлена как
local. - Функции библиотек никогда не вызывают
exit— они используютreturn, чтобы вызывающий код продолжал работу. - Используйте единообразный префикс пространства имён (например,
str_,log_), чтобы избежать конфликтов имён. - Защищайтесь от повторного подключения с помощью переменной-индикатора.
Ниже приведена минимальная библиотека строковых утилит, следующая этим соглашениям.
#!/usr/bin/env bash
# lib/str.sh — string utility library
[[ -n "${_LIB_STR_LOADED:-}" ]] && return 0
_LIB_STR_LOADED=1
str_trim() {
local str="$1"
str="${str#"${str%%[![:space:]]*}"}"
str="${str%"${str##*[![:space:]]}"}"
printf '%s' "$str"
}
str_repeat() {
local -i times="$2"
local char="$1"
local -i i
for (( i = 0; i < times; i++ )); do
printf '%s' "$char"
done
}
str_contains() {
local haystack="$1"
local needle="$2"
[[ "$haystack" == *"$needle"* ]]
}
# --- self-test when executed directly ---
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
trimmed=$(str_trim " hello world ")
echo "Trimmed: '${trimmed}'"
str_repeat '-' 20; echo
if str_contains "bash scripting" "script"; then
echo "Contains: yes"
fi
fiПроверка аргументов внутри функций
Функции, получающие аргументы, должны проверять их в начале работы и возвращать определённый код завершения при некорректном вводе. Этот подход называется шаблоном защитного условия: немедленная и понятная обработка ошибки.
- Проверяйте количество аргументов с помощью
$#. - Проверяйте типы или форматы до начала основной работы.
- Выводите диагностические сообщения только в stderr, никогда — в stdout.
- Используйте разные ненулевые коды возврата (например, 1 = неверные аргументы, 2 = файл не найден), чтобы вызывающий код мог по-разному реагировать на разные причины ошибки.
#!/usr/bin/env bash
file_line_count() {
if (( $# != 1 )); then
printf 'Usage: file_line_count <file>\n' >&2
return 1
fi
local file="$1"
if [[ ! -f "$file" ]]; then
printf 'Error: not a file: %s\n' "$file" >&2
return 2
fi
if [[ ! -r "$file" ]]; then
printf 'Error: cannot read: %s\n' "$file" >&2
return 3
fi
local -i count
count=$(wc -l < "$file")
printf '%d' "$count"
return 0
}
# Test with /etc/hosts (exists on every Linux/macOS system)
if lines=$(file_line_count /etc/hosts); then
echo "/etc/hosts has $lines lines"
else
echo "Failed with exit code: $?"
fiОбъединяем всё: пример из реального проекта
Вот полный автономный скрипт, демонстрирующий совместную работу всех понятий из этого урока:
- Переменные
localв каждой функции. - Коды завершения для сообщения об успехе или ошибке.
printfдля передачи строковых результатов.- Ошибки записываются в stderr, а результаты — в stdout.
- Защитные условия для проверки аргументов.
Изучите последовательность действий: parse_version извлекает данные, version_ge сравнивает их, а main корректно использует обе функции.
#!/usr/bin/env bash
# Parse a semver string into components via nameref
parse_version() {
local -n _major="$2" _minor="$3" _patch="$4"
local version="$1"
local IFS='.'
local -a parts
read -ra parts <<< "$version"
_major="${parts[0]:-0}"
_minor="${parts[1]:-0}"
_patch="${parts[2]:-0}"
}
# Return 0 if version $1 >= version $2
version_ge() {
local -i maj_a min_a pat_a
local -i maj_b min_b pat_b
parse_version "$1" maj_a min_a pat_a
parse_version "$2" maj_b min_b pat_b
if (( maj_a != maj_b )); then (( maj_a > maj_b ))
elif (( min_a != min_b )); then (( min_a > min_b ))
else (( pat_a >= pat_b ))
fi
}
require_bash_version() {
local required="$1"
local actual="${BASH_VERSION%%(*}"
if version_ge "$actual" "$required"; then
printf 'Bash %s satisfies >= %s\n' "$actual" "$required"
return 0
else
printf 'Error: need Bash >= %s, got %s\n' "$required" "$actual" >&2
return 1
fi
}
main() {
require_bash_version "4.3" || return 1
require_bash_version "99.0" || true # demonstrates failure path
}
mainПроверка знаний: локальные переменные и возвращаемые значения
Рассмотрите следующую функцию Bash. Как правильно получить её строковый результат в вызывающем коде и какое утверждение о переменной tmp верно?
transform() {
local tmp="${1,,}" # lowercase
printf '%s' "$tmp"
return 0
}Итоги: функции с локальной областью видимости и кодами возврата
В этом уроке Вы научились писать чистые, составные и безопасные функции Bash:
- Всегда используйте
localдля переменных внутри функций, чтобы не загрязнять область видимости вызывающего кода. - Используйте коды завершения (
return 0/1/N) для сообщения об успехе или ошибке — они естественно работают сif,&&и||. - Используйте
printfдля вывода в stdout, чтобы передавать строковые результаты; вызывающий код может получить их с помощью$(). - Записывайте ошибки в stderr (
>&2), чтобы stdout оставался чистым для передачи данных и работы с конвейерами. - Используйте
local -n(ссылку на переменную), когда нужно записать значение в переменную вызывающего кода без затрат на запуск подоболочки. - Защитные условия (ранняя проверка аргументов и немедленный возврат при некорректном вводе) делают функции надёжными и понятными.
- Файлы библиотек следует подключать через source, использовать префиксы пространств имён, никогда не вызывать
exitи защищать от повторного подключения.
Освоение этих шаблонов отличает хрупкие одноразовые скрипты от профессиональных и удобных в сопровождении кодовых баз Bash.
Часто задаваемые вопросы
Урок «Проектирование функций с локальной областью и кодами возврата» бесплатный?
Да — полный текст урока «Проектирование функций с локальной областью и кодами возврата» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс DevOps Bootcamp, подпишись на CoddyKit PRO. Курс DevOps Bootcamp содержит 4 уроков всего.
Чему я научусь в уроке «Проектирование функций с локальной областью и кодами возврата»?
Пишите функции с локальными переменными, статусами завершения и возвращаемыми значениями на основе printf вместо ненадёжных глобальных переменных. Ты практикуешь DevOps Bootcamp с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать DevOps Bootcamp?
Предыдущий опыт не требуется. DevOps Bootcamp на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.
Сколько времени занимает урок «Проектирование функций с локальной областью и кодами возврата»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке DevOps Bootcamp?
Да. Каждый урок DevOps Bootcamp включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Проектирование функций с локальной областью и кодами возврата
- Создание и подключение повторно используемых библиотек Bash
- Разбор флагов и аргументов с помощью getopts
- Передача массивов и ассоциативных отображений между функциями