Каждый процесс имеет три стандартных потока
Когда Linux kernel запускает процесс (через fork() + execve()), он автоматически открывает три file descriptors (FD):
- FD 0:
stdin— куда процесс читает входные данные (default: клавиатура) - FD 1:
stdout— куда процесс пишет нормальный вывод (default: терминал) - FD 2:
stderr— куда процесс пишет ошибки и diagnostic-сообщения (default: терминал)
В C это STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO (<unistd.h>). В Python — sys.stdin, sys.stdout, sys.stderr. В bash просто <, >, 2>.
Это hardcoded стандарт POSIX — работает для всех процессов одинаково.
Почему отдельные stdout и stderr? Чтобы можно было:
- Перенаправить только результат (stdout) в файл, оставив ошибки на экране
- Игнорировать ошибки (
2>/dev/null), не теряя нормальный вывод - Логировать данные и errors раздельно
Базовые перенаправления
$ command > output.txt # stdout -> output.txt (перезаписывает)
$ command >> output.txt # stdout -> output.txt (append)
$ command 2> errors.txt # stderr -> errors.txt
$ command < input.txt # stdin ← input.txt
$ command < input > output # перенаправить и stdin и stdout
Простой пример с ls:
$ ls /tmp /nonexistent
ls: cannot access '/nonexistent': No such file or directory
/tmp:
file1.txt
file2.txt
# stdout перенаправлен в файл, stderr на экран:
$ ls /tmp /nonexistent > out.txt
ls: cannot access '/nonexistent': No such file or directory
$ cat out.txt
/tmp:
file1.txt
file2.txt
# Сейчас наоборот: stderr в файл, stdout на экран:
$ ls /tmp /nonexistent 2> err.txt
/tmp:
file1.txt
file2.txt
$ cat err.txt
ls: cannot access '/nonexistent': No such file or directory
2>&1: объединить stderr со stdout
Самая используемая конструкция перенаправлений:
$ command > all.log 2>&1
# stdout идёт в all.log
# stderr идёт туда же (2>&1 = "stderr направить туда, куда сейчас идёт stdout=FD1")
Бypass shell-нотация: &1 означает “то место, куда указывает FD1 в данный момент”. Если FD1 указывает на all.log — stderr тоже пойдёт туда.
Order matters!
# [x] Оба в файл:
$ command > all.log 2>&1
# Шаги: (1) stdout -> all.log; (2) stderr -> копия stdout = all.log
# [X] Часто ожидают того же, но получают неожиданное:
$ command 2>&1 > all.log
# Шаги: (1) stderr -> копия stdout (которое сейчас = терминал!);
# (2) stdout -> all.log.
# Итог: stderr НА ТЕРМИНАЛЕ, stdout в файле.
Это самый частый bug при работе с перенаправлениями. Запомни: сначала перенаправь stdout, потом объединяй stderr через 2>&1.
bash-only short: &>file
В bash есть синтаксический сахар:
$ command &> all.log
# Эквивалент: command > all.log 2>&1
# stdout И stderr в all.log
$ command &>> all.log
# То же, но append
Это bash-specific (не POSIX). На sh/dash//bin/sh (Debian default для скриптов) не работает. В CI используй > file 2>&1 для portable POSIX.
/dev/null: чёрная дыра
/dev/null — это special character device. Всё, что в него пишется — отбрасывается (/dev/null принимает любые писания и сразу их забывает). Читать из него = пустой поток (EOF immediately).
# Подавить stdout:
$ command > /dev/null
# Подавить stderr:
$ command 2> /dev/null
# Подавить обе:
$ command > /dev/null 2>&1
$ command &> /dev/null # bash short
DE-сценарии:
# Запустить проверку, нас интересует только exit code:
$ if grep -q ERROR app.log 2>/dev/null; then
echo "Errors found"
fi
# find с подавлением "Permission denied":
$ find / -name "*.cfg" 2>/dev/null
# Запустить cron-job без письма по email (без output вообще):
$ */5 * * * * /opt/etl/run.sh > /dev/null 2>&1
Cron отправляет email на каждый non-empty stdout/stderr. > /dev/null 2>&1 — стандартная фраза в crontabs для silent jobs.
/dev/stdin, /dev/stdout, /dev/stderr
Это виртуальные файлы, представляющие потоки. Полезны когда программа принимает «файл», а у тебя данные на stdin:
# Команда принимает file argument, но мы хотим из pipe:
$ echo "hello" | some_command /dev/stdin
# Или дублировать вывод:
$ command | tee /dev/stderr | another_command
# tee пишет в /dev/stderr (= терминал) и в stdout (для следующего process)
Дублирование FD через cp
# Прочесть из stdin как из файла:
$ md5sum /dev/stdin < input.txt
# md5sum обычно принимает имя файла; здесь читает из /dev/stdin (=input.txt после redirect)
Программирование: куда пишет программа
В Python:
import sys
print("normal output", file=sys.stdout) # -> stdout
print("error message", file=sys.stderr) # -> stderr
data = sys.stdin.read() # ← stdin
В bash-скрипте:
echo "normal output" # -> stdout
echo "error message" >&2 # -> stderr (perlomane >&2 редирект)
read -r line # ← stdin
Канон: диагностику пишет в stderr, результат в stdout. Это позволяет pipeline’у работать только с результатом:
# Скрипт логирует на stderr, выдаёт CSV на stdout:
$ ./extract.sh | jq '.users[]'
# jq получает только CSV (stdout); stderr-логи всё ещё видны на экране
Exit codes и pipes
Exit code последнего процесса в pipeline — это exit code всего pipeline (по умолчанию):
$ false | true
$ echo $?
0
# Хотя false вернул 1, true в конце вернул 0 -> весь pipeline 0
Это часто скрывает ошибки. В bash есть pipefail для исправления:
$ set -o pipefail
$ false | true
$ echo $?
1
# Теперь ненулевой exit code ЛЮБОЙ команды pipeline становится exit-code всего
В production-скриптах всегда set -euo pipefail — про это будет в модуле 18.
Под капотом: что делает shell
open/read/write: системные вызовы файлового I/O Pipes: механика анонимных каналов в ядреКогда ты пишешь command > file.txt, shell:
- Делает
fork()— создаёт дочерний процесс - В дочернем процессе ДО
execve(command):open("file.txt", O_WRONLY|O_CREAT|O_TRUNC)— получает FD, скажем 3dup2(3, 1)— копирует FD 3 в FD 1 (stdout теперь указывает на файл)close(3)— закрывает оригинальный FD 3
- Делает
execve(command)— программа стартует, у неё stdout уже на файл
Это происходит до запуска программы — программа не знает и не должна знать, что её stdout перенаправлен. Это магия POSIX-абстракции FD.
Для command 2>&1:
fork()dup2(1, 2)— копировать FD 1 в FD 2 (stderr теперь как stdout)execve(...)
2>&1 буквально означает «dup2(1, 2)» в shell-нотации. & — это «FD-reference», не bash-background.
DE-сценарии
1. Лог-файл всех данных + ошибок
$ ./etl_job.sh > /var/log/etl/$(date +%Y%m%d).log 2>&1
Всё (stdout + stderr) идёт в один лог. Простое решение для не-критичных jobs.
2. Раздельные логи
$ ./etl_job.sh > /var/log/etl/output.log 2> /var/log/etl/errors.log
Раздельно — позволяет дифференцировать data и errors. Полезно для monitoring (если errors.log не пуст — alert).
3. Cron job с email-on-error
# В crontab — silent if successful, email on error:
0 2 * * * /opt/etl/run.sh > /var/log/etl/$(date +\%Y\%m\%d).log
# stdout идёт в файл, stderr остаётся для cron -> email если стnonempty
Cron отправит email только если stderr не пуст (то есть была ошибка).
4. Дебаг pipeline на чём-то типа strace
$ strace -e trace=openat -o syscalls.log -f ./script.sh
# stdout/stderr скрипта — обычным образом
# strace пишет syscalls в syscalls.log
-o file — strace-specific опция, перенаправляет own diagnostic вывод strace в файл.
Попробуй сам
- Раздельные стдин/стдаут/стдерр:
ls /tmp /nonexistent > out.txt 2> err.txt cat out.txt cat err.txt - Все в один файл:
ls /tmp /nonexistent > all.log 2>&1 cat all.log - Подавить только stderr:
ls /nonexistent 2>/dev/null - Skript с echo to stderr:
bash -c 'echo "OK" >&1; echo "BAD" >&2' bash -c 'echo "OK" >&1; echo "BAD" >&2' > out.txt 2> err.txt cat out.txt cat err.txt - Подсчитай только stdout:
ls /tmp /nonexistent 2>/dev/null | wc -l
macOS-различия
/dev/null,/dev/stdin,/dev/stdout,/dev/stderr— есть и на macOS.&>(bash-only) — работает на macOS если shell это bash. На zsh (default на macOS 10.15+)&>тоже работает.- Базовые
>,>>,2>,2>&1— POSIX, работают везде. pipefail—set -o pipefailработает на bash 3.2+ (включая macOS default).
Главное
- 3 стандартных FD: 0 (stdin), 1 (stdout), 2 (stderr) — открываются для каждого процесса.
> file— stdout,2> file— stderr,>> file— append,< file— stdin.> file 2>&1— оба в файл. Порядок важен!2>&1 > file— НЕ то же самое./dev/null— отбрасывает всё.> /dev/null 2>&1— silent (без вывода)./dev/stdin,/dev/stdout,/dev/stderr— виртуальные файлы для FD.- bash-short
&>file=> file 2>&1, но не POSIX. - Программа пишет результат в stdout, диагностику в stderr — это convention для clean pipelines.
set -o pipefailдля правильных exit codes в pipeline (модуль 18).