Модуль 05 — Функции, аргументы, массивы

Скрипт из именованных частей и с опциями, как у настоящих утилит

Прогресс курса Модуль 5 из 9

Что вы освоите в этом модуле

01

Объявление и вызов

Функция — именованный блок команд. Вызывается как обычная команда, аргументы получает в $1, $2, $@ — свои, не скрипта. Объявить функцию нужно раньше, чем вызвать.

#!/bin/bash
log() {
    echo "$(date '+%F %T') [$1] ${*:2}" >&2
}

log INFO "старт, аргументов: $#"
log WARN "мало места"
2026-10-14 22:05:11 [INFO] старт, аргументов: 2
2026-10-14 22:05:11 [WARN] мало места

${*:2} — все аргументы, начиная со второго. Функция log пишет в stderr: сообщения не попадут в вывод, который скрипт отдаёт дальше по конвейеру. Встречается и запись function log { … } — это то же самое, но не по стандарту POSIX.

02

local

Переменные в bash по умолчанию глобальные: функция видит и меняет переменные скрипта. Это удобно для счётчиков и опасно для всего остального — функция может затереть чужую переменную с тем же именем. Внутренние переменные объявляют local:

counter=0
bump() {
    counter=$((counter + 1))       # меняет глобальную — намеренно
    local tmp=42                   # видна только внутри bump
}
bump; bump
echo "counter=$counter tmp=${tmp:-нет}"
counter=2 tmp=нет

Правило: всё, что функция создаёт для себя, — local. Глобальные изменения делайте явно и называйте их понятно.

03

Код возврата и значение

У функции, как у команды, есть код возврата — его задаёт return N (0–255) или код последней команды. Он отвечает на вопрос «удалось или нет» и годится для if:

is_root() {
    (( EUID == 0 ))
}
if is_root; then log INFO "под root"; else log WARN "не root"; fi

Значение — строку, число, путь — функция выводит в stdout, а вызывающий забирает через $(…):

disk_used() {
    local mount=${1:-/}
    df --output=pcent "$mount" | tail -1 | tr -dc 0-9
}
u=$(disk_used /)
log INFO "занято на /: $u%"

Частые ошибки — пытаться вернуть число через return или забыть, что лишний echo внутри функции попадёт в результат:

$ f() { return 300; }; f; echo "return 300 → $?"
return 300 → 44
$ g() { echo "результат"; return 3; }
$ r=$(g); echo "r=$r код=$?"
r=результат код=3

300 не помещается в байт и превращается в 300 − 256 = 44. Диагностику внутри «возвращающих» функций пишите в stderr — как делает log.

04

Массивы

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

$ servers=(web01 web02 "db 01")
$ echo "${#servers[@]} ${servers[0]} ${servers[2]}"
3 web01 db 01
$ servers+=(cache01)                       # добавить в конец
$ for s in "${servers[@]}"; do echo "[$s]"; done
[web01]
[web02]
[db 01]
[cache01]
ЗаписьЗначение
"${arr[@]}"Все элементы, каждый отдельным словом — для циклов и аргументов команд
${#arr[@]}Число элементов
${arr[i]}, ${arr[-1]}Элемент по индексу, последний
${!arr[@]}Список индексов
arr+=(x), unset 'arr[1]'Добавить, удалить элемент
files=(*.sh)Массив из шаблона
mapfile -t lines < файлСтроки файла в массив

Главное применение в скриптах — собрать аргументы команды по частям, не теряя пробелов:

opts=(-a --delete)
(( dry_run )) && opts+=(--dry-run)
[[ -n $exclude ]] && opts+=(--exclude "$exclude")
rsync "${opts[@]}" "$src/" "$dst/"
05

Ассоциативные массивы

Ассоциативный массив — словарь: строковый ключ → значение. Объявляется обязательно через declare -A.

$ declare -A port=([ssh]=22 [http]=80 [https]=443)
$ port[pg]=5432
$ echo "${port[http]} ${#port[@]}"
80 4
$ for k in "${!port[@]}"; do echo "$k=${port[$k]}"; done | sort
http=80
https=443
pg=5432
ssh=22

Порядок ключей не гарантирован — для стабильного вывода нужен sort. Типичная задача — подсчёт: сколько раз встретилось каждое значение.

$ declare -A seen
$ for w in a b a c b a; do seen[$w]=$(( ${seen[$w]:-0} + 1 )); done
$ for k in "${!seen[@]}"; do echo "$k ${seen[$k]}"; done | sort
a 3
b 2
c 1
06

Опции скрипта: getopts

Утилиты принимают опции: -v, -d каталог. Разбирать их вручную через $1 неудобно; встроенная команда getopts делает это по стандарту. Строка "vd:k:h" перечисляет опции; двоеточие после буквы — у опции есть значение, оно приходит в $OPTARG.

#!/bin/bash
# opts.sh [-v] [-d КАТАЛОГ] [-k ДНЕЙ] ФАЙЛ…
usage() {
    echo "Использование: $0 [-v] [-d КАТАЛОГ] [-k ДНЕЙ] ФАЙЛ…" >&2
    exit 2
}
verbose=0 dest=/srv/backup keep=7
while getopts "vd:k:h" opt; do
    case $opt in
        v) verbose=1 ;;
        d) dest=$OPTARG ;;
        k) keep=$OPTARG ;;
        h|*) usage ;;
    esac
