Конвейеры 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. Объясните, какую ошибку мог скрыть последний успешный этап.

Что важно запомнить

Источники