Как Bash выполняет команды, раскрывает переменные и соединяет программы

Bash читает командную строку, разбирает её структуру, выполняет подстановки, настраивает ввод и вывод и запускает команды. Терминал при этом является интерфейсом для ввода и отображения, а Bash командным интерпретатором. Когда понимаешь это разделение, становится проще объяснить, почему одинаковый на вид текст иногда даёт разные результаты.

Базовые правила описаны в руководстве GNU Bash. Ниже разберём переменные, кавычки, перенаправления и конвейеры на небольших примерах, которые не требуют прав администратора и не меняют настройки системы.

Что происходит с текстом команды

Оболочка сначала распознаёт слова и операторы, учитывая кавычки и синтаксис. Затем выполняет предусмотренные подстановки. По руководству порядок включает раскрытие фигурных скобок, тильды, параметров, арифметики и команд, после чего могут происходить разделение слов и раскрытие шаблонов имён файлов. Удаление синтаксических кавычек выполняется в конце.

Это не то же самое, что просто разделить всю строку по пробелам. Пробел внутри кавычек может остаться частью аргумента. А текст, полученный из переменной, обычно не разбирается заново как полноценная командная строка с операторами. Повторный разбор через eval является отдельным и опасным действием, особенно для внешних данных.

Как понять, какая команда запускается

type -a printf
type -a cd
command -v bash
printf "%s\n" "$PATH"

Имя может обозначать функцию, встроенную команду или внешний исполняемый файл. В интерактивной оболочке на поведение влияют и алиасы. Для диагностики полезен type -a: он показывает варианты, а не только найденный на диске файл. Если в имени внешней команды есть /, поиск по каталогам PATH не нужен.

Команда cd должна менять каталог текущей оболочки, поэтому обычный запуск отдельной программы не заменил бы её. Дочерний процесс может поменять собственный рабочий каталог, но не каталог своего родителя. Это связано с общей моделью процессов Linux, а не с особенностью отображения терминала.

Переменная оболочки и окружение процесса

name="Alexey"
printf "%s\n" "$name"
export name
bash -c 'printf "%s\n" "$name"'

В присваивании name=»Alexey» вокруг = нет пробелов. Иначе Bash увидит другую синтаксическую конструкцию. Обычная переменная доступна в оболочке и её подокружениях, а export помечает её для передачи в окружение запускаемых программ. Уже работающие независимые процессы от такого изменения ничего автоматически не получают.

Экспортированное значение не становится секретным или защищённым. Пароли и токены нельзя бездумно печатать через env, set или трассировку команд. Среда запуска, журналы и обработчики ошибок должны быть устроены так, чтобы не выдавать чувствительные значения.

Почему значения почти всегда заключают в двойные кавычки

value="my site"
printf "<%s>\n" $value
printf "<%s>\n" "$value"
printf "%s\n" '$value'

В первом printf значение разделится на два аргумента при обычных настройках IFS. Во втором останется одним. В одинарных кавычках $value передаётся буквально. Без двойных кавычек значение также может участвовать в раскрытии шаблонов имён файлов, если содержит соответствующие символы.

Для набора отдельных аргументов в Bash удобны массивы. Они позволяют не собирать команду в строку и не восстанавливать границы аргументов догадками. При передаче массива используйте «${args[@]}», чтобы каждый элемент оставался отдельным аргументом.

args=("one file.txt" "second.txt")
printf "<%s>\n" "${args[@]}"

Подстановка команды и код завершения

today=$(date +%F)
printf "%s\n" "$today"
value=$((10 + 5))
printf "%s\n" "$value"

Конструкция $(…) подставляет стандартный вывод команды, удаляя завершающие переводы строк. Поток ошибок сам по себе в результат не включается. Для целочисленных вычислений служит $((…)); это не универсальная система вычислений с дробными числами.

Код завершения последней команды доступен в $?, но следующая команда его перезапишет. Поэтому проверять или сохранять его нужно сразу. Ноль обычно означает успешное выполнение; ненулевой код может означать не только аварию, но и обычное условие, например отсутствие совпадений у grep.

if grep -q "needle" /dev/null; then
    printf "Совпадение найдено\n"
else
    rc=$?
    printf "grep завершился с кодом %s\n" "$rc"
fi

Что передаёт конвейер

Оператор | соединяет стандартный вывод одной команды со стандартным вводом следующей. Стандартный поток ошибок по умолчанию туда не попадает. Конвейер передаёт поток байтов, а не массив строк или структурированные объекты: интерпретация содержимого зависит от самих программ.

printf "alpha\nbeta\n" | wc -l
false | true
printf "Код без pipefail: %s\n" "$?"
set -o pipefail
false | true
printf "Код с pipefail: %s\n" "$?"

Без pipefail результатом служит код последней команды. С pipefail возвращается код самой правой команды с ненулевым завершением либо ноль, если все успешны. Это не команда немедленной остановки всех участников: их жизненный цикл и обработка ошибок требуют отдельного внимания.

Команды многокомандного конвейера обычно исполняются в отдельных подокружениях. Поэтому изменение переменной внутри цикла после | может не сохраниться у родителя. У Bash есть исключение с lastpipe при отключённом управлении заданиями, так что утверждение «всегда в отдельном процессе» тоже слишком жёсткое.

Почему set -euo pipefail не заменяет обработку ошибок

Опции полезны, но errexit имеет исключения, в том числе для проверок if, частей списков && и || и инверсии через !. Нельзя рассчитывать, что -e завершит скрипт после абсолютно любого ненулевого результата в любой функции и любом контексте.

Для проверки синтаксиса используйте bash -n script.sh, для статического анализа ShellCheck. Трассировку bash -x применяйте только там, где аргументы не содержат секретов: она выводит команды после подстановок. Скрипт с Bash-массивами запускайте Bash, а не произвольным sh.

Надёжная работа с Bash начинается с границ аргументов, понимания потоков и явной проверки ожидаемых результатов. Кавычки, массивы и коды завершения позволяют убрать значительную часть сюрпризов без сложной автоматизации. А поведение сложной конструкции лучше проверить на маленьком безопасном примере до запуска на рабочем сервере.

Источники и ссылки


Комментарии загружаются…