Любая утилита в Linux - это коробка с тремя проводами: один на вход, два на выход. Понимание, куда эти провода воткнуты по умолчанию и как их перетыкать, превращает набор разрозненных команд в инструмент. Этот урок про то, как администратор собирает из мелких программ обработку логов, фильтрацию, ветвление вывода и пакетные операции - без единой строчки на скриптовом языке, только средствами shell.

Как это работает
Когда ядро запускает процесс, оно открывает ему три файловых дескриптора. Дескриптор 0 - стандартный ввод (stdin), откуда программа читает. Дескриптор 1 - стандартный вывод (stdout), куда идет полезный результат. Дескриптор 2 - стандартный вывод ошибок (stderr), куда идут диагностика и предупреждения. По умолчанию все три привязаны к терминалу, поэтому ошибки и результат смешиваются на экране. Номера тут не случайность, а реальные числа в таблице открытых файлов процесса - именно поэтому в синтаксисе перенаправления мы пишем 2> и 1>&2.
Ключевая идея: stdout и stderr разделены намеренно. stdout несет данные, которые можно скормить дальше по конвейеру, а stderr несет сообщения для человека. Если бы они шли в один поток, любая ошибка попадала бы в данные и ломала следующую команду. Поэтому grep, find и прочие пишут результат в 1, а жалобы вроде "permission denied" в 2.
Перенаправление - это подмена того, куда указывает дескриптор, до запуска программы. Сам процесс этого не замечает: он как писал в дескриптор 1, так и пишет, просто за дескриптором теперь стоит файл, а не терминал. Конвейер (pipe) - это безымянный буфер в памяти ядра: stdout левой команды соединяется со stdin правой. Команды в конвейере запускаются параллельно, данные текут потоком, а не "сначала одна закончила, потом вторая начала".
Команды и примеры
Базовые перенаправления вывода. Один знак больше перезаписывает файл, два - дописывают в конец.
Код: Выделить всё
ls /etc > files.txt # stdout в файл (перезапись)
date >> files.txt # дописать в конец
sort < files.txt # stdin из файла
Код: Выделить всё
find /etc -name '*.conf' > found.txt 2> errors.txt # данные и ошибки в разные файлы
find /etc -name '*.conf' 2> /dev/null # ошибки выбросить, оставить только результат
make 2>&1 | less # объединить stderr со stdout и листать
make &> build.log # и stdout, и stderr в один файл (bash)
Код: Выделить всё
command 2>&1 > file # сначала 2 направили на терминал (туда, где был 1), потом 1 ушел в файл
command > file 2>&1 # правильно: сперва 1 в файл, потом 2 туда же, куда 1
Код: Выделить всё
dmesg | grep -i error | tee errors.log | wc -l # и в файл, и счетчик на экран
echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-fwd.conf # запись в файл от root через sudo
journalctl -p err | tee -a audit.log # -a дописать, не перезаписать
xargs строит команду из потока. Многие утилиты (rm, chmod, kill) принимают аргументы, а не stdin. xargs превращает строки ввода в аргументы.
Код: Выделить всё
find . -name '*.tmp' -print0 | xargs -0 rm -v # -print0/-0 защита от пробелов в именах
echo 'a b c' | xargs -n1 echo # -n1 по одному аргументу на запуск
cat hosts.txt | xargs -I{} -P4 ping -c1 {} # -I{} подстановка, -P4 четыре параллельно
Код: Выделить всё
cat <<'EOF' > /etc/motd
Доступ только для своих.
Все действия пишутся в журнал.
EOF
bc <<< '3 * (4 + 5)' # here-string: одна строка на stdin
grep root <<< "$(getent passwd)"
В Debian/Ubuntu и в RHEL/Fedora синтаксис shell одинаков (это bash), различий нет - перенаправление часть оболочки, а не дистрибутива.
Частые грабли
- Порядок 2>&1 и > перепутан. Запомните правило: сначала направляем stdout в файл, потом stderr дублируем за ним - только > file 2>&1.
- command > file читает И пишет в один файл (a > a) - shell обнуляет файл ДО запуска команды, данные теряются. Для in-place используйте sponge из пакета moreutils или временный файл.
- echo text | sudo command > /root/f - перенаправление делает ваш shell без root, отказ в доступе. Решение - sudo tee.
- xargs без -0 ломается на именах с пробелами и переводами строк. Связка find -print0 | xargs -0 обязательна для произвольных имен.
- Пустой ввод все равно запускает команду один раз. Добавляйте xargs --no-run-if-empty (в GNU это поведение по умолчанию у -r), иначе rm без аргументов может удалить не то.
- &> и >& - синтаксис bash, в POSIX sh их нет. В скриптах с #!/bin/sh пишите > file 2>&1.
- Конвейер прячет код возврата. Статус всего pipe - это статус ПОСЛЕДНЕЙ команды. Для проверки ошибок в середине нужен set -o pipefail.
- Запустите find /etc -name '*.conf' и убедитесь, что ошибки доступа смешаны с результатом на экране.
- Повторите, разделив потоки: результат в conf.txt, ошибки в err.txt. Проверьте оба файла.
- Соберите конвейер: journalctl -p warning | grep -i fail | tee fails.log | wc -l. Сверьте число строк с содержимым файла.
- Через sudo tee запишите строку в /etc/sysctl.d/99-lab.conf, затем убедитесь, что прямой sudo echo > туда падает с отказом.
- Командой find /tmp -type f -print0 | xargs -0 -I{} ls -l {} выведите детали файлов, корректно обработав пробелы в именах.
- Создайте here-doc с двумя строками текста в файл report.txt, один раз с подстановкой $(date), один раз с буквальной меткой 'EOF' - сравните результат.
- Какие номера дескрипторов у stdin, stdout и stderr и почему stderr отделен от stdout?
- Чем отличается результат command 2>&1 > log от command > log 2>&1 и почему?
- Зачем нужен tee и как с его помощью записать файл, требующий прав root?
- В каком случае обязательна связка find -print0 | xargs -0 и что произойдет без нее?
- Чем here-document отличается от here-string и что меняет кавычки вокруг метки (<<EOF против <<'EOF')?
- Почему конвейер a | b может вернуть код успеха, даже если a завершилась с ошибкой, и как это исправить?