Конвейеры shell: соединяем команды через pipe
Конвейер передаёт stdout одной команды в stdin следующей. Благодаря этому данные не приходится сохранять во временный файл после каждого шага: одна программа выбирает строки,…
Конвейер передаёт stdout одной команды в stdin следующей. Благодаря этому данные не приходится сохранять во временный файл после каждого шага: одна программа выбирает строки, другая сортирует, третья считает итог.
Как shell запускает pipeline
В выражении producer | consumer shell создаёт канал и запускает оба процесса. Producer пишет байты, consumer читает их по мере появления. Команды работают одновременно, поэтому большой поток может обрабатываться постепенно, а не помещаться целиком в память.
printf '%s\n' api worker api scheduler worker api \
| sort \
| uniq -c \
| sort -nrКаждый этап имеет один контракт. Первый создаёт строки, второй делает одинаковые значения соседними, uniq -c считает группы, последний сортирует числа по убыванию. Удаление sort перед uniq меняет смысл результата: утилита объединяет только соседние повторения.
Коды ошибок в конвейере
По умолчанию Bash возвращает статус последней команды pipeline. Это опасно: producer может упасть, а consumer корректно обработает неполный ввод и вернёт ноль.
set -o pipefail
generate-report | gzip >report.txt.gz
status=$?
printf 'pipeline status=%s\n' "$status"С pipefail конвейер становится неуспешным, если неуспешен любой его элемент. Массив PIPESTATUS в Bash позволяет сразу после выполнения увидеть статусы всех компонентов, но его нужно читать до запуска следующей команды.
Потоковое выполнение и backpressure
Канал имеет ограниченный буфер. Если consumer читает медленно, producer блокируется на записи и не заполняет память бесконечно. Если consumer завершается раньше, producer может получить SIGPIPE. Например, yes | head -n 3 печатает три строки и прекращает бесконечный источник именно благодаря закрытию канала.
Это нормальная механика, но с pipefail раннее завершение consumer иногда делает весь pipeline неуспешным. Перед тем как считать это дефектом, воспроизведите статусы каждого этапа и проверьте ожидаемый контракт.
Ошибка и диагностика
Типичная ошибка — строить pipeline, в котором промежуточная команда добавляет заголовок или прогресс в stdout. Следующий этап принимает служебную строку за данные. Запустите этапы отдельно на маленьком fixture, перенаправьте stderr и проверьте первые байты через head или od. Диагностику промежуточной утилиты нужно отправлять в stderr.
Практика
Создайте поток из имён расширений js, ts и md с повторениями. Соберите pipeline, который выводит частоты по убыванию. Затем вставьте заведомо падающую команду перед wc -l, сравните status с выключенным и включённым pipefail. Объясните, какую ошибку мог скрыть последний успешный этап.
Что важно запомнить
- Pipe соединяет stdout слева со stdin справа; stderr в него сам не попадает.
- Этапы выполняются конкурентно и должны иметь понятный формат входа и выхода.
- Для надёжных Bash-скриптов проверяйте весь pipeline через
pipefail.