7.1. Перенаправление вывода и использование tee(1)

[+]7.1.1. Особенности csh(1)

Описание:  Кандидат BSDA должен уметь перенаправлять стандартный вывод, ввод или поток ошибок программы, использовать pipe чтобы послать вывод одной программы в другую программу или в файл. Использовать tee(1) чтобы копировать стандартный ввод на стандартный вывод.

Практика: <, >, |, tee(1), >& и |&

Комментарий

За каждой программой запущенной в UNIX (и не только UNIX) закреплено минимум три файловых дескриптора: стандартный ввод (STDIN), стандартный вывод (STDOUT) и стандартный вывод ошибок (STDERR). Хотя мы говорим «файловый дескриптор», на самом деле это не обязательно именно файлы. Речь идёт об «обобщённых файлах» — некоторых объектах, куда можно писать и откуда можно читать. В норме, приложение запущенное в терминале направляет STDOUT и STDERR на консоль. Таким образом, мы видим результат деятельности программы напечатанным на экране. STDIN программы, это поток информации читаемый ею с клавиатуры.

Напрмер, запустим программу grep следующим образом:

$ grep r
          

В этом случае программа grep будет искать строки содержащие букву r в потоке STDIN (то есть в тексте набираемом с клавиатуры), и выводить этот текст на STDOUT (то есть на экран). Сразу после запуска ничего не происходит: команда grep ожидает ввода с клавиатуры т.е. читает STDIN. Мы печатаем текст и нажимаем клавишу <Enter>. После этого строка попадает в grep и если она содержит букву r она печатается второй раз на экране т.е. grep печатает её на STDOUT.

Ниже приведён листинг такого примера. Знаком < помечены строки котороые набрал пользователь (STDIN), а знаком > строки напечатанные в ответ программой grep(1).

$ grep r
< DragonFly BSD
> DragonFly BSD
< FreeBSD
> FreeBSD
< OpenBSD
< NetBSD
        

В нашем листинге STDIN и STDOUT обозначены значками < и >. В жизни этого, конечно, не происходит. Всё вперемешку, что крайне неудобно. Да и вообще, трудно представить себе что кто-то будет руками набивать текст только для того, чтобы его профильтровал grep(1). Гораздо удобнее пользоваться символами перенаправления для переопределения стандартного ввода и вывода.

Пусть у нас есть файл BSDA содержащий названия изучаемых нами BSD систем.

$ cat BSDA
DragonFly BSD
FreeBSD
OpenBSD
NetBSD
        

Применим grep для того, чтобы выяснить какие системы BSD содержат в своём названии букву r. Для этого мы переопределим STDIN команды grep. Теперь вместо того, чтобы читать текст с клавиатуры, grep(1) будет читать его из файла BSDA:

$ grep r < BSDA
DragonFly BSD
FreeBSD
        

А что если нам надо сохранить вывод программы grep в файл? Тогда мы должны переопределить ещё и STDOUT:

$ grep r < BSDA > BSDA-r
        

Теперь у нас появился файл с названием BSDA-r содержащий две строки:

$ cat BSDA-r
DragonFly BSD
FreeBSD
        

Но а что если нам надо узнать сколько систем BSD имеют в своём имени букву r? Для этого мы можем воспользоваться программой wc(1) с аргументом -l. Команда wc -l считает строки в своём STDIN и печатает результат на STDOUT. Таким образом, мы можем выполнить последовательно 2 команды:

$ grep r < BSDA > BSDA-r
$ wc -l < BSDA-r
         2
        

Но это неудобно: мы зачем-то создавали временный файл, передавали копеечную информацию и для этого обращались к диску, а это медленная операция. А если бы у нас было много информации, то наши действия тоже были бы нерациональны: Весь STDOUT grep(1)'а мог не поместиться на диске (может там миллион строк!), но он и не нужен wc(1) для работы, wc(1) может каждый отдельный момент работать с кусочком файла.

Напрашивается естественный вывод: надо переопределить STDOUT grep'а так, чтобы он стал STDIN'ом wc(1). Такую конструкцию называют pipe или конвейер.

$ grep r < BSDA | wc -l
         2
        