done
shift $((OPTIND - 1))                  # убрать разобранные опции, остались файлы
(( $# > 0 )) || usage
[[ $keep =~ ^[0-9]+$ ]] || { echo "-k: нужно число, получено '$keep'" >&2; exit 2; }
echo "verbose=$verbose dest=$dest keep=$keep файлы: $*"
$ ./opts.sh -v -d /tmp/b -k 14 a.txt b.txt
verbose=1 dest=/tmp/b keep=14 файлы: a.txt b.txt
$ ./opts.sh a.txt
verbose=0 dest=/srv/backup keep=7 файлы: a.txt
$ ./opts.sh -k abc a.txt
-k: нужно число, получено 'abc'
$ ./opts.sh -x a.txt
./opts.sh: недопустимый параметр — x
Использование: ./opts.sh [-v] [-d КАТАЛОГ] [-k ДНЕЙ] ФАЙЛ…
$ ./opts.sh -d
./opts.sh: для параметра требуется аргумент — d
Использование: ./opts.sh [-v] [-d КАТАЛОГ] [-k ДНЕЙ] ФАЙЛ…

getopts понимает только короткие опции, зато склеенные (-vk 14) и в любом порядке. Длинные --verbose разбирают вручную через case "$1" и shift в цикле while (( $# )) — для учебных и служебных скриптов коротких хватает.

Проверяйте значения опций сразу после разбора: число — регулярным выражением, каталог — [[ -d ]]. Ошибка, найденная в первой строке, лучше, чем rm с пустым путём в сотой.
07

Структура скрипта

Скрипт длиннее экрана удобно строить по одной схеме: настройки → функции → разбор аргументов → main. Тогда его читают сверху вниз как оглавление.

#!/bin/bash
# cleanup — удаляет из КАТАЛОГА файлы старше N дней

# --- настройки
keep=7
dry_run=0

# --- функции
usage() { echo "Использование: $0 [-n] [-k ДНЕЙ] КАТАЛОГ" >&2; exit 2; }
log()   { echo "$(date '+%F %T') $*" >&2; }

main() {
    local dir=$1 count=0 f
    while IFS= read -r -d '' f; do
        if (( dry_run )); then log "удалил бы $f"; else rm -- "$f"; fi
        count=$((count + 1))
    done < <(find "$dir" -type f -mtime +"$keep" -print0)
    log "файлов: $count"
}

# --- аргументы
while getopts "nk:" opt; do
    case $opt in n) dry_run=1 ;; k) keep=$OPTARG ;; *) usage ;; esac
done
shift $((OPTIND - 1))
[[ -d ${1:-} ]] || usage

main "$1"

find … -print0 и read -d '' разделяют имена нулевым байтом — единственным символом, который не может встретиться в имени файла. Это надёжный способ обработать любые имена, даже с переводом строки внутри. Опция -n (dry run, «пробный прогон») показывает, что будет сделано, ничего не меняя, — её стоит добавлять в каждый скрипт, который удаляет.

Ключевые выводы модуля

Запомните главное

  • Функция получает свои $1, $@; внутренние переменные — local
  • return — только успех или ошибка (0–255); значение — через вывод и $(…)
  • Массив — arr=(…), перебор — "${arr[@]}"; аргументы команды собирают в массив
  • Словарь — declare -A; порядок ключей не гарантирован
  • Опции — getopts, потом shift $((OPTIND-1)), проверка значений и usage
  • Схема скрипта: настройки → функции → аргументы → main; опция пробного прогона для опасных действий
Практические задания — на учебном портале. Задания, критерии оценивания, сдача работ и оценки преподавателя — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Модуль 04: Циклы Все модули Модуль 06: Текст