Классическая развилка в терминале: либо смотреть на выполнение команды вживую, либо отправлять вывод в файл, чтобы разобрать потом. Программа tee убирает выбор: данные проходят сквозь неё в stdout и одновременно пишутся в файл, а то и в несколько. Приём старый как мир, но от этого не менее нужный: без tee половина всех длинных операций заканчивается тем, что лог потерян при закрытом терминале или, наоборот, вывода на экране не было - и никто не знает, на каком месте всё встало. В статье разбираем реальную практику: базовые паттерны, sudo и перенаправления, ротация логов, интеграция в скрипты и нетривиальные приёмы, которые делают tee полноценным инструментом наблюдения.

База, запись и наблюдение одновременно

Минимальный паттерн известен каждому: команда | tee файл. Например, apt update | tee update.log показывает процесс на экране и сохраняет его в файл для истории. Флаг -a добавляет к существующему файлу вместо перезаписи: apt upgrade | tee -a update.log, и тогда лог за всю сессию складывается последовательно, а не затирается каждый раз. Множественные приёмники работают без сюрпризов: команда | tee local.log /srv/archive/$(date +%F).log пишет сразу в два места.

Противоположный режим tee -a /dev/null фактически глушит запись, но вывод остаётся; это редкая нужда, но стоит знать, что сам источник не знает о приёмнике: tee прозрачна для потока. Ловушка такая же старая: команда 2>в файл направляет только stderr, а stdout по-прежнему уходит на экран; для совместного захвата используют команда 2>&1 | tee log.txt - сначала слияние потоков, потом tee. Порядок здесь принципиально важен.

Sudo и запись туда, куда вам нельзя

Частая ситуация: скрипт пишет лог в /var/log, куда у обычного пользователя нет прав. Наивный вариант sudo команда | tee /var/log/run.log не работает: перенаправление выполняет оболочка от вашего имени до запуска команды - sudo уже применился к команде, но не к tee, и запись падает с отказом в доступе. Рабочее решение старое, как sudo tee: команда | sudo tee /var/log/run.log направляет вывод через tee под правами root, и файл создаётся корректно. Для дозаписи то же самое с -a: команда | sudo tee -a /var/log/run.log.

Та же конструкция решает задачу записи конфигов: echo "new.setting=1" | sudo tee -a /etc/sysctl.d/99-tune.conf - привычная всем строка, и её хватает на десятилетия. Гигиена требует лишь привычки выводить результат на экран, что tee делает бесплатно: вы видите, что именно ушло в системный файл, и это удалённый родственник операции cat, только в обратную сторону.

Логирование в скриптах, шаблон для cron

В скриптах под cron весьма полезен компактный шаблон: exec > >(tee -a /var/log/task.log) 2>&1 в самом начале перенаправляет вообще весь последующий вывод скрипта одновременно на stdout и в лог. В cron это даёт двойной выигрыш: cron шлёт почту с выводом (или складывает в локальный ящик), а лог при этом копится на диске для ретроспективы. Без подобного трюка расхождение историй о неудачной ночной задаче - классика жанра.

Аккуратность требуется в одном: process substitution >( ) есть в bash и zsh, но не в POSIX sh; скрипт стоит запускать именно под bash, и об этом говорит шебанг #!/bin/bash. Вторая мелочь: буферизация. Программа, заметившая пайп вместо TTY, иногда буферизует вывод большими кусками и заставляет зрителя ждать; stdbuf -oL -eL команда переводит её на построчный режим, и вывод через tee обновляется плавно, строка за строкой.

Операции на глазах и в архиве, типовые сценарии

Первый сценарий - установка и обновления: apt full-upgrade | tee ~/logs/apt-$(date +%F-%H%M).log оставляет честную историю, и через месяц можно ответить, когда именно обновился вон тот подозрительный пакет. Второй сценарий - компиляции и сборки: make -j8 2>&1 | tee build.log позволяет после падения посмотреть последние строки лога, не пересобирая ничего. Третий сценарий - миграции данных: rsync -aP src dst | tee -a sync.log фиксирует список переданных файлов, что используется потом для сверки.

