0Pricing
Linux Command Line & Bash Scripting Mastery · Урок

Проектирование функций с локальной областью и кодами возврата

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

«Проектирование функций с локальной областью и кодами возврата» — бесплатный урок Linux Command Line & Bash Scripting Mastery на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Linux Command Line & Bash Scripting Mastery, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Linux Command Line & Bash Scripting Mastery содержит 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) и разблокировать остальной курс Linux Command Line & Bash Scripting Mastery, подпишись на CoddyKit PRO. Курс Linux Command Line & Bash Scripting Mastery содержит 4 уроков всего.

Чему я научусь в уроке «Проектирование функций с локальной областью и кодами возврата»?

Пишите функции с локальными переменными, статусами завершения и возвращаемыми значениями на основе printf вместо ненадёжных глобальных переменных. Ты практикуешь Linux Command Line & Bash Scripting Mastery с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Linux Command Line & Bash Scripting Mastery?

Предыдущий опыт не требуется. Linux Command Line & Bash Scripting Mastery на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.

Сколько времени занимает урок «Проектирование функций с локальной областью и кодами возврата»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Linux Command Line & Bash Scripting Mastery?

Да. Каждый урок Linux Command Line & Bash Scripting Mastery включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Проектирование функций с локальной областью и кодами возврата
  2. Создание и подключение повторно используемых библиотек Bash
  3. Разбор флагов и аргументов с помощью getopts
  4. Передача массивов и ассоциативных отображений между функциями
← Назад к Linux Command Line & Bash Scripting Mastery