Скрипт из именованных частей и с опциями, как у настоящих утилит
local-v -d каталог через getopts и выводить справкуФункция — именованный блок команд. Вызывается как обычная команда, аргументы получает в $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.
Переменные в bash по умолчанию глобальные: функция видит и меняет переменные скрипта. Это удобно для счётчиков и опасно для всего остального — функция может затереть чужую переменную с тем же именем. Внутренние переменные объявляют local:
counter=0
bump() {
counter=$((counter + 1)) # меняет глобальную — намеренно
local tmp=42 # видна только внутри bump
}
bump; bump
echo "counter=$counter tmp=${tmp:-нет}"
counter=2 tmp=нет
Правило: всё, что функция создаёт для себя, — local. Глобальные изменения делайте явно и называйте их понятно.
У функции, как у команды, есть код возврата — его задаёт 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.
Массив хранит список значений, в том числе с пробелами, без риска разбиения. Индексы — с нуля.
$ 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/"
Ассоциативный массив — словарь: строковый ключ → значение. Объявляется обязательно через 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
Утилиты принимают опции: -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 с пустым путём в сотой.Скрипт длиннее экрана удобно строить по одной схеме: настройки → функции → разбор аргументов → 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, $@; внутренние переменные — localreturn — только успех или ошибка (0–255); значение — через вывод и $(…)arr=(…), перебор — "${arr[@]}"; аргументы команды собирают в массивdeclare -A; порядок ключей не гарантированgetopts, потом shift $((OPTIND-1)), проверка значений и usagemain; опция пробного прогона для опасных действий