Четвёртый - развёртывание инфраструктуры: terraform apply | tee apply-$(git rev-parse --short HEAD).log привязывает лог применения к коммиту, и это простая гарантия воспроизводимости действий. Пятый - разовые диагностики: iperf3 -c host | tee net-$(date +%s).log оставляет для истории и жалоб провайдеру точные цифры, а не рассказ о том, как всё тормозило.

Границы и смежные инструменты

Справедливости ради: для долговременной записи существуют специализированные инструменты. script команда записывает весь сеанс терминала, включая цвета и интерактивность, и годится для записали-и-показали-коллегам. logger отправляет строку в syslog, и часто правильнее писать туда, чем в файл. perp, daemontools и supervisord сами собирают stdout служб в ротируемые логи без tee. Против всех них tee выигрывает тем, что присутствует на любой системе без установки и не требует ни настройки, ни объяснений.

Ротация логов заслуживает отдельной строчки: tee сам не ротирует файлы, поэтому на долгоживущих логах скрипта смысл прогонять через logrotate. Конфиг на три строки с weekly, rotate 8 и compress избавляет от ежегодной процедуры чистки. И полезно помнить про размер: tee не экономит диск и пишет всё, что проходит; на логах размером в гигабайт стоит фильтровать поток до tee, а не надеяться на ночные уборки.

Итог, самая недооценённая буква в конвейере

Резюме предельно короткое: ставьте tee во все длительные команды, по которым потом могут быть вопросы. Стоимость её близка к нулю, ценность увиденного в процессе и сохранённого на диске - несопоставимо выше. Привычка tee -a любой-длинный-скрипт.log избавила бессчётное число администраторов от перезапуска ночного задания только ради ответа на вопрос "а что там было написано". Звучит негламурно, работает безотказно, и именно такими хочется видеть инструменты в повседневном арсенале.

Раздельный сбор stdout и stderr, аккуратные схемы

В более строгих сценариях ошибки и обычный вывод хотят жить в разных файлах, но оба должны оставаться на экране. Связка с подстановкой процессов решает и это: команда > >(tee stdout.log) 2> >(tee stderr.log >&2) пишет обычный вывод в stdout.log, ошибки в stderr.log, и всё это продолжает сыпаться на консоль в привычном порядке. На практике такого уровня аккуратности ждут от скриптов, которые ночами будут читать по несколько человек: смешанный поток удобен при чтении, а раздельный - при автоматическом разборе проблем.

Отдельной строкой стоит помнить про сохранение статуса выхода: команда | tee - в POSIX-шелле код выхода пайпа это код последней команды, то есть самой tee, а она завершается успешно почти всегда. Вслед за пайпом должен стоять либо set -o pipefail в начале скрипта, либо аккуратный анализ массива PIPESTATUS: ${PIPESTATUS[0]} несёт в себе код исходной команды. Без этой привычки ошибочный прогон сборки будет с триумфом зарегистрирован как зелёный, и это неподражаемый способ испортить себе вечер.

Multiwriter в экстремальных сценариях, fifo, сеть, процессы

tee умеет писать не только в обычные файлы. Теоретически и практически валидна запись в именованный канал: mkfifo /tmp/alerts.fifo, команда 2>&1 | tee /tmp/alerts.fifo | less, и на другом конце fifo сидит скрипт, реагирующий на строки с ERROR отправкой уведомления. Подобные схемы заменяют полноценный мониторинг на пару вечеров, пока он разворачивается, и годятся для аварийной обсервации.

Тем же духом проникнута отправка вывода на соседний хост: команда | tee >(ssh logger@host 'cat >> /srv/logs/remote.log') складывает копию лога на соседнюю машину прямо в процессе. Перед интрузивными изменениями сети или firewall такая схема особенно приятна: даже если машина потеряет сеть посреди операции, копия хода работ уже лежит на соседнем хосте, и разбор не будет строиться по памяти и скриншотам.

Сопутствующие привычки, метки времени и формат

