Урок по треку Linux 6 мин

Потоки ввода-вывода и пайпы за 6 минут

stdin, stdout, stderr, перенаправления, пайпы, tee, here-doc и коды возврата в конвейере.

У каждого процесса есть три открытых дескриптора: 0 stdin (ввод), 1 stdout (обычный вывод), 2 stderr (ошибки). Команды в Linux читают из 0 и пишут в 1 и 2, а shell решает, куда эти дескрипторы смотрят: в терминал, файл или другой процесс.

Перенаправления

cmd > out.txt        # stdout в файл (перезаписать)
cmd >> out.txt       # дописать в конец
cmd 2> err.txt       # stderr в файл
cmd > all.txt 2>&1   # оба потока в один файл
cmd &> all.txt       # то же, короткая форма bash
cmd < in.txt         # stdin из файла
cmd 2>/dev/null      # выбросить ошибки

2>&1 читается как «сделай дескриптор 2 копией того, куда сейчас смотрит 1». Поэтому порядок важен:

cmd > all.txt 2>&1   # оба потока в файл
cmd 2>&1 > all.txt   # stderr в терминал (куда смотрел 1 в момент копирования), stdout в файл

Shell обрабатывает перенаправления слева направо, до запуска команды.

Пайпы

a | b соединяет stdout команды a со stdin команды b. Обе команды запускаются одновременно, данные текут через буфер ядра (обычно 64 КиБ). Если b читает медленно, a блокируется на записи. Если b завершилась, a при следующей записи получает SIGPIPE и умирает: поэтому yes | head -1 не висит вечно.

stderr в пайп не попадает. Чтобы отправить оба потока: cmd 2>&1 | less или в bash cmd |& less.

Код возврата конвейера

По умолчанию код конвейера это код последней команды:

false | true; echo $?          # 0
set -o pipefail
false | true; echo $?          # 1

С pipefail конвейер вернёт код последней упавшей команды. Коды всех частей лежат в массиве PIPESTATUS: echo "${PIPESTATUS[@]}". В скриптах curl ... | tar xz без pipefail молча «успешен», даже если скачивание оборвалось.

tee: и в файл, и дальше

make 2>&1 | tee build.log            # видеть и сохранить
echo "nameserver 1.1.1.1" | sudo tee /etc/resolv.conf

Второй пример решает классическую ловушку: sudo echo x > /etc/file не работает, потому что перенаправление делает ещё не повышенный shell. tee запускается под sudo и сам открывает файл.

Here-doc и here-string

cat > app.conf <<'EOF'
port=8080
home=$HOME
EOF

grep -c x <<< "$text"

С кавычками вокруг 'EOF' переменные не подставляются, без кавычек подставляются. <<-EOF срезает ведущие табы (не пробелы).

Подстановка процессов

<(cmd) превращает вывод команды в «файл» вида /dev/fd/63:

diff <(sort a.txt) <(sort b.txt)

Удобно, когда программа ждёт имя файла, а не stdin.

Особые файлы

  • /dev/null: всё записанное пропадает, чтение даёт конец файла.
  • /dev/zero: бесконечные нули, /dev/urandom: случайные байты.
  • /dev/stdin, /dev/stdout, /dev/stderr: ссылки на дескрипторы 0, 1, 2.

Буферизация

Когда stdout смотрит в терминал, программы на C буферизуют построчно, а когда в пайп или файл, блоками по несколько КиБ. Поэтому tail -f log | grep x | sort кажется «зависшим»: sort ждёт конца ввода, а grep копит буфер. Помогают grep --line-buffered и stdbuf -oL.

Ловушка: read в пайпе

count=0
cat list.txt | while read -r line; do count=$((count+1)); done
echo $count   # 0

Каждая часть пайпа в bash выполняется в подоболочке, и переменная меняется в ней. Решения: while ... done < list.txt или shopt -s lastpipe в скрипте.

Частые ошибки

  • cmd 2>&1 > file вместо cmd > file 2>&1.
  • sudo echo ... > /etc/... вместо | sudo tee.
  • Конвейеры в CI без set -o pipefail.
  • cat file | grep x там, где хватит grep x file (не ошибка, но лишний процесс).
  • > file на файл, который читается в той же команде: sort f > f обнуляет файл до чтения.

Что спросят на собесе

  • Что такое дескрипторы 0, 1, 2 и как перенаправить stderr.
  • Чем отличаются > f 2>&1 и 2>&1 > f.
  • Что вернёт конвейер, если упала первая команда, и что делает pipefail.
  • Почему sudo echo > /etc/file не работает.
  • Почему переменная, изменённая в while read после пайпа, не видна снаружи.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.