Learning Platform
Глоссарий Troubleshooting
Урок 10.04 · 22 мин
Средний
teeHere-docHere-stringProcess substitution

tee: T-fitting в pipeline

tee (от водопроводного fitting в форме T) — копирует stdin в файл И в stdout:

$ command | tee file
# command's stdout -> file AND видимо на экране (stdout tee)

Это даёт две вещи одновременно: запись в файл и возможность продолжить pipe. Базовое использование — видеть вывод длинной команды и сохранять её одновременно:

$ ./run-etl.sh | tee /tmp/etl-run.log
# Видишь все строки на экране, и они сохранены в /tmp/etl-run.log

Флаги tee

  • tee file — overwrite (default)
  • tee -a file — append (важно для логов)
  • tee file1 file2 file3 — в несколько файлов одновременно
  • tee -i file — ignore SIGINT (не падать на Ctrl+C — иногда полезно)
# Дублировать в два файла + на экран:
$ command | tee logs/today.log logs/all.log

tee внутри pipeline

Самая мощная фишка — прерывание pipeline для логирования:

$ command | tee full-output.log | grep ERROR
# full-output.log получает полный вывод
# на экран попадают только ERROR-строки

Это полезно: pipeline транформации, но lossless intermediate log. После выполнения у тебя есть полный лог для post-mortem.

# ETL с lossless checkpoint:
$ extract.py | tee raw_data.json | jq '.users[]' | psql -c "COPY users FROM stdin"
# raw_data.json — full backup, на случай если transform/load упали

tee и sudo

Классический use-case для tee — запись в root-only файлы из обычной shell:

# [X] НЕ работает: > обрабатывается shell-ом ДО sudo:
$ sudo echo "127.0.0.1 myhost" > /etc/hosts
bash: /etc/hosts: Permission denied
# Shell делает open(/etc/hosts) под твоим UID — fail

# [x] Работает: sudo применяется к tee, который пишет в файл:
$ echo "127.0.0.1 myhost" | sudo tee -a /etc/hosts
127.0.0.1 myhost      # вывод tee на экран
$ tail -1 /etc/hosts
127.0.0.1 myhost      # запись прошла

-a (append) обязателен — без него файл будет перезаписан, что обычно НЕ то, что ты хочешь для /etc/hosts.

Если не нужен echo от tee на экран:

$ echo "127.0.0.1 myhost" | sudo tee -a /etc/hosts > /dev/null
# Запись произошла, на экран ничего не выводится

tee и process substitution

Самая магическая возможность — разветвление pipeline на несколько направлений:

$ command \
    | tee >(grep ERROR > errors.log) \
    | tee >(grep WARN > warnings.log) \
    | grep INFO > info.log

>(...)process substitution: bash превращает команду в файл-аналог (FIFO под капотом). tee пишет stdin одновременно:

  • в первый >(grep ERROR > errors.log) — внутри запускается grep, который пишет в errors.log
  • в свой stdout (для следующего pipe)

Результат: данные одновременно идут в errors.log, warnings.log и info.log. Это fan-out без temp-файлов.

# Сразу несколько compressed-уровней:
$ command | tee >(gzip > backup.gz) >(xz > archive.xz) > raw.log
# raw.log — uncompressed
# backup.gz — gzip-сжатый
# archive.xz — xz-сжатый
# Все три за один проход данных через command

Here-docs (heredoc): multi-line input

<<EOF — heredoc, передаёт multi-line блок текста как stdin команды. Полезно для:

  • Передачи multi-line строки в команду без файла
  • Embed SQL в bash-скрипт
  • Передачи Python/awk-программ в одну строку shell
$ cat <<EOF
Line 1
Line 2
Line 3
EOF
Line 1
Line 2
Line 3

<<EOF — bash читает следующие строки до строки EOF (на отдельной строке, без отступа в default mode), передаёт их как stdin команды.

EOF — это delimiter, можешь использовать любой identifier:

$ cat <<END
hello
END

$ cat <<MYBLOCK
content
MYBLOCK

Variable expansion в heredoc

$ NAME=alice
$ cat <<EOF
Hello, $NAME!
EOF
Hello, alice!

По default переменные $VAR и command substitution $(...) expanded. Если хочешь literal (без expansion) — quoted delimiter:

$ cat <<'EOF'
Hello, $NAME! This is a $literal string.
EOF
Hello, $NAME! This is a $literal string.

Quoted <<'EOF' сохраняет dollar-signs как литералы.

<<- для отступов

<<-EOF (с минусом) игнорирует leading TABS (только табы, не пробелы):

$ cat <<-EOF
	indented line  (tabs at start ignored)
	another one
	EOF
indented line  (tabs at start ignored)
another one

Это удобно в bash-скриптах с indentation:

function setup_database() {
    psql <<-EOF
	CREATE TABLE users (
	    id SERIAL PRIMARY KEY,
	    email TEXT
	);
	EOF
}

Tabs внутри скрипта (для читаемости) удаляются при передаче. Без - — текст имел бы tabs включёнными.

DE-сценарии heredoc

1. SQL внутри bash

COPY и INSERT в PostgreSQL — загрузка данных из файлов
$ psql -d analytics <<EOF
CREATE TABLE staging_orders (
    order_id INT,
    user_id INT,
    amount NUMERIC,
    country TEXT
);

