3.8. Потоки ввода-вывода и коды возврата

Автор: Кирилл Шевчук

В этой статье мы познакомимся с тем, как команды в терминале обмениваются данными. Каждая команда может получать входные данные (stdin), отправлять результат работы (stdout) и сообщения об ошибках (stderr).

Мы научимся управлять этими потоками с помощью перенаправлений и разберёмся, что такое код возврата команды и как его использовать для проверки успеха или ошибки.

Ключевые вопросы статьи

  1. Что такое стандартные потоки stdin, stdout и stderr?
  2. Что такое перенаправление потока и для чего оно нужно?
  3. Как и для чего комбинировать разные перенаправления?
  4. Что такое коды возврата команд и как их можно проверить?
  5. Что за файл /dev/null и зачем он нужен?

Что такое потоки ввода-вывода

В предыдущей статье мы использовали команды вроде grep, wc и diff, чтобы анализировать данные и сравнивать результаты.

Но все эти команды не просто «что-то делают» — они принимают данные, обрабатывают их и отдают результат обратно в терминал.
Именно этот процесс передачи данных называется вводом и выводом (англ. input/output или просто I/O).

Каждая команда в Linux общается с внешним миром через три «канала связи» — потоки ввода-вывода.

Они встроены в систему и существуют для каждой программы по умолчанию:

Поток Название Описание
stdin Стандартный ввод То, откуда команда получает данные (обычно клавиатура, но может быть файл или вывод другой команды)
stdout Стандартный вывод То, куда команда отправляет результат своей работы (обычно экран терминала)
stderr Стандартный поток ошибок То, куда команда пишет сообщения об ошибках (тоже обычно экран, но отдельно от основного вывода)

Можно представить это так:

2.8.1

Потоки ввода и вывода

Эти потоки работают одновременно, но независимо друг от друга. Это позволяет, например, записывать результат работы команды в один файл, а ошибки — в другой.

Пример 1. Команда без ошибок

alice@ubuntu:~$ echo "Hello, world!"
Hello, world!

Команда echo получает текст «Hello, world!» через свои аргументы, выводит его в стандартный поток вывода (stdout), и текст попадает на экран.
Всё прошло успешно — ошибок нет, поэтому в поток stderr ничего не попало.

Пример 2. Команда с ошибкой

alice@ubuntu:~$ cat missing.txt
cat: missing.txt: No such file or directory

cat пыталась прочитать файл missing.txt, но не нашла его.
Текст ошибки No such file or directory — это не просто сообщение на экране, это вывод в поток stderr, отдельный от основного вывода stdout.

Почему это важно

Пока команды просто показывают результат на экране, разницы между stdout и stderr не видно.

Но когда вы начнёте перенаправлять вывод в файлы, передавать результаты другим командам или писать скрипты, разделение этих потоков становится критически важным.

Например:

  • вы можете сохранить результат работы программы в файл, но ошибки записать в отдельный;
  • или объединить оба потока, чтобы получить полный журнал выполнения;
  • или вовсе скрыть ошибки, если они вам не нужны.

Это и есть основа управления вводом и выводом, с которой мы познакомимся дальше.

Перенаправление вывода

Ранее мы уже применяли эту механику, когда вызывали команду и записывали результат в файл c помощью оператора >.

Это частый случай перенаправления вывода. Он нужен, например, чтобы потом проанализировать файл, отправить коллеге или просто не потерять после закрытия терминала.

И точно так же можно перенаправлять вывод ошибок и делать многие другие вещи. Разберём перенаправления подробнее.

Перенаправление стандартного вывода (stdout)

Чтобы записать результат работы команды в файл вместо экрана, используется символ >:

alice@ubuntu:~$ echo "Hello" > output.txt

Теперь ничего не появилось на экране, потому что текст «Hello» был направлен не в терминал, а в файл output.txt.

Если откроем его, увидим наш результат:

alice@ubuntu:~$ cat output.txt
Hello

Символ > создаёт новый файл, если его не было, или перезаписывает существующий, если он уже есть.
Ранее мы создавали новый файл таким образом в качестве альтернативы команде touch.

Запустим команду снова с другим текстом:

alice@ubuntu:~$ echo "World" > output.txt
alice@ubuntu:~$ cat output.txt
World

Видим, что старое содержимое файла исчезло — на его месте теперь только слово World.

