У каждого процесса есть три открытых дескриптора: 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после пайпа, не видна снаружи.