Язык shell: кавычки, подстановки, коды возврата
TL;DR: Shell обрабатывает строку до передачи команде: раскрывает переменные, разворачивает glob (
*), разбивает по пробелам. Одинарные кавычки'...'отключают всё. Двойные"..."— только glob. Условия (if) работают на кодах возврата команд: 0 = true, не-0 = false.[— это обычная программа, а не синтаксис.
Зачем это знать
Без понимания того, как shell разбирает строку, вы будете писать скрипты вслепую — «работает, но не знаю почему» или «ломается, и не понимаю где». Конкретно:
grep r.*t /etc/passwdработает… пока в текущей директории не появятся файлыr.input,r.output— тогда shell развернётr.*tв имена файлов до передачи grepecho $100выведет00, потому что shell подставит$1(пустую переменную) +00if [ $var = "hi" ]упадёт с ошибкой, если$varпустая — потому что[получит[ = hi ]
Как shell обрабатывает строку
Каждая строка проходит через цепочку подстановок до того, как команда её увидит:
Вы вводите: echo "Hello $USER" *.txt
│ │
▼ ▼
1. Раскрытие "Hello alice" *.txt
переменных ($USER → alice)
│ │
▼ ▼
2. Раскрытие "Hello alice" notes.txt todo.txt
glob (*) (файлы в текущей директории)
│ │ │
▼ ▼ ▼
3. Разбиение Параметр 1 Пар.2 Пар.3
по пробелам "Hello alice"
│ │ │
▼ ▼ ▼
echo получает: echo "Hello alice" notes.txt todo.txt
Команда echo не знает, что были переменные и glob — она видит только готовые аргументы. Именно поэтому кавычки критичны: они управляют тем, какие шаги shell выполняет.
Кавычки: три уровня защиты
Одинарные кавычки '...' — полная защита
Всё внутри одинарных кавычек — литерал. Shell ничего не раскрывает, не подставляет, не разбивает:
echo '$USER' # → $USER (буквально)
echo 'Hello World' # → Hello World (пробелы сохранены)
grep 'r.*t' /etc/passwd # → grep ищет regex r.*t (не имена файлов!)
echo '$100' # → $100 (не пытается подставить $1)Правило: если нужен текст буквально и без подстановок — одинарные кавычки. Это самый безопасный вариант по умолчанию.
Двойные кавычки "..." — частичная защита
Внутри двойных кавычек shell раскрывает переменные ($VAR) и подстановки команд ($(cmd)), но не раскрывает glob (*, ?) и не разбивает по пробелам:
echo "Hello $USER" # → Hello alice (переменная раскрыта)
echo "Files: *.txt" # → Files: *.txt (glob НЕ раскрыт)
echo "There are $(wc -l < f) lines" # → подстановка команды работает
echo "Path: $PATH" # → Path: /usr/bin:/usr/local/bin:...Правило: если нужны переменные, но не нужен glob — двойные кавычки. Это стандартный выбор для строк с переменными.
Обратная косая черта \ — защита одного символа
echo \$USER # → $USER (экранирован один $)
echo "Price: \$100" # → Price: $100 (внутри двойных кавычек)
echo don\'t # → don't (одинарная кавычка вне кавычек)Сводка
| Синтаксис | Переменные | Glob (*) | Пробелы | Когда использовать |
|---|---|---|---|---|
'text' | ❌ не раскрывает | ❌ | Сохраняет | Regex, литеральные строки |
"text" | ✅ раскрывает | ❌ | Сохраняет | Строки с переменными |
\x | Экранирует один символ | — | — | Отдельные спецсимволы |
| без кавычек | ✅ | ✅ | Разбивает | Только когда нужны glob и word splitting |
Одинарная кавычка внутри одинарных кавычек
Это единственный сложный случай — внутри '...' нельзя поставить '. Два решения:
# Через обратную косую черту (вне кавычек)
echo 'it'\''s working' # it's working
# ^^^ ^^^^^^^^^^
# lit \' lit
# Через двойные кавычки
echo "it's working" # it's workingGlob: раскрытие имён файлов
Shell раскрывает шаблоны *, ?, [...] в имена существующих файлов до передачи команде:
# В директории: notes.txt todo.txt readme.md
echo *.txt # → notes.txt todo.txt (shell подставил)
echo '*.txt' # → *.txt (литерал, без раскрытия)
ls *.md # shell раскроет в: ls readme.md
rm *.log # если *.log не совпадает ни с чем → ошибка (или литерал *.log)Подвох с regex: grep r.*t file — shell может раскрыть r.*t в имена файлов, если они совпадут. Всегда кавычьте regex:
grep 'r.*t' /etc/passwd # ✅ grep получает regex r.*t
grep r.*t /etc/passwd # ⚠️ может сломаться, если есть файл r.outputКоды возврата: 0 = успех
Каждая Unix-программа при завершении возвращает числовой код. Shell хранит его в $?:
ls /etc > /dev/null
echo $? # → 0 (успех)
ls /nonexistent > /dev/null 2>&1
echo $? # → 1 (ошибка: нет такого файла)0 = успех, всё остальное = ошибка. Это фундаментальное соглашение Unix.
Нюанс: не-0 не всегда ошибка
Некоторые программы используют разные коды для разных ситуаций:
| Программа | Код 0 | Код 1 | Код 2 |
|---|---|---|---|
grep | Найдено совпадение | Нет совпадений (не ошибка!) | Реальная ошибка |
diff | Файлы идентичны | Файлы различаются (не ошибка!) | Реальная ошибка |
Поэтому grep -q pattern file || echo "not found" — это нормальная логика, а не обработка ошибки.
exit в скриптах
# Завершить скрипт с кодом ошибки
if [ ! -f "$CONFIG" ]; then
echo "$0: config file not found: $CONFIG" >&2
exit 1
fi
# Код возврата 0 (успех) — по умолчанию, если скрипт дошёл до концаВажно: $? перезаписывается
ls /nonexistent 2>/dev/null
echo $? # → 2 (ошибка ls)
echo $? # → 0 (успех предыдущего echo!)Если нужен код — сохраняйте сразу: result=$?.
Условные операторы работают на кодах возврата
Это ключевой инсайт: if в shell — не сравнение значений, а проверка кода возврата команды.
if command; then
# выполняется если command вернула 0 (успех)
else
# выполняется если command вернула не-0 (ошибка)
fiПоэтому if работает с любой командой, не только с [:
# if с grep (код 0 = найдено)
if grep -q root /etc/passwd; then
echo "root exists"
fi
# if с ping
if ping -c 1 -W 2 8.8.8.8 > /dev/null 2>&1; then
echo "Internet is up"
fi
# if с mkdir
if mkdir -p /tmp/myapp; then
echo "Directory ready"
fiТочка с запятой перед then — это разделитель команд, а не синтаксис if. Без неё shell передаст then как аргумент команде [, что приведёт к непонятной ошибке. Альтернатива — then на отдельной строке:
if [ "$1" = "hi" ]; then # ; отделяет команду [ от ключевого слова then
echo "hi"
fi
# Эквивалент без ;
if [ "$1" = "hi" ]
then
echo "hi"
fi[ — это программа, а не синтаксис
Когда вы пишете if [ $x = "hi" ], символ [ — это настоящая программа (/usr/bin/[), также известная как test. Она сравнивает аргументы и возвращает 0 (true) или 1 (false):
# Эти записи эквивалентны:
if [ "$x" = "hi" ]; then ...
if test "$x" = "hi"; then ...
# [ — это команда, пробелы обязательны:
[ "$x" = "hi" ] # ✅ четыре аргумента: "$x", =, "hi", ]
["$x" = "hi"] # ❌ shell ищет команду ["$x"Именно поэтому пробелы вокруг [ и ] обязательны — это аргументы команды, а не скобки в синтаксисе.
[ vs [[
[[ — это ключевое слово bash (не внешняя программа), работает надёжнее:
[ ... ] (test) | [[ ... ]] (bash) | |
|---|---|---|
| Стандарт | POSIX (любой sh) | Только bash |
| Без кавычек у переменных | Ломается при пустых значениях | Работает (но кавычки всё равно хорошая привычка) |
| Regex | Нет | [[ $str =~ ^[0-9]+$ ]] |
| Логика | -a (AND), -o (OR) | &&, || |
# POSIX (переносимый) — всегда кавычки
if [ "$var" = "value" ]; then ...
# Bash — надёжнее, поддерживает regex
if [[ $var == value ]]; then ...
if [[ $input =~ ^[0-9]+$ ]]; then echo "число"; fiПроизводительность: хотя
/usr/bin/[существует как отдельная программа, в bash (и большинстве современных sh)test/[встроен в оболочку — shell не запускает отдельный процесс для каждой проверки.
Операторы test / [
[ поддерживает десятки проверок. Они делятся на три категории: файлы, строки, числа.
Проверка файлов — тип:
| Оператор | True, если… |
|---|---|
-e file | Файл существует (любого типа) |
-f file | Обычный файл (не каталог, не устройство) |
-d file | Каталог |
-h file | Символическая ссылка |
-b file | Блочное устройство |
-c file | Символьное устройство |
-p file | Именованный канал (FIFO) |
-S file | Сокет |
Нюанс с symlink: все проверки кроме
-hпроверяют объект, на который указывает ссылка, а не саму ссылку. Еслиlink→ обычный файл, то[ -f link ]вернёт true.
Проверка файлов — права и свойства:
| Оператор | True, если… |
|---|---|
-r file | Есть право на чтение |
-w file | Есть право на запись |
-x file | Есть право на исполнение |
-s file | Файл не пустой (размер > 0) |
-u file | Установлен бит setuid |
-g file | Установлен бит setgid |
-k file | Установлен sticky bit |
Сравнение файлов (бинарные):
| Оператор | True, если… |
|---|---|
f1 -nt f2 | f1 новее f2 (newer than) |
f1 -ot f2 | f1 старше f2 (older than) |
f1 -ef f2 | Одинаковый inode (жёсткая ссылка) |
# Практические примеры
[ -f "$CONFIG" ] || error "Config not found"
[ -d "$BACKUP_DIR" ] || mkdir -p "$BACKUP_DIR"
[ -x "$BINARY" ] || error "$BINARY is not executable"
[ "$SRC" -nt "$OBJ" ] && recompile "$SRC" # пересобрать, если исходник новееСтроки:
| Оператор | True, если… |
|---|---|
str1 = str2 | Строки равны |
str1 != str2 | Строки не равны |
-z str | Строка пустая |
-n str | Строка не пустая |
Числа:
| Оператор | Значение |
|---|---|
-eq | Равно |
-ne | Не равно |
-lt | Меньше |
-gt | Больше |
-le | Меньше или равно |
-ge | Больше или равно |
Ключевое различие = vs -eq: оператор = сравнивает строки, -eq сравнивает числа:
[ "01" = "1" ] # false! (строки "01" и "1" различаются)
[ "01" -eq "1" ] # true (числа 01 и 1 равны)
[ "10" -gt "9" ] # true (числовое сравнение)
# Но: [ "10" > "9" ] — НЕ сравнение, а перенаправление в файл "9"!Логика внутри [: -a (AND), -o (OR), ! (NOT):
# -a и -o — внутри одной команды [
[ -f "$FILE" -a -r "$FILE" ] # файл существует И читаем
[ "$1" = "hi" -o "$1" = "bye" ] # одно ИЛИ другое
# ! — инверсия
[ ! -d "$DIR" ] && mkdir -p "$DIR" # если НЕ каталог → создать
[ ! "$1" = "hi" ] # не равно (эквивалент !=)
# В [[ используйте && и || вместо -a и -o:
[[ -f "$FILE" && -r "$FILE" ]]Логические операторы && и ||
Работают на кодах возврата — это сокращённая форма if:
# && — выполнить вторую команду, если первая успешна (код 0)
mkdir -p /tmp/app && echo "Created" # echo выполнится только при успехе mkdir
# || — выполнить вторую команду, если первая неуспешна (код не-0)
cd /app || exit 1 # exit выполнится только если cd не удался
# Комбинация (паттерн: сделай или умри)
cd "$DEPLOY_DIR" || { echo "Cannot cd to $DEPLOY_DIR" >&2; exit 1; }elif — цепочка условий
elif позволяет проверять несколько условий последовательно. Выполняется только первая истинная ветка:
if [ "$1" = "hi" ]; then
echo "Привет"
elif [ "$1" = "bye" ]; then
echo "Пока"
elif [ "$1" = "help" ]; then
usage
else
echo "Неизвестная команда: $1" >&2
exit 1
fiСовет: если
elif-веток больше двух-трёх — переходите наcase, он читается лучше и работает через сопоставление с шаблонами, а не коды возврата.
case — множественное ветвление
case сравнивает значение с шаблонами (glob, не regex). Каждая ветка завершается ;;:
case "$1" in
start)
echo "Starting..."
;;
stop|restart) # | = ИЛИ (несколько паттернов)
echo "Stopping..."
;;
-h|--help)
usage
;;
*) # default (любое значение)
echo "Unknown: $1" >&2
exit 1
;;
esaccase — основной способ обработки подкоманд и форматов в скриптах. Часто встречается внутри getopts (см. write-scripts).
Циклы
В shell два вида циклов, и они работают по-разному: for итерирует по списку слов, while проверяет код возврата команды.
for — итерация по списку
for присваивает переменной каждое значение из списка и выполняет тело цикла:
for str in one two three four; do
echo "$str"
done
# → one, two, three, four (по одному на строку)Список после in — это обычные слова, которые shell обрабатывает по стандартным правилам (раскрытие переменных, glob). Именно glob делает for мощным инструментом для работы с файлами:
# Итерация по файлам — главная сила for в shell
for f in /etc/*.conf; do
[ -f "$f" ] || continue # пропустить, если glob не раскрылся
echo "Config: $f ($(wc -l < "$f") lines)"
done
# Диапазон (bash)
for i in {1..5}; do echo "$i"; done
# C-style (bash) — для числовых итераций
for ((i=0; i<10; i++)); do
echo "Step $i"
doneКлючевое отличие:
forне проверяет коды возврата — он просто перебирает список. Если список пуст (glob ничего не нашёл), тело цикла не выполняется.
Подробнее о формах for с практическими примерами → 04-shell-and-scripting.
while — цикл на коде возврата
Как и if, цикл while проверяет код возврата команды-условия:
# while выполняется, пока команда возвращает 0 (успех)
while command; do
# тело цикла
doneПоэтому while работает с любой командой, не только с [:
# while с grep — читать файл, пока в последних 10 строках есть "firstline"
while tail -10 "$FILE" | grep -q firstline; do
echo "newline" >> "$FILE"
done
# while с read — построчное чтение (самый частый паттерн)
while IFS= read -r line; do
echo "Line: $line"
done < /etc/hostsuntil — инверсия while: выполняется, пока команда возвращает не-0:
until ping -c 1 -W 2 server.local > /dev/null 2>&1; do
echo "Waiting for server..."
sleep 5
done
echo "Server is up!"break выходит из цикла досрочно, continue переходит к следующей итерации.
Рекомендация Уорда: если вам понадобился сложный
while— возможно, пора переключиться на Python или awk.forс glob-ом (for f in *.txt) — это сила shell.whileс арифметикой или сложными условиями — уже не его территория.
Shebang: как запускаются скрипты
#!/bin/sh # POSIX shell (максимальная переносимость)
#!/bin/bash # Bash (массивы, [[ ]], regex, ${var,,})
#!/usr/bin/env bash # Bash из PATH (не привязан к /bin/bash)
#!/usr/bin/env python3 # PythonКогда вы запускаете ./myscript, ядро читает первую строку, видит #! и запускает указанный интерпретатор: фактически выполняется /bin/sh myscript.
#!/usr/bin/env bash — env ищет bash в текущем $PATH. Удобно, если bash установлен не в /bin/bash (macOS, нестандартные системы). Минус: первый найденный bash может оказаться не тем, что нужен.
# Сделать скрипт исполняемым
chmod +x script.sh # чтение + выполнение для всех
chmod 700 script.sh # чтение + выполнение только для владельцаПодводные камни
| Ловушка | Почему | Решение |
|---|---|---|
echo $100 → 00 | $1 подставляется (пусто) + 00 | echo '$100' или echo "\$100" |
[ $var = hi ] при пустом $var | [ получает [ = hi ] (нет первого аргумента) | [ "$var" = hi ] (всегда кавычки!). В legacy-скриптах встречается [ x"$var" = x"hi" ] — префикс x гарантирует непустой аргумент даже в старых shell без корректной обработки "". Сегодня кавычек достаточно |
grep r.*t file ломается случайно | Shell развернул r.*t в имена файлов | grep 'r.*t' file |
if [$var = "hi"] | [ — программа, нужны пробелы | if [ "$var" = "hi" ] |
echo $? дважды → второй раз 0 | $? перезаписан успешным echo | result=$? сразу после команды |
| Скрипт не запускается | Нет shebang или chmod +x | #!/bin/bash + chmod +x |
Связанные материалы
- shell-internals — подоболочки, sourcing, exec (уровень процессов)
- write-scripts — шаблон скрипта, аргументы, getopts, trap, отладка
- 04-shell-and-scripting — первый скрипт, переменные, циклы, функции