Но мы можем не перезаписывать, а добавлять текст в конец файла, и для этого используется двойная стрелка >>:

alice@ubuntu:~$ echo "Hello" > output.txt
alice@ubuntu:~$ echo "World" >> output.txt
alice@ubuntu:~$ cat output.txt
Hello
World

Теперь мы видим в файле обе строки, которые мы записали.

Примечание

Запомните: > — создаёт или перезаписывает файл, >> — добавляет текст в конец файла.

Перенаправление ошибок (stderr)

По умолчанию и обычный вывод (stdout), и ошибки (stderr) печатаются на одном и том же экране.
Но Linux позволяет направлять их в разные файлы.

Для этого используется 2> — цифра 2 обозначает поток ошибок (stderr), а 1 — стандартный вывод (stdout).

Попробуем пример:

alice@ubuntu:~$ ls /root 2> errors.log

Если у пользователя нет прав доступа к каталогу /root, команда ls выведет сообщение об ошибке. Но теперь ошибка окажется в файле errors.log, а не на экране.

alice@ubuntu:~$ cat errors.log
ls: cannot open directory '/root': Permission denied

При этом, если бы в выводе были обычные данные (stdout), они всё равно появились бы в терминале.

Разделение вывода и ошибок

Часто программы выводят и результат, и ошибки одновременно.
Чтобы их не перепутать, можно записать их в разные файлы:

alice@ubuntu:~$ ls -l /var /root > output.log 2> errors.log

Теперь:

  • всё, что программа выводит «нормально» (stdout), попадёт в output.log;
  • всё, что она пишет как ошибку (stderr), окажется в errors.log.

Можем проверить наши файлы, чтобы в этом убедиться:

alice@ubuntu:~$ cat output.log
/var:
total 40
drwxr-xr-x  2 root root   4096 Oct  6 14:43 backups
drwxr-xr-x 15 root root   4096 Sep 23 15:17 cache
drwxrwsrwt  2 root root   4096 Jun  5 12:55 crash
drwxr-xr-x 43 root root   4096 Sep 23 15:17 lib
drwxrwsr-x  2 root staff  4096 Apr 22  2024 local
lrwxrwxrwx  1 root root      9 Jun  5 12:53 lock -> /run/lock
drwxrwxr-x  9 root syslog 4096 Oct 19 00:00 log
drwxrwsr-x  2 root mail   4096 Jun  5 12:53 mail
drwxr-xr-x  2 root root   4096 Jun  5 12:53 opt
lrwxrwxrwx  1 root root      4 Jun  5 12:53 run -> /run
drwxr-xr-x  4 root root   4096 Jun  5 12:53 spool
drwxrwxrwt  9 root root   4096 Oct 21 07:45 tmp
alice@ubuntu:~$ cat errors.log
ls: cannot open directory '/root': Permission denied

Разделение потоков вывода — одна из самых практичных возможностей Linux.
Оно позволяет собирать чистые логи, анализировать ошибки отдельно и не терять полезную информацию в большом потоке сообщений.

Объединение потоков

Многие программы выводят и обычные результаты, и сообщения об ошибках. Мы уже знаем, что это два разных потока (stdout и stderr), поэтому если перенаправить только стандартный вывод

command > output.log

— ошибки всё равно появятся на экране.

Чтобы сохранить всё в одном месте, направьте поток ошибок туда же, куда уже уходит обычный вывод. Это называется объединением потоков.

Классический способ

Для объединения потоков используется следующая запись:

command > output.log 2>&1

Разберём, что здесь происходит, по шагам:

  1. > output.log — направляет стандартный вывод (stdout, поток 1) в файл output.log.
  2. 2>&1 — говорит системе: «Отправь поток ошибок (stderr, поток 2) туда же, куда сейчас направлен поток 1».

Другими словами, всё, что команда напишет — и обычные сообщения, и ошибки, — попадёт в один и тот же файл.

Пример:

alice@ubuntu:~$ ./script.sh > output.log 2>&1

Теперь в output.log окажутся и успешный вывод, и ошибки:

alice@ubuntu:~$ cat output.log
Starting process...
Warning: configuration file missing
Process completed successfully

Это удобно, когда вы хотите сохранить весь вывод программы для последующего анализа, например, при отладке скриптов или программ.

Современный короткий синтаксис

Начиная с Bash 4 появился более короткий способ объединить потоки:

