Синтаксис bash: переменные, кавычки, подстановки
Урок курса «Курс подготовки DevOps-инженеров»
Хронометраж: занятие 60 минут — теория 30 минут, практика 30 минут. Домашняя работа: 1–2 часа.
Кратко о чём урок: Раскрытие параметров, разница между кавычками, глоббинг, арифметика, массивы, подстановка команд. Почему большинство ошибок в скриптах — это незакавыченная переменная.
1. Что изучаем
После урока вы сможете:
- объяснить разницу трёх видов кавычек и сказать, когда какие нужны;
- предсказать, что произойдёт с переменной без кавычек, и назвать два механизма, которые её испортят;
- пользоваться подстановками параметров вместо вызова внешних команд;
- читать файл построчно так, чтобы данные не искажались;
- объяснить разницу
"$@"и"$*"и почему это важно в скриптах.
2. Зачем это нужно и где применяется
Bash — основной язык автоматизации в эксплуатации, потому что он есть везде. И он же язык, в котором легче всего написать код, работающий «в обычном случае» и ломающийся на пробеле в имени файла.
Скрипт сломался на данных. Классика: работал год, потом в каталоге появился файл с пробелом, и
rm $FILE удалил не то. Причина не в редком случае, а в отсутствии кавычек — и это ловится
пониманием, а не тестами.
Вы читаете чужие скрипты. В блоках 09–14 будет много готовых скриптов: в конвейерах, в
плейбуках, в контейнерах. Читать ${VAR:-default} и ${PATH##*/} придётся постоянно.
Скрипты уже встречались. В уроках 02.05, 02.06 и 03.07 мы писали хуки и небольшие скрипты. Там
работали интуитивно; здесь разбираемся, что именно происходит, — и в уроке 03.07 уже видели, чем это
кончается: моя проверка секретов ложно срабатывала из-за поведения xargs с пустым вводом.
Обработка вывода команд. Вся работа в командной строке — это подстановки и разбиение на слова. Не понимая их, вы будете получать загадочные результаты и списывать это на «bash странный».
3. Теория (30 минут)
3.1. Что такое скрипт и с чего он начинается
Скрипт — текстовый файл с командами. Чтобы его можно было запускать как программу, нужны две вещи:
#!/usr/bin/env bash # строка-указатель интерпретатора (shebang)
chmod +x script.sh # право на исполнение (урок 02.04)
Почему #!/usr/bin/env bash, а не #!/bin/bash: путь к bash различается между системами
(в macOS штатный /bin/bash — версии 3.2, а установленный из Homebrew лежит иначе). env находит
его в PATH.
Отдельно про выбор интерпретатора. Если написать #!/bin/sh, скрипт будет выполняться не bash,
а POSIX-совместимой оболочкой — и часть конструкций из этого урока не заработает. Мы уже видели это
в уроке 02.01: в Alpine оболочка BusyBox ash, и command -v там принимает только один аргумент.
Поэтому правило: если пишете на bash — указывайте bash, и не полагайтесь на то, что sh окажется
им же.
Проверить, что за оболочка выполняет скрипт:
echo "$BASH_VERSION" # непусто только в bash
3.2. Переменные
NAME="значение" # без пробелов вокруг =
echo "$NAME"
Пробелы вокруг = — самая частая первая ошибка: NAME = "значение" bash понимает как вызов команды
NAME с аргументами.
Три уровня видимости, которые надо различать:
| Как задано | Видно в скрипте | Видно дочерним процессам |
|---|---|---|
NAME=значение |
да | нет |
export NAME=значение |
да | да |
local NAME=значение (в функции) |
только в функции | нет |
Это прямое следствие урока 02.05: дочерний процесс получает копию окружения, а обычные
переменные оболочки в окружение не входят. Отсюда понятно, почему переменная, заданная в скрипте, не
видна вызванной из него программе, если не сделать export.
Про имена: по соглашению переменные окружения и «константы» пишут заглавными (PATH, HOME),
локальные переменные скрипта — строчными. Это не требование bash, а способ не переопределить
случайно системную переменную. Особенно опасно переопределить PATH.
3.3. Кавычки: три вида и главное правило
Это центральная тема урока — большая часть ошибок в bash растёт отсюда.
$ V="привет мир"
$ echo "$V" # двойные: подстановки работают
привет мир
$ echo '$V' # одинарные: всё буквально
$V
$ echo $V # без кавычек: подстановка + РАЗБИЕНИЕ
привет мир
В третьем случае вывод выглядит так же, и это обманчиво. Разница проявляется на данных:
$ F="файл с пробелом.txt"
$ ls $F
ls: с: No such file or directory
ls: файл: No such file or directory ← слово разбилось на три
$ ls "$F"
файл с пробелом.txt
Без кавычек bash делает с результатом подстановки две вещи:
- разбиение на слова (word splitting) по символам из
IFS— по умолчанию пробел, табуляция, перевод строки; - раскрытие шаблонов (globbing) — если в значении есть
*,?,[.
Второе тоже проверяется наглядно:
$ S="*"
$ echo $S
a.txt b.txt файл с пробелом.txt ← превратилось в список файлов!
$ echo "$S"
*
Отсюда правило, которое стоит принять как безусловное:
Подстановка переменной или команды всегда берётся в двойные кавычки, кроме случаев, когда вы сознательно хотите разбиения на слова.
Одинарные кавычки нужны, когда текст должен попасть буквально: регулярные выражения, шаблоны для
find, тела команд для sed и awk. Мы уже пользовались этим в уроке 02.06: <<'EOF' вместо
<<EOF там, где текст не должен подставляться.
Внутри двойных кавычек особый смысл сохраняют $, ` и \. Если нужен буквальный доллар,
его экранируют: "цена \$5".
3.4. Подстановки параметров: работа со строками без внешних команд
Bash умеет обрабатывать строки сам. Это не только быстрее (нет запуска процесса), но и надёжнее — нет разбора вывода чужой команды.
Значения по умолчанию:
${VAR:-по-умолчанию} # подставить, если не задана ИЛИ пуста
${VAR-по-умолчанию} # подставить, только если НЕ ЗАДАНА
${VAR:=создать} # подставить и присвоить
${VAR:?сообщение} # прервать с ошибкой, если не задана
Разница двоеточия проверяется прямо:
$ BLANK=""
$ echo "${BLANK:-подставилось}"
подставилось
$ echo "${BLANK-НЕ подставилось}"
← пусто: переменная задана, хоть и пустая
Форма ${VAR:?сообщение} — лучший способ потребовать обязательный параметр:
$ echo "${MUST:?не задана переменная MUST}"
script.sh: line 11: MUST: не задана переменная MUST
Работа со строками. Возьмём путь и разберём его без basename, dirname и sed:
P="/srv/app/logs/error.log"
${#P} → 23 # длина
${P##*/} → error.log # имя файла (жадно с начала)
${P%/*} → /srv/app/logs # каталог (с конца)
${P%.log} → /srv/app/logs/error # убрать расширение
${P/logs/archive} → /srv/app/archive/error.log # заменить первое вхождение
${P//\//_} → _srv_app_logs_error.log # заменить все
${P:5:3} → app # срез: с 5-го символа, 3 символа
Правило запоминания: # убирает с начала (решётка стоит в начале строки на клавиатуре), %
убирает с конца. Одинарный символ — нежадно (короткое совпадение), двойной — жадно.
Практическая польза очевидна в скриптах: вместо NAME=$(basename "$P") пишется NAME="${P##*/}",
и это не запускает процесс. В цикле по тысяче файлов разница заметна.
3.5. Подстановка команд
D=$(date -u +%F) # современная форма
D=`date -u +%F` # устаревшая, вложенность неудобна
Форма $(...) предпочтительна: она читается и вкладывается.
$ echo "$(echo "внешний $(echo внутренний)")"
внешний внутренний
Два свойства, которые надо знать точно.
Подстановка отбрасывает переводы строк в конце. Проверено:
$ printf 'строка1\nстрока2\n\n\n' > multi.txt
$ V=$(cat multi.txt)
$ wc -c < multi.txt # 30 байт в файле
$ echo "${#V}" # 15 символов в переменной
Пятнадцать вместо тридцати: три завершающих перевода строки съедены. Это регулярный источник расхождений при сравнении «файл против переменной».
Кавычки вокруг подстановки так же обязательны, как вокруг переменной:
$ LIST=$(printf 'a\nb c\n')
$ for x in $LIST; do echo "[$x]"; done # [a] [b] [c] — разбилось!
$ for x in "$LIST"; do echo "[$x]"; done # [a\nb c] — одна строка
3.6. Разбиение на слова и IFS
Переменная IFS (internal field separator) задаёт, по каким символам bash разбивает. По умолчанию
это пробел, табуляция и перевод строки. Меняя её, можно разбирать структурированные строки:
$ LINE="root:x:0:0:root:/root:/bin/bash"
$ IFS=: read -r user _ uid _ _ home shell <<< "$LINE"
$ echo "$user $uid $home $shell"
root 0 /root /bin/bash
Здесь три приёма сразу: IFS=: действует только на эту команду; _ — соглашение «этот аргумент не
нужен»; <<< — передача строки на вход (из урока 02.06).
Чтение файла построчно. Правильная форма выглядит громоздко, и у каждой части есть причина:
while IFS= read -r line; do
printf '%s\n' "$line"
done < файл
IFS=(пустое) — не отрезать пробелы по краям строки;-r— не обрабатывать обратный слэш как экранирование;- кавычки вокруг
"$line"— не разбивать и не раскрывать шаблоны.
Что происходит без этого, проверено на файле со пробелом в начале строки и обратным слэшем:
--- while IFS= read -r (правильно) ---
[ пробел в начале]
[обратный\слэш]
--- while read (без IFS= и -r) ---
[пробел в начале] ← пробел съеден
[обратныйслэш] ← слэш съеден
Данные молча испорчены. Поэтому while IFS= read -r line заучивают целиком, как одну конструкцию.
Отдельно: не читайте файлы через for. Запись for line in $(cat файл) разбивает по словам, а
не по строкам, и ломается на пробелах.
3.7. Массивы и "$@" против "$*"
Когда данных несколько — список файлов, набор аргументов — их держат в массиве, а не в строке с пробелами. Строка ломается ровно на том, на чём ломается незакавыченная переменная: на пробеле внутри элемента.
files=("a.txt" "файл с пробелом.txt" "b.txt") # три элемента
echo "${#files[@]}" # 3 — число элементов
echo "${files[1]}" # файл с пробелом.txt — по индексу (с нуля)
for f in "${files[@]}"; do printf '[%s]\n' "$f"; done
Ключевая форма — "${files[@]}" в кавычках: она раскрывается в отдельные элементы, каждый как
одно слово, даже если внутри пробелы. Без кавычек (${files[@]}) снова происходит разбиение, и
элемент с пробелом разваливается — та же ошибка, что в разделе 3.3, только с массивом.
Отсюда же разница двух особых переменных, которую надо знать точно, потому что она про аргументы
скрипта. Внутри скрипта или функции $@ и $* содержат все переданные аргументы, но ведут себя
по-разному в кавычках:
"$@"раскрывается в отдельные аргументы ("$1""$2"…) — каждый сохраняется целиком;"$*"склеивает всё в одну строку через первый символIFS(обычно пробел).
Проверено на аргументе с пробелом:
show() { echo "через @:"; for a in "$@"; do echo " [$a]"; done
echo "через *:"; for a in "$*"; do echo " [$a]"; done; }
show один "два слова" три
# через @: [один] [два слова] [три] ← три аргумента
# через *: [один два слова три] ← один!
Практическое правило: почти всегда нужен "$@" — когда вы передаёте аргументы дальше
(команда "$@"), они должны сохраниться как есть. "$*" берут редко и осознанно — например, чтобы
собрать аргументы в одну строку для сообщения в журнал. Голые $@ и $* без кавычек не используют
никогда: они теряют границы аргументов.
3.8. Арифметика
$ N=7
$ echo "$(( N * 3 ))" # 21
$ echo "$(( N / 2 ))" # 3 — целочисленное деление!
$ echo "$(( N % 2 ))" # 1
$ echo "$(( ++N ))" # 8, и N теперь 8
Внутри $(( )) знак $ перед именем переменной не нужен, кавычки внутри не требуются.
Ключевое ограничение: bash считает только целыми числами. $(( 7 / 2 )) даёт 3, а не 3.5.
Для дробных нужны внешние средства (awk, bc) — или, если расчётов много, это признак, что задача
уже не для bash (об этом в разделе 3.9).
Сравнения в условиях разберём в уроке 04.02; здесь достаточно знать, что для чисел используется
(( )), а для строк — [[ ]].
3.9. Когда bash — неподходящий инструмент
Честная граница применимости, чтобы не пришлось переписывать:
| Признак | Что делать |
|---|---|
| нужны дробные вычисления, статистика | Python (блок 06) |
| разбор JSON или YAML | jq/yq (урок 04.05), при сложной логике — Python |
| скрипт перевалил ~200–300 строк | пора переписать на Python |
| нужны структуры данных сложнее массива строк | Python |
| нужна работа с HTTP API с обработкой ошибок и повторами | Python |
| нужны параллельные вычисления с синхронизацией | не bash |
Bash хорош там, где задача — склеить существующие программы: запустить, передать вывод дальше, проверить код возврата. Как только начинается собственная логика с данными, он становится хуже Python по всем параметрам, кроме доступности.
Практический ориентир: если в скрипте появляется третий вложенный цикл или разбор структурированных данных руками — остановитесь и посмотрите, не пора ли менять инструмент.
3.10. Примеры и варианты реализации
Задача: написать и запустить скрипт.
| Среда | Как решается | Что нужно иметь |
|---|---|---|
| Локальный стенд | Скрипт на своей машине; для проверки в «чужой» оболочке — контейнер | bash, редактор |
| VPS / bare-metal | То же; важно, что версия bash на сервере может быть старее | сервер из блока 02 |
| Yandex Cloud | Скрипты в cloud-init и в конвейерах; там оболочка может быть не bash |
облачный аккаунт |
Локальный стенд. Здесь и пишем. Полезно сразу завести привычку проверять скрипт в контейнере с
другой оболочкой: docker run --rm -v "$PWD:/w" -w /w alpine:3 sh script.sh быстро показывает, что
скрипт полагался на bash, хотя объявлен как sh.
VPS / bare-metal. Существенное отличие — версия bash. На старых системах может не быть
конструкций из новых версий (например, ассоциативных массивов до bash 4). Проверять:
bash --version. Отсюда практическое правило: если скрипт поедет на неизвестные машины, либо
проверяйте версию в начале, либо не используйте свежие возможности.
Yandex Cloud. Скрипты попадают в cloud-init (урок 02.02) и в конвейеры CI. Там их выполняет
не ваша интерактивная оболочка, а неинтерактивная, часто /bin/sh, и без вашего окружения.
Отсюда две привычки: явный shebang и никакой опоры на переменные вашей сессии.
Что выбрать. Bash одинаков во всех средах; различаются его версия и то, какая оболочка окажется
за sh. Поэтому существенны две вещи: указывать интерпретатор явно и проверять скрипт там, где он
будет работать.
4. Практика на занятии (30 минут)
Шаг 1 (5 минут). Первый скрипт и окружение.
- Что ввести:
mkdir -p ~/lab/04.01 && cd ~/lab/04.01
cat > env.sh <<'EOF'
#!/usr/bin/env bash
echo "версия bash: $BASH_VERSION"
LOCAL_VAR="только тут"
export EXPORTED_VAR="видно детям"
echo "внутри скрипта: LOCAL_VAR=$LOCAL_VAR EXPORTED_VAR=$EXPORTED_VAR"
bash -c 'echo "в дочернем: LOCAL_VAR=[$LOCAL_VAR] EXPORTED_VAR=[$EXPORTED_VAR]"'
EOF
chmod +x env.sh && ./env.sh
- Зачем: увидеть разницу обычной и экспортированной переменной — дочерний процесс получает копию окружения, а не всех переменных оболочки.
- Ожидаемый результат: внутри скрипта видны обе переменные; в дочернем
bash -cLOCAL_VARпуста ([]), аEXPORTED_VARвидна. - Если не так:
LOCAL_VARвидна и в дочернем — использован<<EOFвместо<<'EOF', и bash подставил значение при создании файла; повторите с кавычками в heredoc. - Ответьте: почему
LOCAL_VARв дочернем процессе пуста? Как это связано с уроком 02.05?
Шаг 2 (8 минут). Кавычки: увидеть поломку.
- Что ввести:
mkdir -p qt && cd qt && touch "файл с пробелом.txt" a.txt b.txt
F="файл с пробелом.txt"
ls $F # сломается — прочитайте сообщение
ls "$F" # работает
S="*"
echo $S # раскрылось в список файлов!
echo "$S" # звёздочка
V="привет мир"; echo "$V"; echo '$V'; echo $V
cd ..
- Зачем: увидеть два механизма, портящих незакавыченную подстановку: разбиение на слова (пробел
в имени) и раскрытие шаблона (значение
*). - Ожидаемый результат:
ls $Fдаёт три ошибки «No such file» (слово разбилось);ls "$F"находит файл;echo $Sпечатает список файлов,echo "$S"— звёздочку. - Если не так:
echo $Sнапечатал*, а не список — в каталоге нет файлов; шаблон раскрывается только при наличии совпадений. - Ответьте: для трёх видов кавычек — работают ли подстановки, есть ли разбиение, раскрываются ли шаблоны?
Шаг 3 (7 минут). Подстановки параметров вместо внешних команд.
- Что ввести:
P="/srv/app/logs/error.log"
echo "длина: ${#P}"
echo "имя файла: ${P##*/}"
echo "каталог: ${P%/*}"
echo "без .log: ${P%.log}"
echo "замена: ${P/logs/archive}"
echo "все слэши: ${P//\//_}"
echo "через basename: $(basename "$P")"
unset OPT; echo "по умолчанию: ${OPT:-значение}"
echo "обязательная: ${REQUIRED:?не задана}" # прервётся — это ожидаемо
- Зачем: обрабатывать строки средствами bash, без
basename/dirname/sed— быстрее и надёжнее (нет разбора чужого вывода). - Ожидаемый результат:
${P##*/}даётerror.log,${P%/*}—/srv/app/logs; совпадает сbasename; последняя строка прерывает выполнение с текстомREQUIRED: не задана. - Если не так:
${P##*/}вернул весь путь — перепутаны#(с начала) и%(с конца). - Ответьте: чем
${P##*/}лучше$(basename "$P")в цикле по тысяче файлов?
Шаг 4 (7 минут). Чтение строк без порчи данных.
- Что ввести:
printf ' пробел в начале\nобратный\\слэш\nобычная строка\n' > lines.txt
echo "--- правильно ---"
while IFS= read -r line; do printf ' [%s]\n' "$line"; done < lines.txt
echo "--- без IFS= и -r ---"
while read line; do printf ' [%s]\n' "$line"; done < lines.txt
echo "--- через for (так делать нельзя) ---"
for line in $(cat lines.txt); do printf ' [%s]\n' "$line"; done
LINE="root:x:0:0:root:/root:/bin/bash"
IFS=: read -r user _ uid _ _ home shell <<< "$LINE"
echo "$user $uid $home $shell"
- Зачем: увидеть, что
while readбезIFS=и-rмолча портит данные, аforпо$(cat)читает по словам, — и почемуwhile IFS= read -rзаучивают целиком. - Ожидаемый результат: в «правильно» сохранены пробел в начале и обратный слэш; в «без IFS= и -r»
они съедены; в «через for» строки разбиты по словам; разбор по
:печатаетroot 0 /root /bin/bash. - Если не так: все три вывода одинаковы — в данных нет ведущих пробелов или слэша; используйте
printfиз шага дословно. - Ответьте: что именно испортилось во втором и третьем случае?
Шаг 5 (3 минуты). Подстановка команд и переводы строк.
- Что ввести:
printf 'строка1\nстрока2\n\n\n' > multi.txt
V=$(cat multi.txt)
echo "в файле байт: $(wc -c < multi.txt)"
echo "в переменной символов: ${#V}"
- Зачем: увидеть, что подстановка команд
$(...)отбрасывает завершающие переводы строк — частая причина расхождения «файл против переменной». - Ожидаемый результат: в файле байт заметно больше, чем символов в переменной (три завершающих перевода строки съедены).
- Если не так: числа равны — в файле не было завершающих пустых строк; добавьте
\n\nвprintf. - Ответьте: куда делись символы и когда это может создать проблему?
Проверка результата. Вы воспроизвели поломку из-за отсутствия кавычек в двух вариантах
(пробел и шаблон), заменили basename подстановкой параметра, и увидели, как while read без флагов
портит данные.
5. Домашняя работа (1–2 часа)
Задание. Напишите скрипт, устойчивый к «плохим» данным.
- Скрипт разбора путей. Образец для сверки —
examples/04.01-bash-syntax/paths-reference.sh; открывайте после того, как напишете свой. В нём же оставлена настоящая находкаshellcheckпро SC2295 — случай, когда внутренние кавычки в${base%."$ext"}обязательны. Напишитеpaths.sh, который принимает список путей аргументами и для каждого выводит: имя файла, каталог, расширение, имя без расширения. Только подстановками параметров, безbasename,dirname,sed,awk. Проверьте на путях с пробелами, с точками в имени каталога, без расширения, с несколькими точками. - Эксперимент про кавычки. Стенд для опытов —
examples/04.01-bash-syntax/quoting-demo.sh: там незакавыченные переменные оставлены намеренно, и в начале файла объяснено, почему подавлениеshellcheckдля всего файла записано именно так. Проверьте гипотезу: «отсутствие кавычек ломает скрипт двумя разными способами». Создайте каталог с файлами, включая файл с пробелом и файл с именем*, и покажите на своём выводе оба механизма: разбиение на слова и раскрытие шаблона. Оформите карточкой эксперимента из урока 01.07. Отдельно ответьте: почему файл с именем*особенно опасен? - Эксперимент про
while read. Проверьте, чтоwhile read lineбезIFS=и-rискажает данные. Подготовьте файл, где есть: строка с пробелами по краям, строка с обратным слэшем, строка с символом табуляции, пустая строка. Покажите разницу трёх способов чтения (while IFS= read -r,while read,for ... in $(cat)). Сведите в таблицу, что теряется. - Обязательные и необязательные параметры. Напишите
backup.sh, который берёт из окружения:SRC(обязательно, иначе внятная ошибка через${VAR:?}),DST(по умолчанию/tmp/backup),KEEP(по умолчанию 7). Покажите три запуска: безSRC, только сSRC, со всеми переменными. - Сравнение с внешними командами. Измерьте разницу: цикл на 2000 итераций, в котором имя файла
получается через
${P##*/}против$(basename "$P"). Используйтеtime. Приведите числа и объясните разницу. - Проверка в другой оболочке. Возьмите свой
paths.sh, поменяйте shebang на#!/bin/shи запустите в контейнере Alpine:docker run --rm -v "$PWD:/w" -w /w alpine:3 sh paths.sh /a/b.txt. Опишите, что сломалось и почему. Вернитеbashи объясните, что означает эта проверка для скриптов, которые поедут в контейнеры. - Своя шпаргалка. Соберите таблицу подстановок параметров с примером на каждую (минимум
десять форм), которые вы реально выполнили, — по шаблону
examples/04.01-bash-syntax/expansion-cheatsheet.md. Ценность файла в колонке «результат»: пока не увидишь своими глазами, что${x:-знач}и${x:=знач}ведут себя по-разному, разница не запоминается.
Критерии приёмки.
-
paths.shработает на всех четырёх видах путей; вывод приложен. - В
paths.shнет вызововbasename,dirname,sed,awk. - Карточка эксперимента показывает оба механизма поломки без кавычек.
- Объяснено, почему файл с именем
*особенно опасен. - Таблица потерь при чтении заполнена по четырём видам строк и трём способам чтения.
-
backup.shвыдаёт внятную ошибку безSRCи подставляет значения по умолчанию. - Приведены числа замера
${P##*/}противbasenameс объяснением разницы. - Описано, что сломалось при запуске под
shв Alpine. - Шпаргалка содержит минимум десять форм подстановок, все проверены запуском.
Файлы для работы: blocks/04-bash/examples/04.01-bash-syntax/ — примеры к этому уроку в репозитории курса.
6. Частые ошибки и подводные камни
Пробелы вокруг =. Симптом: NAME: command not found. Причина: NAME = "x" — это вызов
команды. Что делать: NAME="x" без пробелов.
Подстановка без кавычек. Симптом: скрипт работает, пока в данных нет пробелов и шаблонов, потом
ломается или удаляет не то. Причина: разбиение на слова и раскрытие шаблонов. Что делать: всегда
"$VAR" и "$(команда)".
Чтение файла через for. Симптом: строки «разъезжаются» по словам. Причина: for идёт по
словам, а не по строкам. Что делать: while IFS= read -r line.
while read без IFS= и -r. Симптом: молча теряются пробелы по краям и обратные слэши.
Причина: read по умолчанию обрезает и экранирует. Что делать: заучить конструкцию целиком.
Ожидать дробную арифметику. Симптом: $(( 7 / 2 )) даёт 3. Причина: bash считает целыми.
Что делать: awk/bc для дробей; при большом объёме расчётов — Python (блок 06).
Считать, что подстановка сохраняет переводы строк в конце. Симптом: сравнение файла с
переменной не сходится. Причина: $(...) отбрасывает завершающие переводы строк. Что делать:
знать об этом; сравнивать через diff или добавлять маркер.
Забыть export и удивляться. Симптом: переменная не видна вызванной программе. Причина: обычные
переменные оболочки не входят в окружение (урок 02.05). Что делать: export для того, что нужно
дочерним процессам.
Указать #!/bin/sh, а писать на bash. Симптом: скрипт работает у вас и падает в контейнере или
на другой системе. Причина: за sh может быть другая оболочка. Что делать: #!/usr/bin/env bash;
проверять в целевой среде.
Переопределить системную переменную. Симптом: перестают находиться команды. Причина: скрипт
задал свою PATH (или IFS и не вернул). Что делать: локальные переменные строчными буквами;
IFS менять только для одной команды.
7. Проверка усвоения
- Зачем нужен shebang и почему
#!/usr/bin/env bashпредпочтительнее#!/bin/bash? - Что произойдёт при
NAME = "значение"и почему? - Чем
NAME=xотличается отexport NAME=x? Как это связано с уроком 02.05? - Назовите три вида кавычек и для каждого — работают ли подстановки и происходит ли разбиение.
- Какие два механизма портят подстановку без кавычек? Приведите пример на каждый.
- Почему файл с именем
*в каталоге опасен для скрипта без кавычек? - Чем
${VAR:-x}отличается от${VAR-x}? - Как потребовать обязательную переменную одной конструкцией?
- Что делают
${P##*/}и${P%/*}? Как запомнить, где#, а где%? - Чем
${P##*/}лучше$(basename "$P")? - Что происходит с переводами строк в конце при
V=$(cat файл)? - Разберите
while IFS= read -r line: за что отвечает каждая часть? - Почему нельзя читать файл через
for line in $(cat файл)? - Что даст
$(( 7 / 2 ))и почему? - Назовите три признака того, что задача уже не для bash.
8. Материалы
Первоисточники:
- Bash Reference Manual — официальное руководство. Для этого урока: разделы Quoting, Shell Parameter Expansion, Word Splitting. Читается тяжело, но это единственный точный источник.
man bash— то же локально; поиск по разделуParameter Expansionзакрывает большинство вопросов.- Bash Reference Manual: Shell Parameter Expansion — все формы подстановок параметров в одном месте; стоит держать под рукой.
Практические руководства:
- Google Shell Style Guide — соглашения о написании скриптов: когда bash уместен, а когда пора на другой язык, как оформлять. Короткий и полезный документ; часть правил мы применяем в этом блоке.
- BashFAQ и BashPitfalls (Greg's Wiki) — разбор типовых ошибок с объяснениями. Список ловушек, из которых половина про кавычки; полезно прочитать целиком один раз.
Дальше в курсе:
- Условия, циклы и функции — урок 04.02.
set -euo pipefail,trapи надёжность — урок 04.03 (частично уже разобрано в 02.06).shellcheckнаходит большинство ошибок из этого урока автоматически — урок 04.07.
← Предыдущий: 03.08. Монорепозиторий и полирепозиторий; структура инфраструктурных репозиториев · Блок 04 · Следующий: 04.02. Условия, циклы, функции, коды возврата →
Материалы для практики
Скрипты, шпаргалки и заготовки к заданиям урока. Текст урока — на этой странице.
-
Материалы к уроку 04.01 «Синтаксис bash переменные, кавычки, подстановки».zip