Bash скрипты: Работа с функциями

При администрировании серверов на базе Linux или управлении хостингом автоматизация играет ключевую роль. Язык сценариев Bash (Bourne Again SHell) остается золотым стандартом для написания скриптов администрирования, резервного копирования, мониторинга и развертывания приложений. Однако по мере роста сложности задач линейные скрипты превращаются в «спагетти-код»: их трудно читать, отлаживать и поддерживать.

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

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

  • Упрощает отладку. Ошибка локализуется внутри конкретной функции.

  • Повышает читаемость. Скрипт превращается в последовательность осмысленных действий: check_disk_spacecreate_backupsend_report.

  • Облегчает тестирование. Каждую функцию можно проверить изолированно.

Данная информация предназначена для услуг: VPS хостинг или Облачный хостинг

Синтаксис объявления функций

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

Классический синтаксис (POSIX-совместимый)

Этот вариант предпочтителен, если ваш скрипт должен работать не только в Bash, но и в других оболочках (shdash).
имя_функции () {
  # тело функции
  команда1
  команда2
}

Пример:

print_message () {
  echo "Сервер работает в штатном режиме"
}

Синтаксис с ключевым словом function (расширение Bash)

Этот вариант более читаем для новичков и явно указывает на определение функции. Однако он не совместим с POSIX sh.
function имя_функции {
  # тело функции
  команда1
}

Пример:

function check_root {
  if [[ $EUID -ne 0 ]]; then
    echo "Этот скрипт требует прав root" >&2
    return 1
  fi
}

Рекомендации по выбору

  • Используйте function имя () или просто имя (), если скрипт предназначен только для Bash.

  • Используйте имя (), если требуется переносимость между разными sh-совместимыми оболочками.

  • Избегайте пробелов между именем функции и скобками.

Вызов функций и передача аргументов

Функция в Bash вызывается как обычная команда. Аргументы передаются через пробел.
# Определение
greet_user () {
  echo "Привет, $1! Твой ID: $2"
}

# Вызов
greet_user "admin" 1001

Внутренние переменные аргументов

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

Переменная Описание
$0 Имя самого скрипта (не функции)
$1 ... $9 Позиционные параметры (аргументы функции)
${10} Десятый и далее аргументы (обязательно в фигурных скобках)
$# Количество переданных аргументов
$@ Все аргументы как разделенные строки
$* Все аргументы как одна строка

Пример обработки аргументов

process_file () {
  if [[ $# -lt 2 ]]; then
    echo "Ошибка: требуется два аргумента: файл и действие" >&2
   return 1
  fi

  local filename="$1"
  local action="$2"

  case "$action" in
    backup) cp "$filename" "$filename.bak";;
    delete) rm "$filename";;
    *) echo "Неизвестное действие: $action";;
  esac
}

Передача массивов в функции

Массивы в Bash не передаются напрямую по значению. Вместо этого передаются отдельные элементы.
declare -a sites=("site1.com" "site2.org" "site3.net")

deploy_sites () {
  local sites_list=("$@") # собираем массив из аргументов
  for site in "${sites_list[@]}"; do
    echo "Деплой на $site"
  done
}

deploy_sites "${sites[@]}"

Области видимости: local против глобальных переменных

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

Глобальная область (по умолчанию)

var="глобальная"

test_scope () {
  var="изменена внутри"
  echo "Внутри функции: $var"
}

test_scope
echo "Снаружи: $var" # Выведет: "изменена внутри" — глобальная переменная перезаписана!

Локальная область (ключевое слово local)

Чтобы изолировать переменные внутри функции, используйте local.
var="глобальная"

test_scope () {
  local var="локальная"
  echo "Внутри функции: $var" # "локальная"
}

test_scope
echo "Снаружи: $var" # "глобальная"

Рекомендации по использованию

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

  • Исключение: конфигурационные переменные, которые должны быть доступны из любой точки скрипта.

  • Проверяйте declare -p var, чтобы понять область переменной.