alice@ubuntu:~$ ./script.sh &> output.log

Он делает то же самое, что и > output.log 2>&1, но выглядит аккуратнее.
А если нужно добавлять в существующий файл, используйте &>>:

alice@ubuntu:~$ ./script.sh &>> output.log

Важно

Обратите внимание: &> и &>> работают только в Bash. Если вы используете другие оболочки (например, sh или dash), делайте классическую запись > file 2>&1.

Один важный нюанс

Может показаться, что записи > output.log 2> output.log и > output.log 2>&1 эквивалентны.

Но это не так.

Дело в том, что Bash открывает файлы по порядку, слева направо. Когда вы пишете:

command > output.log 2> output.log

— Bash делает следующее:

  1. Открывает output.log для стандартного вывода (stdout).
  2. Открывает тот же файл ещё раз отдельно для ошибок (stderr).
  3. Запускает команду.

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

Error: missing file
Output: process sError: retrying...
uccessful

То есть порядок строк и даже целостность данных может нарушиться — файл не будет отражать реальный ход вывода.

А вот 2>&1 создаёт общее соединение потоков: оба потока используют один и тот же файловый дескриптор, а значит, пишут в файл согласованно и в правильной последовательности.

Когда стоит объединять потоки

Объединение потоков полезно, когда:

  • вы хотите получить единый лог выполнения команды;
  • вам нужно проанализировать ошибки позже, без визуального шума в терминале;
  • команда запускается в фоне (через &) и её вывод нужно сохранить целиком.

Но объединение потоков не всегда удобно — чаще полезнее разделять их, чтобы не смешивать обычные результаты и сообщения об ошибках.

Важно

Выбор зависит от задачи: для отладки — объединяем, для анализа — разделяем.

Перенаправление ввода (stdin)

Мы уже научились управлять выводом команд — направлять результаты в файлы или объединять их в логи.
Теперь посмотрим на процесс с другой стороны: как команды получают данные для работы.

Большинство программ в Linux ждут ввод из стандартного потока ввода (stdin).
Обычно это клавиатура: вы печатаете текст, программа его читает.
Но мы можем заменить клавиатуру файлом, чтобы программа читала данные оттуда автоматически.

Для этого используется символ <.

Пример 1. Подать данные из файла

Команда sort сортирует строки текста.
Мы можем подать ей файл напрямую в качестве аргумента:

alice@ubuntu:~$ sort names.txt

Или сделать то же самое с использованием <:

alice@ubuntu:~$ sort < names.txt

Результат будет одинаковым. sort возьмёт содержимое names.txt из потока ввода (stdin), отсортирует и выведет результат на экран (stdout):

Alice
Bob
Charlie
Diana

Разницы на экране нет, но < становится особенно полезен, когда вы строите цепочки команд или работаете со скриптами, ведь он позволяет гибко заменять источник данных.

Пример 2. Подать файл в wc

Команда wc -l подсчитывает количество строк во входных данных.
Её можно использовать с файлом в качестве аргумента:

alice@ubuntu:~$ wc -l data.txt
42 data.txt

А можно подать файл через стандартный ввод:

alice@ubuntu:~$ wc -l < data.txt
42

Разница в том, что при использовании < команда видит только поток данных, без имени файла.
Поэтому в выводе остаётся только число 42, без лишней информации.

Пример 3. cat < file.txt

Если подать файл через <, команда cat просто прочитает его содержимое из потока ввода и выведет на экран:

alice@ubuntu:~$ cat < file.txt
Hello from stdin!

Эта запись полностью эквивалентна:

alice@ubuntu:~$ cat file.txt
Hello from stdin!

Здесь < просто заменяет «чтение из файла по имени» «чтением из стандартного ввода».
Это мелочь, но в скриптах иногда именно такая форма бывает удобнее, особенно если имя файла хранится в переменной или подставляется автоматически.

Как это работает «под капотом»

Когда вы вводите < file.txt, оболочка открывает файл и подключает его как источник данных для команды.
Команда «думает», что данные приходят с клавиатуры, хотя на самом деле они читаются из файла.

Можно представить это схематично:

2.8.2

Файл в потоке ввода

Таким образом, < — это операция, обратная >:

  • > отправляет данные из команды в файл,
  • < подаёт данные из файла в команду.

Примеры на практике

