# Bash

## Що це таке

Bash (Bourne-Again SHell), що поширюється як GNU Bash і запускається командою
`bash`, — це програма, яка читає команди, набрані в рядку запрошення або
збережені у файлі, і виконує їх: запускає інші програми, з'єднує їхні вхід і
вихід та додає змінні, цикли, перевірки й функції, щоб послідовність команд
могла ухвалювати рішення. Назва обіграє прізвище Стівена Борна, чия оболонка
`sh` постачалася із сьомою редакцією Research Unix у Bell Labs. Посібник описує
Bash як здебільшого сумісну з тією оболонкою, з можливостями, запозиченими з
Korn shell і C shell, і як таку, що має відповідати частині стандарту POSIX про
оболонку. Поточний випуск — 5.3, а посібник до нього датовано травнем 2025 року.

Оболонку легко сплутати з вікном, у якому вона працює: термінал малює символи й
передає натискання клавіш, а оболонка — це програма по інший бік, яка їх
тлумачить. Bash працює інтерактивно, з рядком запрошення, історією та
автодоповненням за клавішею Tab, або неінтерактивно, читаючи скрипт від початку
до кінця без нагляду людини, і саме там ховається більшість її пасток.

Bash — одна з кількох програм, що можуть виконувати цю роль. POSIX визначає
переносну мову оболонки, `sh`, а те, яка програма встановлена як `/bin/sh`,
залежить від системи. У Debian та Ubuntu це dash — невелика оболонка POSIX, що
походить від Almquist shell, а Debian Policy називає bash, dash і ksh
найпоширенішими оболонками, які користувачі можуть обрати як `/bin/sh`. Zsh є
типовою оболонкою для нових облікових записів у macOS від версії 10.15, хоча Mac
досі містить `/bin/bash`. Скрипт обирає свій інтерпретатор першим рядком, так
званим shebang: `#!/bin/sh` просить ту оболонку POSIX, яка є в системі, а
`#!/bin/bash` або `#!/usr/bin/env bash` просить саме Bash.

## Чому це важливо

Bash — мова, що зшиває між собою все на серверах Unix. Вона є майже на кожній
машині з Linux, а команди, які інженер набирає в рядку запрошення, уже написані
на Bash, тож кілька таких команд, збережених у файлі, стають скриптом без нової
мови, без пакета для встановлення і без середовища виконання, яке треба
оновлювати. Щоб з'єднати наявні програми в ланцюжок, наприклад зняти дамп бази
даних, стиснути його й скопіювати деінде, мало яка мова буде такою ж стислою, бо
канали, перенаправлення та коди завершення — її рідна лексика.

Ті самі риси перетворюються на витрати, коли скрипт росте. Кожне значення — це
рядок, а арифметика працює лише з цілими числами, тож усе, що має структуру,
як-от JSON, дати чи дробові суми, доводиться передавати іншому інструменту.
Розгортання без лапок розбиваються на слова, а потім зіставляються з іменами
файлів, тож значення з пробілом чи зірочкою змінює зміст команди, в яку
потрапляє. Обробку помилок треба вмикати самому: типово скрипт працює далі після
того, як команда завершилася з помилкою, а параметри, що це змінюють, `set -e` і
`pipefail`, мають винятки, на перелік яких посібнику потрібно кілька абзаців.
Робоче правило таке: щойно скрипту знадобилися структури даних, парсер чи набір
тестів, він став програмою, і мову на кшталт Python дешевше підтримувати.

Переносність — теж рішення. Debian Policy вимагає, щоб скрипти для `/bin/sh`
обмежувалися можливостями POSIX і коротким списком розширень, а скрипт, якому
потрібно більше, називав свою оболонку в першому рядку, наприклад `#!/bin/bash`.
Для пошуку порушень вона радить `checkbashisms` або запуск у суворішій оболонці,
як-от posh, а в разі сумніву — `/bin/bash`. Стандарт рухається повільно: у Bash
`pipefail` є з версії 3.0, а POSIX додав його лише у виданні 2024 року.

Оболонка перетворює текст на команди, а отже, стає межею безпеки всюди, куди
потрапляє текст ззовні. Вразливість Shellshock, зареєстрована як CVE-2014-6271 у
вересні 2014 року, показала, наскільки широкою може бути ця межа. Bash до версії
4.3 включно виконувала команди, що стояли після визначення функції у змінній
середовища, тож усе, що копіювало текст зловмисника в середовище перед запуском
Bash, ставало способом виконати код на машині: CGI-скрипти під Apache, механізм
ForceCommand в OpenSSH і деякі клієнти DHCP. Перше виправлення виявилося
неповним, і те, що лишилося, отримало власний ідентифікатор, CVE-2014-7169.
Повсякденний урок скромніший: текст ззовні має потрапляти до команди як
аргумент, а не як частина рядка, переданого `eval` чи `sh -c`.

## Як це виглядає на практиці

У роботі компанії Bash має те місце, яке їй відводить
[сторінка технологій](/uk/tech-stack/): Python відповідає за роботу з даними,
автоматизацію та сервіси, а Bash з'єднує все це між собою і пишеться так, щоб
його було легко читати.