Переменные только для чтения

readonly MAX_TRIES=3

retry_operation () {
  # MAX_TRIES=5 # Ошибка: переменная только для чтения
  for (( i=1; i<=MAX_TRIES; i++ )); do
    # попытка выполнить операцию
  done
}

Возврат значений: return и echo

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

Код возврата (числовой статус)

return N устанавливает код завершения функции. По соглашению 0 означает успех, любое ненулевое значение — ошибку.

check_user_exists () {
  local username="$1"
  if id "$username" &>/dev/null; then
    return 0 # пользователь существует
  else
    return 1 # не существует
  fi
}

# Использование
if check_user_exists "www-data"; then
  echo "Пользователь найден"
else
  echo "Пользователь не найден"
fi

Возврат произвольных данных (строк, массивов)

Так как return может вернуть только число от 0 до 255, для возврата данных используется вывод команды (echo) с последующим захватом через $().

get_server_load () {
  local load=$(uptime | awk -F 'load average:' '{print $2}')
  echo "$load" # это будет возвращено
}

# Захват вывода
load_values=$(get_server_load)
echo "Средняя нагрузка: $load_values"

Возврат массивов

get_mysql_databases () {
  local databases=($(mysql -e "SHOW DATABASES;" -s --skip-column-names))
  echo "${databases[@]}"
}

# Преобразование строки обратно в массив
IFS=' ' read -r -a db_array <<< "$(get_mysql_databases)"

Комбинация return + echo

Промышленный паттерн: функция выводит данные через echo, а код возврата использует для сигнализации об ошибке.
get_disk_usage () {
  local partition="$1"
  if [[ ! -d "$partition" ]]; then
    echo "Ошибка: раздел $partition не существует" >&2
    return 1
  fi
  local usage=$(df -h "$partition" | awk 'NR==2 {print $5}' | sed 's/%//')
  echo "$usage"
  return 0
}

if output=$(get_disk_usage "/"); then
  echo "Использование диска: $output%"
else
  echo "Ошибка: $output" >&2
fi

Расширенные концепции

Рекурсия в Bash