Поток лога в файл удобно дополнить метками времени: команда 2>&1 | while read -r line; do printf '%s %s\n' "$(date +%T)" "$line"; done | tee -a run.log - строчная метка времени превращает разбор лога из гадания в хронологию. Некоторые предпочитают ts из moreutils для того же самого эффекта с меньшей писаниной. С метками появляется возможность мерить скорость этапов и находить замедления без всяких сторонних инструментов.

И формат самого лога: держите его плоским, по строкам; человеку потом grep-ать. Никаких красивостей в стиле JSON в логе человека рядом с tee, если рядом нет второго структурированного потока; консольный лог читается глазами, а машинная обработка заслуживает отдельного канала. Такое разделение простое, но именно оно сохраняет логи читабельными через годы эксплуатации вперемешку с ротациями и ночными инцидентами.

Полный скелет скрипта с tee под капотом

Соберём итоговый образчик: скрипт обслуживания, который виден в сеансе и сохраняется в архив, с метками времени и корректным выходом:

#!/bin/bash
set -euo pipefail
LOG=/var/log/maintenance-$(date +%F-%H%M).log
exec > >(tee -a "$LOG") 2>&1
echo "начало: $(date -Is)"
apt update
apt full-upgrade -y
echo "завершение: $(date -Is)"

Скрипт из четырех строчек сути закрывает вопрос и наблюдения, и истории. В cron такой скрипт вызывается как есть, и его вывод аккуратно складывается в файл с датой, а экран отвечает за живую работу администратора. Возвращайтесь к нему через полгода - каждая строка будет на своём месте, и это тот случай, когда простое решение вдребезги побеждает красивые альтернативы длиной в страницу.

Частые ошибки и как их избегать

Перечислим типические, чтобы не наступать дважды. Первая - очистка файла вместо дозаписи: забытый -a превращает трёхнедельный лог в последний запуск; если лог важен, -a должна стать рефлексом. Вторая - tee до слияния потоков вместо после: команда | tee log 2>&1 выглядит похоже на правильную, но перенаправлено тут stderr самой tee в stdout уже после старта записи, и в файл идёт не то, что вы ожидали; правильный порядок - сначала 2>&1, затем tee. Третья - утерянный pipefail, о котором уже говорилось. Четвёртая - запись в файл, который сам лежит в наблюдаемой папке: команда ls -R /logs | tee /logs/list.txt на больших деревьях порождает неуловимый самоприменимый цикл вопросов к самому себе; вывод не должен находиться внутри директории входных данных.

Пятая - отсутствие каталога: tee получает путь в несуществующую папку и обламывается с понятной, но раздражающей ошибкой посреди ночного задания. защита это mkdir -p "$(dirname "$LOG")" перед запуском записи. Такая деталь делает скрипт устойчивым к переносам, и это ещё один довод писать строку с tee последовательно, а не тянуться к ней без дисциплины.

Финал, маленькая утилита большого спокойствия

В экосистеме Unix много инструментов с претензией на величие и очень мало - с претензией на простоту. tee из второй категории и потому до сих пор стоит в миллионе скриптов, не требуя ни документации, ни оправданий. Добавляйте её везде, где видите одностороннее перенаправление вывода в файл: вывод перестанет быть невидимкой, а запись - пропащей. Этой сделки достаточно, чтобы статья оказалась прочитанной не зря.

И ещё одна экономная мысль на выход: в среде, где tee уже прижилась, стоит один раз пройтись по наследству cron-задач и добавить её там, где вывод сейчас уходит в /dev/null. Цена правки нулевая, а эффект от того, что ночная задача перестанет быть ящиком без дна, оценивается при первом же инциденте, который почему-то всегда случается с утра в понедельник.

Практическое правило для запоминания: каждый раз, когда команда дольше минуты или её результат кому-то понадобится, tee уже должен стоять в конвейере. Остальное - вопрос последовательности флагов, которые теперь у вас перед глазами.
С этим набором в кармане tee перестаёт быть казённым фильтром из методички и становится вашим постоянным наблюдательным постом на каждом длинном запуске.