COPY staging_orders FROM '/tmp/orders.csv' WITH (FORMAT csv, HEADER);

SELECT COUNT(*) FROM staging_orders;
EOF

Заменяет .sql файлы для quick-and-dirty ETL.

2. Inline Python

$ python3 <<EOF
import json
data = json.load(open('/tmp/data.json'))
print(sum(item['amount'] for item in data))
EOF

3. Конфиг-файл из bash:

$ cat > /etc/myapp.conf <<EOF
host = $HOSTNAME
port = 5432
user = $USER
EOF

Создаёт конфиг с подстановкой переменных.

4. AWS CLI multi-step через heredoc

$ aws s3api put-bucket-policy --bucket my-bucket --policy "$(cat <<EOF
{
  "Version": "2012-10-17",
  "Statement": [...]
}
EOF
)"

JSON-policy inline в скрипте — нет нужды в отдельном файле.

Here-strings: <<< для одной строки

<<< — короткая версия heredoc для одной строки:

$ grep "alice" <<< "alice 30
bob 25
carol 28"
alice 30

$ wc -c <<< "hello"
6   # 5 chars + \n

$ bc <<< "2 + 2"
4

<<< принимает строку (после bash-expansion переменных), передаёт как stdin.

Полезно когда нужно передать переменную на stdin команды:

$ data="alice,30,USA"
$ awk -F',' '{print $1}' <<< "$data"
alice

Без <<<: echo "$data" | awk ... — то же, но с extra процессом echo.

Heredoc combined with redirect

$ cat > /tmp/script.py <<'PYEOF'
import sys
for line in sys.stdin:
    print(line.upper(), end='')
PYEOF
# Создаёт /tmp/script.py с этим содержимым
$ cat >> /tmp/log.txt <<EOF
$(date) - Script started
$(date) - Processing
EOF
# Append-логирует с timestamps

Попробуй сам

  1. tee базовое:
    ls /tmp | tee /tmp/files.txt | wc -l
  2. tee fan-out:
    echo "data" | tee >(tr a-z A-Z > /tmp/upper.txt) >(rev > /tmp/rev.txt) > /tmp/orig.txt
    cat /tmp/upper.txt /tmp/rev.txt /tmp/orig.txt
  3. Heredoc:
    cat <<EOF
    Hello $USER
    Today is $(date +%Y-%m-%d)
    EOF
  4. Quoted heredoc (no expansion):
    cat <<'EOF'
    $USER won't be expanded
    $(date) stays literal
    EOF
  5. Here-string:
    awk '{print $2}' <<< "alice 30 USA"

macOS-различия

  • tee идентичен на macOS и Linux.
  • Heredoc (<<EOF, <<-EOF, <<'EOF') — POSIX, работает на bash, zsh, dash.
  • Here-string (<<<) — bash/zsh extension, не POSIX! На /bin/sh (dash в Debian/Ubuntu) не работает. Для portable скриптов используй echo "$var" | cmd вместо cmd <<< "$var".
  • Process substitution >(...) и <(...) — bash/zsh, не POSIX.
Проверка знанийKnowledge check
Объясни, почему 'echo "hi" | sudo cat > /etc/test.txt' все равно падает с Permission denied, хотя там есть sudo, и как правильно записать в root-owned файл через pipe.
ОтветAnswer
Ключ — порядок выполнения. Bash обрабатывает '> /etc/test.txt' ПЕРЕД запуском любой команды. Шаги: (1) Shell видит '> /etc/test.txt' и делает open('/etc/test.txt', O_WRONLY|O_CREAT|O_TRUNC). Это open происходит под текущим UID юзера (НЕ root). Если /etc принадлежит root и обычный user не имеет на него write — permission denied прямо здесь. Sudo даже не успел запуститься. (2) Pipe echo | cat сам по себе работал бы. Но shell провалился на шаге 1, и весь pipeline отменён. Правильно: 'echo \"hi\" | sudo tee /etc/test.txt'. Здесь sudo применяется к tee, который запускается с euid=0 (root). Tee делает open('/etc/test.txt') уже под root — succeeds. Tee читает stdin (от echo) и пишет в файл + stdout. Для append: 'echo ... | sudo tee -a /etc/file'. Если экран не нужен: 'echo ... | sudo tee /etc/file > /dev/null'. Это базовая идиома для admin tasks из обычной shell без необходимости делать 'sudo bash -c \"echo ... > /etc/file\"' (что тоже работает, но через дополнительный shell). Tee-подход — стандарт в Ansible/cloud-init, потому что не требует quoting-magic.

Главное

  • tee FILE копирует stdin в FILE И в stdout. -a append. Несколько файлов: tee f1 f2 f3.
  • Идиома ... | sudo tee FILE для записи в root-owned файлы из non-root shell.
  • tee >(cmd1) >(cmd2)fan-out pipeline в несколько направлений (process substitution).
  • Heredoc <<EOF ... EOF — multi-line stdin. Quoted <<'EOF' — без variable expansion.
  • <<-EOF — leading tabs игнорируются (для красивых indented скриптов).
  • Here-string cmd <<< "text" — одна строка как stdin. Bash/zsh-only.
  • Process substitution >(...), <(...) — bash-only, не POSIX.
  • Используй heredoc для inline SQL/Python/JSON — заменяет необходимость отдельных файлов.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Зачем нужна команда tee в pipeline? Чем отличается от обычного > или >>?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5