Перенаправление потоков
В каждом языке есть собственный способ напечатать результат на экран:
javascript
console.log('hello!');
php
<?php
echo 'hello!';
python
print('hello!')
java
System.out.print("hello!");
ruby
puts 'hello!'
Несмотря на разнообразие языков и способов печати, с точки зрения операционной системы, которая запускает программу, все они работают абсолютно идентично. При старте любой программы операционная система связывает с ней три так называемых потока: STDIN (Standard Input), STDOUT (Standard Output) и STDERR (Standard Error). Для языка программирования они выглядят как файлы, и взаимодействие с ними происходит как с файлами. STDOUT как раз отвечает за вывод на экран. Каждый раз, когда в программе (на любом языке) происходит печать на экран, функция печати, на самом деле, записывает с помощью функции write данные в STDOUT, а вот уже операционная система решает куда вывести результат. По умолчанию вывод происходит на экран терминала.
Здесь нужно сказать, что хорошее понимание этой темы требует знания устройства операционных систем, в частности подсистемы отвечающей за процессы и файловую систему. В двух словах, никакой язык программирования не может знать про существование экрана, а уж тем более не может с ним взаимодействовать. Ответственность за взаимодействие с железом целиком и полностью лежит на плечах операционной системы, а программы могут только лишь попросить операционную систему выполнить ту или иную задачу. При таком разделении реализация языков программирования сильно упрощается. Достаточно знать про существование STDOUT и уметь писать в него, а дальше всё сделает операционная система. Это значит, что программа, написанная на одном компьютере, без проблем запустится на другом с другой конфигурацией и монитором (или даже без монитора).
Самое удивительное начинается дальше. ОС позволяет подменять эти потоки при старте системы, что открывает интересные возможности. Например, вывод любой команды, запущенной в баше, можно записать в файл вместо вывода на экран.
$ ls -la > output
Запустив эту команду, вы увидите, что на экране ничего не отобразилось, но в текущей директории появился файл output.
$ cat output
total 58
rwx------ 2 rekoshed rekoshed 4096 Jul 25 11:03 .some
drwxrwxr-x 21 rekoshed rekoshed 4096 Sep 1 01:58 Some.Disk
drwxr-xr-x 2 rekoshed rekoshed 4096 Jul 14 09:25 Видео
drwxr-xr-x 2 rekoshed rekoshed 4096 Jul 14 09:25 Документы
drwxr-xr-x 2 rekoshed rekoshed 4096 Sep 27 09:17 Загрузки
drwxr-xr-x 2 rekoshed rekoshed 4096 Jul 27 08:12 Изображения
drwxr-xr-x 2 rekoshed rekoshed 4096 Jul 14 09:25 Музыка
drwxr-xr-x 2 rekoshed rekoshed 4096 Jul 14 09:25 Общедоступные
drwxr-xr-x 2 rekoshed rekoshed 4096 Jul 14 09:25 Шаблоны
Операция, которую мы сделали выше, называется перенаправление потоков. Символ > означает, что нужно взять вывод из команды, указанной слева, и отправить его в файл, указанный справа. > всегда перезаписывает файл. Такое перенаправление работает с абсолютно любой командой, которая выводит результаты своей работы на экран.
$ grep alias .bash_profile > result
$ cat result
alias fixssh='eval $(tmux showenv -s SSH_AUTH_SOCK)'
Если нужно не перезаписывать, а добавлять, то используйте >>.
Для экспериментов с выводом удобно использовать встроенную в шел команду echo. Она принимает на вход строчку и выдаёт её в STDOUT, который уже можно перенаправлять.
# > перетирает файл
$ echo 'hello' > result
$ cat result
hello
$ echo 'hello' > result
$ cat result
hello
# >> добавляет содержимое в конец файла
$ echo 'hello' >> result
$ cat result
hello
hello
$
Кроме стандартного вывода, с каждым процессом ассоциируются два дополнительных потока: один STDIN (стандартный ввод) и STDERR (вывод ошибок). STDIN работает в обратную сторону: через него программа может получать данные на вход. В *nix системах встроена утилита wc
(word count — "количество слов"), которая умеет считать количество слов, строк или символов в файле. Когда мы говорим о файле, то в *nix это почти всегда означает, что данные можно передать и в стандартный поток ввода.
# Флаг l (l а не 1) говорит о том, что надо считать количество строк
$ wc -l < result
2
Выглядит довольно логично — стрелка меняет своё направление в другую сторону и содержимое файла отправляется в STDIN запускаемой программы wc. Теперь сделаем финт и объединим перенаправление ввода и вывода.
$ wc -l < result > output
$ cat output
$ 2
Кстати, таким же способом можно отправить вывод на печать, но оставлю эту возможность на самостоятельное изучение.
Последний вопрос связан с тем, зачем нужен STDERR. Он, как и STDOUT, по умолчанию идёт на экран. STDERR позволяет отделить нормальный вывод программы от возникающих ошибок. Такой подход удобен при ведении логов, для реагирования и отладки. Будьте осторожны, перенаправление вывода в файл перенаправляет только STDOUT. Убедиться в этом очень просто. Если попробовать перейти в несуществующую директорию, то команда cd выдаст ошибку:
$ cd lala
-bash: cd: lala: No such file or directory
Теперь попробуем перенаправить вывод в файл output
$ cd lala > output
-bash: cd: lala: No such file or directory
Перенаправление есть, но сообщение вывелось на экран. Это произошло именно по той причине, что STDERR остался привязан к экрану, а внутри файла output — пустота. Решить эту задачу можно двумя способами. Перенаправив STDERR в STDOUT, либо отправив их оба в файл.
Перенаправление STDERR в STDOUT
# Сначала STDERR перенаправляется в STDOUT, затем STDOUT в файл
$ cd lala > output 2>&1
$ cat output
-bash: cd: lala: No such file or directory
2 - в данном случае обозначает номер потока. В POSIX, за каждым потоком закреплен определенный номер, который является файловым дескриптором если быть точным: STDIN — 0, STDOUT — 1, STDERR — 2. Конструкцию 2>&1 нужно просто запомнить, она говорит о том, что поток с номером 2 отправляем в поток с номером 1
Перенаправление STDERR бывает полезно само по себе, без вывода в файл.
# STDERR просто перенаправляется в другой поток (STDOUT)
$ cd lala 2>&1
-bash: cd: lala: No such file or directory
Перенаправление обоих потоков в файл
# Сначала STDERR перенаправляется в STDOUT, затем STDOUT в файл
$ cd lala &> output
$ cat output
-bash: cd: lala: No such file or directory
Пайплайн
Раз у одного процесса есть вход, а у другого — выход, и их можно подменять, то логично предположить, что их можно соединить. Данный подход носит название pipeline (конвейер). Благодаря пайплайну можно соединять программы и протаскивать данные сквозь них, как сквозь цепочку функций, каждая из которых выступает в роли преобразователя или фильтра.
Когда мы грепали, то делали это по какому-то одному слову, но часто возникает задача грепать по нескольким словам. Не важно, как они расположены внутри строки, главное, что они встречаются там вместе. Такую функциональность можно было бы сделать, усложнив саму программу grep. Но пайплайн позволяет добиться такого же поведения без необходимости писать сложную программу.
$ grep alias .bashrc | grep color # enable color support of ls and also add handy aliases alias ls='ls --color=auto' #alias dir='dir --color=auto' #alias vdir='vdir --color=auto' alias grep='grep --color=auto' alias fgrep='fgrep --color=auto' alias egrep='egrep --color=auto'
| — этот символ называется пайп, он указывает шелу взять STDOUT одного процесса и соединяет его с STDIN другого процесса. Поскольку grep принимает на вход текст (как я говорил в прошлом уроке, все утилиты, которые читают файлы, могут принимать данные через STDIN) и возвращает текст, то его можно комбинировать бесконечно.
Запись grep alias .bashrc | grep color можно изменить, используя перенаправление. Так она станет проще для модификации:
$ cat .bashrc | grep alias | grep color
В примере выше файл читается катом и отправляется в STDIN грепа.
Ещё один пример:
~$ cat source Snoop Dog Doggy cupcakes Tom cat Reservoir Dogs Jerry mouse Dog Dog ~$ ~$ cat source | grep Dog | uniq | sort Dog Doggy cupcakes Reservoir Dogs Snoop Dog ~$
cat source | grep Dog | uniq | sort
- Читается файл source
- Входные данные грепаются по подстроке "Dog"
- Убираются дубли (в исходном файле две одинаковых строки "Dog")
- Входные данные сортируются и выводятся на экран
Пайплайн стал основой Unix философии, которая звучит так:
- Пишите программы, которые делают что-то одно и делают это хорошо.
- Пишите программы, которые бы работали вместе.
- Пишите программы, которые бы поддерживали текстовые потоки, поскольку это универсальный интерфейс.
Именно поэтому большинство утилит работают с сырым текстом — принимают его на вход и возвращают в STDOUT. Такой подход позволяет получать сложное поведение из крайне простых составных блоков. Такая концепция называется стандартные интерфейсы и хорошо отражена в конструкторах Lego.