Функции могут вызывать сами себя — это полезна для обхода древовидных структур (например, директорий).
#!/bin/bash
# Рекурсивное удаление пустых директорий
remove_empty_dirs () {
  local dir="$1"
  if [[ -d "$dir" ]]; then
    # Рекурсивный вызов для всех поддиректорий
    for subdir in "$dir"/*/; do
      if [[ -d "$subdir" ]]; then
        remove_empty_dirs "$subdir"
      fi
    done

    # Удаляем текущую директорию, если она пуста
    if [[ -z "$(ls -A "$dir")" ]]; then
      echo "Удаление пустой директории: $dir"
      rmdir "$dir"
    fi
  fi
}

remove_empty_dirs "/var/cache/app"

Функции с опциями (как у getopt)

Создание функций, поддерживающих длинные и короткие опции.
backup_database () {
  local user="root"
  local password=""
  local database="all"
  local backup_dir="/backups"

  while [[ $# -gt 0 ]]; do
    case "$1" in
      -u|--user)
        user="$2"
        shift 2
        ;;
      -p|--password)
        password="$2"
        shift 2
        ;;
      -d|--database)
        database="$2"
        shift 2
        ;;
      --backup-dir)
        backup_dir="$2"
        shift 2
        ;;
      -h|--help)
        echo "Использование: backup_database [-u user] [-p pass] [-d db]"
        return 0
        ;;
      *)
        echo "Неизвестная опция: $1" >&2
        return 1
        ;;
    esac
  done

  echo "Резервное копирование $database пользователем $user в $backup_dir"
  # реальная логика backup
}

backup_database --user=admin --database=mydb --backup-dir=/mnt/backup

Переопределение встроенных команд

Bash позволяет создавать функции с именами, совпадающими со стандартными командами (хотя это не рекомендуется, кроме случаев оберток).
# Обертка для cd с логированием
cd () {
  builtin cd "$@" # вызов встроенной команды
  echo "Текущая директория: $(pwd)" >> /var/log/cd_history.log
}

# Использование
cd /etc/nginx

Экспорт функций в дочерние процессы

Если вы запускаете другой Bash-скрипт из текущего, функции не передаются автоматически. Используйте export -f.
# В родительском скрипте
my_helper () {
  echo "Helper called with: $1"
}
export -f my_helper

# Вызов дочернего скрипта
bash child_script.sh

В child_script.sh функция my_helper будет доступна.

Отладка функций

Режим трассировки set -x

Включение трассировки внутри одной функции.
debug_ftp_connection () {
  set -x
  # подключение к FTP
  ftp -n <<EOF
  open ftp.example.com
  user admin pass
  ls
  bye
EOF
  set +x # отключаем трассировку
}

Проверка синтаксиса без выполнения

bash -n script.sh
Подробная отладка с PS4

Установка переменной PS4 увеличивает детализацию вывода set -x с номерами строк и именем функции.
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -x
# код скрипта

Пример вывода:
+(script.sh:12): check_disk(): df -h / | awk 'NR==2 {print $5}'

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

error_handler () {
  local line=$1
  local func=$2
  echo "Ошибка в функции $func на строке $line" >&2
}

my_risky_function () {
  trap 'error_handler $LINENO ${FUNCNAME[0]}' ERR
  mkdir /protected/dir # вызовет ошибку, если нет прав
  trap - ERR # сброс обработчика
}

Практические примеры

Функция ротации логов

rotate_logs () {
  local log_dir="/var/log/nginx"
  local max_age_days=30

  # Проверка входных параметров
  if [[ ! -d "$log_dir" ]]; then
    echo "Ошибка: директория $log_dir не существует" >&2
    return 1
  fi

  # Ротация всех .log файлов
  for logfile in "$log_dir"/*.log; do
    [[ -f "$logfile" ]] || continue
    local timestamp=$(date +"%Y%m%d_%H%M%S")
    mv "$logfile" "${logfile}.${timestamp}"
    kill -USR1 $(cat /var/run/nginx.pid) 2>/dev/null
  done

  # Удаление старых логов
  find "$log_dir" -name "*.log.*" -type f -mtime +$max_age_days -delete
  echo "Ротация логов завершена: $(date)"
}

Функция мониторинга MySQL с порогом срабатывания

monitor_mysql_slow_queries () {
  local threshold=10
  local output_file="/tmp/slow_queries_report.txt"

  # Получение количества медленных запросов за последнюю минуту
  local slow_count=$(mysql -e "SHOW GLOBAL STATUS LIKE 'Slow_queries'" -s --skip-column-names | awk '{print $2}')
  local prev_count_file="/tmp/last_slow_count"
  if [[ -f "$prev_count_file" ]]; then
    local prev_count=$(cat "$prev_count_file")
    local delta=$((slow_count - prev_count))

    if [[ $delta -gt $threshold ]]; then
      echo "Внимание: $delta медленных запросов за минуту!" >&2
      # Здесь можно отправить email или записать в лог
    fi
  fi

  echo "$slow_count" > "$prev_count_file"
  return 0
}

Функция проверки SSL-сертификата

check_ssl_cert () {
  local domain="$1"
  local warn_days=14

  if [[ -z "$domain" ]]; then
    echo "Использование: check_ssl_cert example.com" >&2
    return 1
  fi

  local expiry_date=$(echo | openssl s_client -servername "$domain" -connect "$domain":443 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
  if [[ -z "$expiry_date" ]]; then
    echo "Не удалось получить сертификат для $domain" >&2
    return 2
  fi

  local expiry_epoch=$(date -d "$expiry_date" +%s)
  local now_epoch=$(date +%s)
  local days_left=$(( (expiry_epoch - now_epoch) / 86400 ))

  if [[ $days_left -lt 0 ]]; then
    echo "КРИТИЧЕСКАЯ ОШИБКА: SSL сертификат $domain истек!" >&2
    return 3
  elif [[ $days_left -lt $warn_days ]]; then
    echo "ПРЕДУПРЕЖДЕНИЕ: SSL сертификат $domain истекает через $days_left дней" >&2
    return 1
  else
    echo "OK: SSL сертификат $domain действителен еще $days_left дней"
    return 0
  fi
}

# Циклическая проверка списка доменов
for domain in $(cat /etc/hosting/domains_list.txt); do
  check_ssl_cert "$domain"
done

Функция создания резервной копии с ротацией

create_full_backup () {
  local source_dir="$1"
  local backup_base="/backups"
  local retention_days=7

  # Валидация параметров
  if [[ $# -ne 1 ]] || [[ ! -d "$source_dir" ]]; then
    echo "Ошибка: укажите существующую директорию для бэкапа"
    return 1
  fi

  # Создание имени архива
  local backup_name=$(basename "$source_dir")_$(date +%Y%m%d_%H%M%S).tar.gz
  local backup_path="$backup_base/$backup_name"

  # Создание бэкапа
  echo "Создание бэкапа $source_dir в $backup_path"
  if tar -czf "$backup_path" -C "$(dirname "$source_dir")" "$(basename "$source_dir")"; then
    echo "Успех: бэкап создан"
  else
    echo "Ошибка: не удалось создать бэкап" >&2
    return 2
  fi

  # Ротация старых бэкапов
  find "$backup_base" -name "$(basename "$source_dir")_*.tar.gz" -mtime +$retention_days -delete

  # Дополнительно: проверка целостности
  if tar -tzf "$backup_path" &>/dev/null; then
    echo "Проверка целостности пройдена"
  else
    echo "ПРЕДУПРЕЖДЕНИЕ: бэкап $backup_path поврежден!" >&2
    return 3
  fi
}

Библиотеки функций: организация кода

В промышленной среде функции редко хранятся в одном скрипте. Их выносят в отдельные файлы—библиотеки.

Создание библиотеки libhosting.sh

#!/bin/bash
# libhosting.sh — общие функции для управления хостингом

# Функция логирования
log () {
  local level="$1"
  local message="$2"
  local logfile="/var/log/hosting_operations.log"
  echo "$(date '+%Y-%m-%d %H:%M:%S') [$level] $message" >> "$logfile"
}

# Функция проверки свободного места
check_free_space () {
  local path="$1"
  local required_mb="$2"
  local available_mb=$(df -m "$path" | awk 'NR==2 {print $4}')

  if [[ $available_mb -lt $required_mb ]]; then
    log "ERROR" "Недостаточно места на $path: $available_mb MB, требуется $required_mb MB"
    return 1
  fi
  return 0
}

# Функция перезапуска сервиса с ожиданием
restart_service () {
  local service="$1"
  local max_wait=30

  systemctl restart "$service"
  for (( i=1; i<=max_wait; i++ )); do
    if systemctl is-active --quiet "$service"; then
      log "INFO" "Сервис $service успешно перезапущен"
      return 0
    fi
    sleep 1
  done

  log "ERROR" "Не удалось перезапустить $service за $max_wait секунд"
  return 1
}

Использование библиотеки в основном скрипте

#!/bin/bash
# deploy_script.sh

# Подключение библиотеки
source /usr/local/lib/libhosting.sh

# Использование функций
check_free_space "/var/www" 1024 || exit 1
log "INFO" "Начинаю деплой приложения"
restart_service "php8.1-fpm" || exit 1

Поиск библиотек через FPATH или переменные окружения

# Определение пути к библиотекам
LIB_PATH="/opt/hosting/lib"
for lib in "$LIB_PATH"/*.sh; do
  source "$lib"
done

# Или с проверкой существования
[[ -f "$LIB_PATH/db_utils.sh" ]] && source "$LIB_PATH/db_utils.sh"

Обработка ошибок в функциях

Конструкция || return как ранний выход

deploy_application () {
  local app_dir="$1"

  cd "$app_dir" || return 1
  git pull origin main || return 2
  composer install --no-dev || return 3
  php artisan migrate || return 4

  echo "Деплой успешен"
  return 0
}

Использование set -e и set -o pipefail

Внутри функции можно переопределить поведение оболочки.
critical_operation () {
  set -e # прерывать при любой ошибке
  set -o pipefail # учитывать ошибки в пайпах

  rm -rf /tmp/build/*
  mkdir /tmp/build/new
  cp -r ./src/* /tmp/build/new/

  set +e # отключаем строгий режим
}

Глобальный обработчик ошибок через ERR trap

#!/bin/bash
on_error () {
  local line=$1
  local func=$2
  local code=$3
  echo "[FATAL] Ошибка $code в функции $func (строка $line)" >&2
  exit $code
}

trap 'on_error $LINENO ${FUNCNAME[0]:-main} $?' ERR

risky_function () {
  ls /nonexistent/dir # вызовет ошибку
  echo "Эта строка не выполнится"
}

Производительность: на что обратить внимание

Избегайте лишних внешних вызовов внутри циклов

Плохо:
process_files () {
  for file in *.txt; do
    lines=$(cat "$file" | wc -l) # два внешних вызова
    echo "$file: $lines"
  done
}

Хорошо:
process_files () {
  for file in *.txt; do
    lines=$(wc -l < "$file") # один вызов
    echo "$file: $lines"
  done
}

Используйте встроенные возможности Bash вместо внешних команд

Внешняя команда Встроенная альтернатива
grep pattern file [[ $(<file) =~ pattern ]]
cut -d: -f1 IFS=: read -r field1
sed 's/old/new/' ${var/old/new}
awk '{print $1}' set -- $var; echo $1

Кэширование результатов функций

get_ip_address () {
  local interface="$1"
  local cache_file="/tmp/ip_cache_$$"

  if [[ -f "$cache_file" ]] && [[ $(find "$cache_file" -mmin -5) ]]; then
    cat "$cache_file"
    return
  fi

  local ip=$(ip -4 addr show "$interface" | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
  echo "$ip" > "$cache_file"
  echo "$ip"
}

Частые ошибки и как их избежать

Отсутствие кавычек вокруг переменных

# Ошибка: разбиение на слова и подстановка путей
remove_files () {
  rm $1 # если $1 содержит пробелы, rm получит несколько аргументов
}

# Исправление
remove_files () {
  rm -- "$1"
}

Забытый local приводит к побочным эффектам

# Что происходит:
counter=0
increment () {
  counter=$((counter + 1)) # изменяет глобальную переменную
}

# Правильно:
increment () {
  local counter=0
  ((counter++))
  echo $counter
}

Использование return для вывода строк

# Ошибка: return "OK" # так нельзя, return ждет число

# Правильно:
echo "OK"
return 0

Вызов функции до её определения

Bash — интерпретируемый язык, но определение функции должно быть до первого вызова.
# Ошибка:
hello # вызов до определения

hello () {
  echo "Hi"
}

# Исправление: переместить определение выше.

Заключение

Функции в bin/bash — это мощный механизм, который превращает хаотичные скрипты в модульные, тестируемые и поддерживаемые системы. При разработке скриптов для хостинга, где цена ошибки высока (простой сайтов, потеря данных), следование дисциплине использования функций становится обязательным.

  • 0 Пользователи нашли это полезным

Помог ли вам данный ответ?

Ищете что-то другое?

xvps.ru