Задача Команда Что происходит
Отсортировать строки из файла sort < names.txt Содержимое файла читается как ввод
Подсчитать строки в файле wc -l < data.txt wc считает строки, поданные через stdin
Просмотреть содержимое файла cat < file.txt Эквивалентно cat file.txt
Передать данные из одного файла в другой cat < old.txt > new.txt Чтение из old.txt, запись в new.txt

Использование < не обязательно, но удобно, когда вы:

  • хотите писать команды в универсальной форме, не привязываясь к конкретным файлам;
  • работаете с перенаправлениями в обе стороны (например, cat < input.txt > output.txt);
  • хотите автоматизировать передачу данных между программами — в конвейерах, циклах или скриптах.

Примеры сочетаний перенаправлений

Мы уже познакомились с каждым типом перенаправления по отдельности. Теперь посмотрим, как их можно комбинировать.

В Linux можно соединять разные направления потоков как конструктор: перенаправлять ввод и вывод, разделять ошибки, объединять их или добавлять данные в существующие файлы.
Эта гибкость делает терминал невероятно мощным инструментом: вы можете точно контролировать, куда уходит каждый байт информации.

Задача Команда Что происходит
Записать результат в файл echo "ok" > log.txt Основной вывод (stdout) записывается в файл. Если файл существует — он будет перезаписан
Добавить результат к концу файла echo "done" >> log.txt Данные добавляются в конец файла (stdout в режиме дозаписи)
Сохранить только ошибки ls /root 2> err.txt Ошибки (stderr) направляются в err.txt, обычный вывод остаётся на экране
Разделить потоки ./run.sh > out.txt 2> err.txt Основной вывод (stdout) идёт в out.txt, ошибки (stderr) — в err.txt
Объединить всё в один лог ./run.sh &> all.txt И обычный вывод, и ошибки сохраняются в одном файле all.txt
Подать ввод из файла sort < names.txt Файл names.txt используется как источник данных (stdin)

Как это запомнить

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

2.8.3

Файл в потоке ввода

  • 1> — основной поток (stdout),
  • 2> — поток ошибок (stderr),
  • < — поток ввода (stdin).

А комбинации вроде 2>&1 просто говорят оболочке: «Отправь ошибки туда же, куда направлен обычный вывод».

Коды возврата команд

Когда вы запускаете любую команду в терминале, она не только выводит результат на экран, но и сообщает системе, чем всё закончилось.
Это делается при помощи кода возврата (англ. exit code).

Код возврата — это число, которое программа передаёт обратно оболочке после завершения.
Оно показывает, успешно ли выполнена команда, и используется для автоматической проверки результата в скриптах и цепочках команд.

Код Значение Что это значит
0 Успех Команда выполнилась без ошибок
1 Общая ошибка Что-то пошло не так (универсальный код)
2 Ошибка аргументов Команде переданы неверные параметры или файл не найден
126 Команда найдена, но не может быть выполнена Нет прав или это не исполняемый файл
127 Команда не найдена Bash не смог найти указанную команду
130 Прерывание пользователем (Ctrl + C) Выполнение остановлено вручную

Важно

Эти значения не жёстко фиксированы: разные программы могут использовать свои коды ошибок, но принцип остаётся тем же: 0 всегда означает «всё хорошо».

Как проверить код возврата

После выполнения любой команды вы можете узнать её код при помощи специальной переменной $?.
Эта переменная всегда хранит код последней выполненной команды.

Пример успешной команды:

alice@ubuntu:~$ ls /home
alice
bob
carol
alice@ubuntu:~$ echo $?
0

Код 0 говорит о том, что последняя команда (а именно ls /home) выполнилась успешно.

Если команда завершилась с ошибкой:

alice@ubuntu:~$ ls /notfound
ls: cannot access '/notfound': No such file or directory
alice@ubuntu:~$ echo $?
2

Теперь код возврата равен 2. Это значит, что последняя команда выполнилась неуспешно, в нашем случае ls не смогла найти указанный каталог.

Даже если вы не видите вывода на экран, Bash всегда хранит код возврата последней команды. Это ключевой элемент для работы логических операторов и условий в скриптах, о них мы поговорим в следующей статье.

/dev/null — «чёрная дыра»