Стандартный вывод wc(1) (число 2) тоже можно передать на стандартный ввод другой программы, таким образом длину конвейера или трубы (pile-line) можно сделать сколь угодно длинной. В следующем примере количество систем BSD содержащих в своём названии букву r будет распечатано на принтере:

$ grep r < BSDA | wc -l | lpr
        

А чтоже делать, если мы хотим и список систем получить и посчитать их количество? Можно как и прежде выполнить 2 действия поочереди: сперва создать файл BSDA-r, полюбоваться на него, а потом скормить его программе wc. А можно воспользоваться программой tee(1). tee ничего не делает с потоком данных, которые через неё идут, она просто копирует STDIN в STDOUT. Но если ей в качестве аргумента задать некоторый файл, то она будет заодно записывать эту информацию и в него. Таких файлов команде tee можно задать много. Таким образом, tee является разветвителем в трубе:

$ grep r < BSDA | tee BSDA-r | wc -l
         2
$ cat BSDA-r
DragonFly BSD
FreeBSD
        

tee можно использовать как здесь — для сохранения промежуточных результатов, а можно использовать для того, чтобы протоколировать в файл то, что администратор видит на экране. Напрмер, ниже программа make будет писать что-то на экран, но впоследствии мы сможем прочитать что она там писала из файла make-log:

$ make | tee make-log
        

В этом примере мы добились развоения стандартного вывода программы make: этот поток информации одновременно пишется в файл make-log и печатается в окне терминала обычным образом.

Теперь поговорим про STDERR. Пусть у нас в текущем каталоге есть два файла get.sh и put.sh, и более ничего нет. Выполним следующее действие:

$ ls *sh *gz
ls: *gz: No such file or directory
get.sh  put.sh
        

Мы видим, что на экране присуствует как список файлов с расширением sh так и сообщение об ошибке связанное с тем, что в текущем каталоге отсутствуют файлы с расширением gz. Обе эти строки напечатаны в окне терминала, но это два разных потока. Сообщение об ошибке было напечатано в стандартный вывод ошибок — STDERR. Убедимся в этом:

$ ls *sh *gz > /dev/null
ls: *gz: No such file or directory
$ ls *sh *gz 2> /dev/null
get.sh  put.sh
        

В приведённом примере мы в первом случае перенаправили STDOUT в файл /dev/null (это уcтройство поглащающее байты вроде «чёрной дыры»), а во втором случае мы перенаправили STDERR. Поэтому в первом случае у нас напечаталось только сообщение об ошибке, а во втором только список файлов с расширением sh.

Файловые дескрипторы соответствующие STDIN, STDOUT и STDERR имеют номера, соответственно 0, 1 и 2. Когда мы пишем знак >, система неявно предполагает, что мы перенаправляем дескриптор с номером 1 и перенаправляет STDOUT. Если же мы хотим перенаправить STDERR нам надо явно указать его номер: 2>.

[Важно]Важно
2> пишется слитно без пробела.

Вы можете объединить файловые дескрипторы, если вам надо, например, писать сообщения выводящиеся на STDOUT и STDERR в один файл:

$ make > make.log 2>&1
        

Здесь мы направили стандартный вывод в файл make.log (написав > make.log), а затем STDERR перенаправили в STDOUT (написав 2>&1). Причём порядок действий здесь важен. Команда

$ make 2>&1 > make.log
        

приведёт к тому, что в файл make.log будет направлен только STDOUT, так как STDERR был перенаправлен тогда, когда STDOUT направлялся ещё на консоль.

7.1.1. Особенности csh(1)

Синтаксис перенаправления в csh(1) отличается от синтакиса в sh(1). Ниже приведены эквивалентные команды на sh(1) и на csh(1):

sh:  $ make 2>&1 > make.log
csh: % make >& make.log

sh:  $ make 2>&1 | less
csh: % make |& less

sh:  $ make 2>/dev/null > make.log
csh: % (make > make.log) >& /dev/null
        

В последней строке продемонстрировано досадное ограничение csh(1): для того, чтобы перенаправить STDERR и STDOUT в разные места приходится заворачиваться в блин: перенаправить произвольный файловый дескриптор по его номеру, увы, нельзя. Можно лишь пользоваться перенаправлением суммы STDERR и STDOUT используя символы |& и >&.