# bash-скрипты - основы и идиомы _Команды · LinuxLab Knowledge Base_ **TL;DR:** Bash-скрипт - текстовый файл с shebang `#!/usr/bin/env bash` и `chmod +x`. Обязательный starting point - `set -euo pipefail` и `shellcheck` для проверки. ## Когда писать bash, а когда нет Bash хорош для: - Связки CLI-утилит: pipe, фильтрация, redirect, exit-code-проверки. - Скриптов установки/деплоя/CI: запустить серию команд, остановиться при ошибке. - Автоматизации задач системного администрирования. **Перепиши на Python/Go когда:** - Нужны массивы объектов / map'ы / JSON-парсинг. - Скрипт > ~150 строк или есть функции с параметрами. - Нужны concurrency, тесты, типы. Когда дочитал до момента «нужен bash-array со словарём внутри» - писать не на bash. ## Минимальный безопасный скелет ```bash #!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' # код тут ``` Что это: - `#!/usr/bin/env bash` - портативный shebang. `/bin/bash` может отсутствовать на macOS/BSD/в контейнерах с busybox. - `set -e` - упасть на первой ошибке (любая команда с non-zero exit). - `set -u` - упасть на использовании необъявленной переменной. - `set -o pipefail` - exit-code пайпа = exit-code последней упавшей команды, а не последней. Без этого `false | true` = success. - `IFS=$'\n\t'` - split только по newline и tab, не по пробелам. Защита от файлов с пробелами в именах в `for f in $(ls)`-стиле. Без `set -euo pipefail` ошибки **молча игнорируются** и скрипт продолжает делать неправильное. ## Переменные ```bash name="serge" # без пробелов вокруг = echo "$name" # ВСЕГДА в кавычках, иначе ломается на пробелах echo "${name}_user" # ${} когда нужно ограничить имя port=8080 echo "URL: http://localhost:${port}/api" ``` Кавычки: - `"..."` - interpolate `$var`, `$(...)`, escape'ы. **Default, всегда используй**. - `'...'` - буквально, никаких подстановок. - Без кавычек - **word splitting** + glob expansion. Опасно с пробелами и `*`. Правило: **всегда** оборачивай переменные в `"$var"`. Даже когда «не нужно». ## Command substitution - `$(cmd)` ```bash today=$(date +%F) files=$(find /var/log -name '*.log' | wc -l) echo "Today: ${today}, log files: ${files}" ``` Старая школа `` `cmd` `` тоже работает, но `$()` лучше: - Можно вкладывать: `$(echo $(date))`. - Меньше escape-головной боли. ## Условия - `[[ ]]` и `(( ))` В bash есть три синтаксиса проверок. Используй современные. ### Строки/файлы - `[[ ]]` ```bash if [[ -f /etc/passwd ]]; then echo "exists"; fi if [[ -z "$var" ]]; then echo "empty"; fi if [[ "$user" == "root" ]]; then echo "root"; fi if [[ "$msg" =~ ^ERROR ]]; then echo "regex match"; fi ``` | Тест | Что проверяет | |----------------|----------------------------| | `-f path` | regular file существует | | `-d path` | каталог | | `-e path` | существует (любого типа) | | `-r path` | читаемо | | `-w path` | записываемо | | `-x path` | исполняемо | | `-z "$s"` | строка пуста | | `-n "$s"` | строка не пуста | | `"$a" == "$b"` | равенство строк | | `"$a" != "$b"` | не равно | | `"$s" =~ regex`| regex-match | Старый `[ ... ]` (POSIX) тоже работает, но `[[ ]]` богаче и не требует кавычек вокруг переменных. Используй `[[ ]]` всегда - кроме случаев когда пишешь под `/bin/sh`. ### Числа - `(( ))` ```bash if (( count > 100 )); then echo "many"; fi if (( $# < 2 )); then echo "need 2+ args"; exit 1; fi total=$(( a + b )) # арифметика ``` Внутри `(( ))` `$` для переменных не обязателен и нет кавычек - это **арифметический контекст**. ## Циклы ```bash # for-in по списку for host in web1 web2 web3; do ssh "$host" "uptime" done # for по файлам - НЕ через $(ls), а через glob for f in /var/log/*.log; do echo "Processing $f" done # while-read построчно (стандарт для обработки файлов) while IFS= read -r line; do echo "Got: $line" done < input.txt # C-style for ((i=0; i<10; i++)); do echo "$i" done ``` **`while IFS= read -r`** - единственный правильный способ читать файл построчно. `IFS=` запрещает обрезку whitespace, `-r` - не обрабатывать backslash-escapes. Иначе строки с табами или `\n` ломаются. **Никогда**: `for f in $(ls *.txt)` - ломается на пробелах в именах, glob-expansion двойная. Используй `for f in *.txt` напрямую. ## Функции ```bash greet() { local name="${1:-world}" # дефолт если $1 не передан echo "Hello, $name!" } greet # → Hello, world! greet "serge" # → Hello, serge! ``` Аргументы - `$1`, `$2`, ..., `$@` (все), `$#` (количество), `$0` (имя скрипта). **`local` обязательно** для всех переменных в функциях. Без `local` они глобальные и портят состояние снаружи. Это самая частая bash-ошибка. ## Redirect и pipe ```bash cmd > file # stdout → file (перезапись) cmd >> file # stdout → file (append) cmd 2> file # stderr → file cmd > file 2>&1 # stdout И stderr → file (порядок важен!) cmd &> file # bash-shortcut для того же cmd < file # stdin из файла cmd1 | cmd2 # stdout cmd1 → stdin cmd2 cmd1 |& cmd2 # stdout И stderr → stdin cmd2 cmd > /dev/null 2>&1 # тихо, всё в /dev/null ``` Heredoc для multi-line input: ```bash cat < /etc/myapp.conf port=8080 user=$USER # подставится - без кавычек у EOF EOF cat <<'EOF' > /etc/myapp.conf port=$PORT # НЕ подставится - кавычки вокруг EOF EOF ``` ## Exit codes - главный механизм ошибок Каждая команда возвращает int 0-255. `0 = success`, всё остальное - ошибка. ```bash cmd && echo "ok" # выполнится только если cmd succeeded cmd || echo "failed" # выполнится только если cmd failed cmd || exit 1 # упасть если cmd failed cmd1; cmd2 # cmd2 запустится независимо echo $? # exit code последней команды ``` Проверка нескольких: ```bash if cmd1 && cmd2; then echo "both ok"; fi ``` С `set -e` отдельные `cmd || true` = «знаю что может упасть, продолжай». ## Argv-парсинг - getopts ```bash while getopts ":vh:f:" opt; do case $opt in v) verbose=1 ;; h) host="$OPTARG" ;; f) file="$OPTARG" ;; \?) echo "Unknown: -$OPTARG"; exit 1 ;; :) echo "-$OPTARG needs argument"; exit 1 ;; esac done shift $((OPTIND-1)) # сдвинуть позиционные за флагами ``` Для long-options (`--verbose`) bash напрямую не умеет - используй `getopt` (внешняя утилита) или Python/Go. ## Дебаг ```bash bash -n script.sh # только синтаксис, не запускать bash -x script.sh # трейс выполнения каждой команды set -x # включить трейс посередине скрипта set +x # выключить ``` В трейс-режиме каждая команда печатается в stderr с префиксом `+`. Самый быстрый способ понять «где падает». ## shellcheck - обязательный линтер Любой нетривиальный скрипт прогонять через `shellcheck`. Он ловит: - Забытые кавычки `$var` без `"`. - Использование `$(ls)` в for. - `[ ... -a ... ]` вместо `[[ ... && ... ]]`. - Несовпадающие кавычки. - Глобальные переменные в функциях. ```bash sudo apt install shellcheck # Debian/Ubuntu sudo dnf install ShellCheck # Fedora shellcheck script.sh ``` Интегрировать в CI и pre-commit. Без shellcheck любой bash-проект накапливает скрытые баги. ## Типичные ловушки - **Не квотированные `$var`**: `rm $file` для `file="my doc.txt"` сделает `rm my doc.txt` (две жертвы). Всегда `rm "$file"`. - **`set -e` не ловит всё**: внутри `if`, после `||`, в pipe (без `pipefail`), в подоболочке. Проверять явно `||` после критичных команд. - **`cd` без проверки**: `cd /opt/app && do_stuff`, не `cd /opt/app; do_stuff`. Если cd упадёт - `do_stuff` запустится в текущем каталоге. - **Trap для cleanup**: `trap 'rm -f "$tmp"' EXIT` чтобы временные файлы удалились даже при kill -2. - **`bash -c` vs `sh -c`**: на системах где `sh = dash` (Debian/Ubuntu) bash-syntax не работает в `sh -c`. ## Команды ```bash shellcheck script.sh ``` Линтер для bash - ловит 90% типичных ошибок. Запускать перед каждым коммитом ```bash bash -x script.sh ``` Запуск с трейсом - каждая команда печатается в stderr перед выполнением ```bash bash -n script.sh ``` Только синтаксис-чек, не запускает - для CI или быстрой проверки ```bash set -euo pipefail ``` Безопасный strict-mode. Первая строка любого нового скрипта ```bash trap 'rm -f "$tmpfile"' EXIT ``` Cleanup при выходе по любой причине - даже kill -TERM или Ctrl+C ## См. также - [sed - потоковый редактор текста](/kb/cmd-sed.md) - [awk - обработка структурированного текста по полям](/kb/cmd-awk.md) - [jq - запросы и трансформация JSON](/kb/cmd-jq.md) - [cron и crontab - расписание задач](/kb/cmd-cron-crontab.md) - [rsync - инкрементальная синхронизация файлов](/kb/cmd-rsync.md) - [Shebang: первая строка скрипта](/kb/shebang.md) - [Process substitution: <(cmd) и >(cmd)](/kb/process-substitution.md) - [xargs и find -exec - массовые операции](/kb/xargs-and-find-exec.md) - [Helm charts - пакетный менеджер для Kubernetes](/kb/helm-charts.md)