Иногда команда работает правильно, но её вывод совершенно не нужен.
Например, вы запускаете скрипт, который печатает десятки строк служебной информации, а вам важно только то, что он завершился успешно.
В таких случаях можно «утилизировать» весь вывод, не загромождая терминал.

Для этого в Linux существует специальное место — /dev/null.

Что это такое

/dev/null — это виртуальный файл, который не хранит данные. Всё, что вы в него запишете, навсегда исчезает.
Можно представить его как «чёрную дыру», которая принимает любые данные, но никогда их не возвращает.

Попробуйте:

alice@ubuntu:~$ echo "Hello" > /dev/null

Ничего не произошло, и это нормально.

Файл /dev/null просто «поглотил» строку Hello, не записав её никуда.

Полное подавление вывода

Если вы хотите, чтобы команда выполнялась молча, можно перенаправить в /dev/null и стандартный вывод (stdout), и ошибки (stderr):

alice@ubuntu:~$ ls -l /var /root > /dev/null 2>&1

Разберём, что делает эта запись:

  1. > /dev/null — отправляет обычный вывод (stdout) в «чёрную дыру».
  2. 2>&1 — направляет поток ошибок (stderr) туда же, куда сейчас направлен stdout, то есть тоже в /dev/null.

В результате команда выполнится, но на экране не появится ни одной строки, даже если произойдут ошибки.

Примеры использования

Цель Команда Что происходит
Скрыть успешный вывод, но оставить ошибки command > /dev/null Ошибки всё ещё видны на экране
Скрыть только ошибки command 2> /dev/null Основной результат остаётся
Скрыть всё command > /dev/null 2>&1 Команда выполняется полностью в тишине

Когда это полезно

Например, для автоматизации: скрипты и cron-задачи часто перенаправляют вывод в /dev/null, чтобы не засорять логи.

Также в тестировании можно проверять код возврата без вывода, как в примере ниже:

grep "ERROR" app.log > /dev/null
echo $?

А ещё при отладке удобно временно «заглушить» шумную команду, не влияя на логику скрипта.

В Linux есть и другие специальные устройства в /dev/, например:

  • /dev/zero — бесконечный поток нулей (используется для тестирования и создания файлов);
  • /dev/random — поток случайных данных.

Но /dev/null — самое известное и часто используемое: оно буквально говорит системе: «Забудь про это».

Что дальше

Теперь, когда вы разобрались, как команды в Linux принимают данные, выводят результаты и сообщают об ошибках, пора перейти к очередному важному шагу — научиться связывать команды между собой.

В следующей статье вы узнаете:

  • Что такое пайп (|) и как он передаёт вывод одной команды в другую.
  • Как строить простые цепочки команд и сложные конвейеры.
  • Как управлять последовательностью выполнения команд с помощью логических операторов.
  • И почему принцип KISS (Keep It Simple, Stupid) лежит в основе философии Unix и конвейеров команд.

После этого вы сможете объединять инструменты Linux в единые цепочки, автоматизировать проверки и анализ данных — от простых фильтров до целых сценариев.

Ключевые выводы по статье

  • Каждая команда в Linux работает с тремя стандартными потоками:
    — stdin — стандартный ввод (обычно клавиатура, но может быть файл или другая команда);
    — stdout — стандартный вывод (результат работы команды, по умолчанию — на экран);
    — stderr — поток ошибок (сообщения о сбоях и предупреждениях).
  • Перенаправления позволяют управлять этими потоками:
    — > — записать результат в файл (перезаписать его);
    — >> — добавить результат в конец файла;
    — 2> — записать ошибки (stderr) в отдельный файл;
    — > out.log 2> err.log — разделить основной и ошибочный выводы;
    — > all.log 2>&1 или &> all.log — объединить оба потока в один файл;
    — < file.txt — подать файл как входные данные (stdin).
  • Комбинируя перенаправления, можно создавать сложные цепочки: сохранять результаты в один файл, ошибки в другой или подавлять вывод вовсе.
  • Коды возврата (или exit-коды) — способ, которым команда сообщает, успешно ли она завершилась:
    — 0 — успех;
    — значения, отличные от 0, — ошибка (например: 2 — файл не найден, 127 — команда не найдена).
  • Код возврата можно проверить через $?, они часто используются в скриптах и цепочках команд.
  • /dev/null — «чёрная дыра» Linux: всё, что туда записано, исчезает. Используется для подавления ненужного вывода (> /dev/null 2>&1).