У
[шести пайплайнах ETL і ELT](/uk/portfolio/designed-implemented-and-administered-6-etl-elt-pipelines-8/),
які наповнювали Google BigQuery даними зі сховища за IPsec VPN, завантажені
архіви містили файли Parquet, CSV і .bak. Скрипти на Bash і Python діставали з
них файли й розподіляли кожен за типом і схемою, після чого bcp, psql і SSIS
завантажували дані в проміжні схеми, а gzip, Google Cloud Storage та утиліта bq
доставляли результат у BigQuery. Автоматизація стиснула понад три години ручної
роботи до менше ніж 20 хвилин на весь цикл і перевела синхронізацію даних із
щотижневої на щоденну.

[Сто автоматизованих критичних резервних копій даних](/uk/portfolio/automated-100-critical-data-backups-using-barman-google-18/)
показують такий самий поділ праці. Barman відповідав за PostgreSQL, Google Cloud
зберігав копії за межами основного майданчика, а Bash і Python керували всім
процесом і перевіряли його: політики зберігання тримали копії актуальними, а
перевірки цілісності підтверджували, що кожна з них справді придатна, а не
просто існує. Саме таку роботу Bash виконує найкраще: послідовність наявних
інструментів, де цінність скрипту полягає в перевірці того, що повернув кожен
крок.

## Де виникають проблеми

**Ім'я файлу з пробілом перетворюється на два аргументи.** `$file` без лапок
розбивається за пробільними символами, а потім розгортається як шаблон glob, тож
`report 2024.csv` доходить до `rm` як два імені, а випадкова `*` захоплює цілий
каталог. Беріть у лапки кожне розгортання, як-от `"$file"`, передавайте списки
аргументів далі як `"$@"`, зберігайте списки імен у масивах і доручіть решту
ShellCheck.

**Конвеєр резервного копіювання звітує про успіх, хоча дамп зірвався.** Код
завершення конвеєра — це код його останньої команди, тож у
`pg_dump app | gzip > app.sql.gz` невдалий дамп однаково лишає gzip успішним на
тих крихтах, які він отримав. Увімкніть `set -o pipefail`, щоб конвеєр повертав
останній ненульовий статус, і перевіряйте результат командою `gzip -t` або, ще
краще, пробним відновленням.

**Скрипт із `set -e` іде далі після команди, що завершилася з помилкою.**
Параметр ігнорується в умові `if` чи `while`, у будь-якій частині списку `&&`
або `||`, крім останньої, і в будь-якій функції, викликаній із цих місць, тож
дамп нижче може зірватися, а скрипт однаково виведе повідомлення про успіх:

```bash
#!/usr/bin/env bash
set -euo pipefail

backup() {
  pg_dump app > /var/backups/app.sql
  gzip -f /var/backups/app.sql
}

if backup; then # тут set -e всередині backup ігнорується
  echo "backup done"
fi
```

Споріднена пастка — `local out=$(cmd)`: такий рядок повертає статус `local` і
приховує помилку в `cmd`. Усередині функцій додавайте `|| return 1` після кожної
важливої команди й оголошуйте змінну окремим рядком перед тим, як присвоїти їй
значення.

**Скрипт, що працює на одній машині, ламається на іншій.** Файл, який
починається з `#!/bin/sh`, але використовує перевірки `[[` або масиви, в одній
системі запускається під Bash, а в наступній — під dash. Там `[[` — невідома
команда, тож `if`, побудований на ній, виводить помилку й переходить до гілки
else, а присвоєння масиву є синтаксичною помилкою, яка завершує скрипт. На Mac
`/bin/bash` повідомляє версію 3.2.57, яка вийшла раніше за асоціативні масиви й
`mapfile`, що з'явилися у 4.0, тож `declare -A` там зупиняється з помилкою про
неприпустимий параметр. Указуйте в shebang ту оболонку, яка справді потрібна
скрипту, і тестуйте на найстарішій версії, на якій він має працювати.

**Рядок очищення видаляє набагато більше, ніж мав би.** Якщо `build_dir` не
задано, `rm -rf "$build_dir"/*` розгортається в `rm -rf /*`. `set -u` робить
незадану змінну помилкою, а запис `"${build_dir:?}"` зупиняє неінтерактивний
скрипт щоразу, коли змінна не задана або містить порожній рядок.

## Що почитати далі

- [GNU Bash Reference Manual](https://www.gnu.org/software/bash/manual/bash.html)
  — посібник до версії 5.3: розгортання, лапки, вбудована команда `set` і точний
  перелік випадків, коли `set -e` ігнорується.
- [Shell Command Language](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html)
  — переносна мова `sh` у POSIX.1-2024, межа для будь-якого скрипту з
  `#!/bin/sh`.
- [Debian Policy: Scripts](https://www.debian.org/doc/debian-policy/ch-files.html#scripts)
  — що має підтримувати `/bin/sh` у Debian, коли скрипт мусить просити Bash на
  ім'я, і поради щодо `set -e`.
- [Bash NEWS](https://tiswww.case.edu/php/chet/bash/NEWS) — перелік нових
  можливостей від супровідника, випуск за випуском, щоб перевірити, якої версії
  потребує конструкція.
- [CVE-2014-6271](https://nvd.nist.gov/vuln/detail/CVE-2014-6271) — запис NVD
  про Shellshock із переліченими шляхами атаки та ідентифікатором подальшої
  вразливості.

<https://engineer.company/uk/glossary/bash/>
