Конвейеры 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.
Частые вопросы
Pipe передаёт строки или байты?
Ядро передаёт байты. Деление на строки и интерпретация кодировки принадлежат программам по обе стороны канала.
Почему uniq не считает все одинаковые значения?
uniq объединяет только соседние строки. Перед подсчётом неупорядоченного набора обычно нужен sort.
Когда лучше использовать временный файл?
Когда результат нужно повторно исследовать, проверить независимо, передать между запусками или сохранить как артефакт. Pipeline удобен для однопроходной обработки, но не заменяет осмысленное хранение.