Перенаправления в bash при помощи exec






Если вы частенько работаете в командной строке, вы должны быть знакомы с перенаправлением ввода/вывода. Но, возможно, вы не знакомы с перенаправлением ввода/вывода внутри bash-скрипта. Я не имею ввиду перенаправление, используемое при вызове других программ из скрипта, я говорю о перенаправлении ввода/вывода вашего скрипта в целом, начиная с момента его запуска.


Давайте, например, предположим, что вам понадобилось снабдить ваш скрипт опцией «--log», при помощи которой пользователь мог бы перенаправлять весь вывод работы вашего скрипта в нужный лог-файл. Конечно, пользователь мог бы просто перенаправить вывод скрипта средствами bash, но давайте представим, что по каким-то причинам такое решение не годится. Давайте реализуем это:

В первом утверждении if при помощи test выполняется проверка, подключён ли файловый дескриптор с номером один (поток стандартного вывода) к терминалу. Если это так, то exec «переоткрывает» его для записи в файл log. Вызов exec в контексте текущей оболочки без передачи ей команды, а только с указанием перенаправления вывода, приводит к тому, что вы можете открывать/закрывать файлы, дублируя их дескрипторы. Если же файловый дескриптор с номером 1 не подключён к терминалу, то мы просто не делаем ничего.

Если вы запустите вышеприведённый скрипт, то вы увидите, что первая и последняя echo выполнят свой вывод в терминал. Первая echo сработает потому, что она появляется до включения перенаправления, а вторая — поскольку в сценарии её вывод перенаправлен в стандартный поток ошибок (файловый дескриптор номер 2). И как же перенаправить стандартный поток ошибок в тот же файл? Всего ли одно небольшое изменение в вызове exec:

В примере выше exec перенаправляет поток ошибок туда же, куда направлен поток вывода (это, собственно, и называется дублированием файловых дескрипторов). Имейте ввиду, что порядок здесь очень важен: если вы его измените и переоткроете сперва поток ошибок (т. е. exec 2>&1 >log), то весь вывод всё равно останется направленным на терминал, поскольку он будет направлен туда же, куда и поток вывода, а тот, в свою очередь по умолчанию направлен в терминал.

Ради интереса можно попробовать выполнить то же самое, когда поток стандартного вывода не подключён к терминалу. Мы не сможем этого сделать, если поток вывода уже подключён к другому файлу или перенаправлен в конвейер, поскольку таким образом мы нарушим существовавшее перенаправление.

Возьмём, например, команду:

То, что нам нужно в конечном итоге, это эквивалент следующей команды:

Вероятно, вашей первой мыслью будет вызвать exec как-то так:

пытаясь таким образом при помощи exec перенаправить вывод в tee, запущенной в фоновом режиме. Но это не будет работать (хотя bash ничего и не скажет). Такая конструкция всего лишь перенаправляет вывод exec в tee, а поскольку exec не выводит ничего, то tee создаст пустой файл и на этом закончит.

Есть ещё одна мысль: запустить tee в фоне, направив в неё ввод из одного файлового дескриптора, а вывод перенапровить в другой. И вы можете это сделать, но проблема в том, что не существует способа создать новый процесс, стандартный ввод которого был бы подключён к каналу. Если бы мы могли это сделать, то получить вывод tee было бы просто, поскольку по умолчанию он направлен туда же, куда и вывод сценария целиком. Таким образом, мы могли бы просто закрыть поток вывода сценария и подключить его к нашему каналу, если бы была возможность этот канал создать.

Получается, мы в тупике? Нет. Решение описано в последних двух предложениях предыдущего абзаца. Нам нужно лишь создать канал, верно? Отлично, давайте использовать именованные каналы.

Здесь, если поток стандартного вывода не подключён к терминалу, мы создаём именованный канал (канал, который располагается в виде файла в файловой системе) при помощи mknod, и при помощи trap удаляем его после завершения работы сценария. Затем мы запускаем tee, связывая его поток ввода с созданным каналом и указываем выполнять запись в файл log. Помните, что tee кроме записи в файл всего полученного из потока ввода, также выводит всё в поток стандартного вывода. Также, вспомните о том, что поток вывода tee направлен туда же, куда и весь вывод сценария, вызывающего tee. Таким образом, весь вывод tee будет попадать туда, куда в данный момент направлен поток вывода нашего сценария, то есть, в перенаправленный пользователем поток вывода или канал конвейера, заданные в момент вызова сценария из командной строки. Так что теперь мы получили стандартный вывод tee там, где нам это нужно: в перенаправление или канал конвейера, определённый пользователем.

Теперь остаётся передать на вход tee нужные данные. Поскольку tee теперь считывает данные из именованного канала, всё что нам необходимо, это перенаправить стандартный вывод в именованный канал. Мы закрываем текущий поток вывода (exec 1>&- ) и открываем его в именованный канал (exec 1>$npipe). Обратите внимание, что закрытие текущего потока вывода ничего не нарушает, поскольку tee также осуществляет вывод в перенаправленный пользователем поток или канал.

Теперь, когда вы выполните команду и направите её вывод в канал к grep, вы получите и стандартный вывод и запись в лог-файле.

Подобных приёмов масса, откройте man-страницу bash!

P.S. В Bash 4 то же самое можно сделать при помощи конструкции coproc, но об этом в другой раз.

Mitch Frazier is an Associate Editor for Linux Journal and the Web Editor for linuxjournal.com.

По мотивам linuxjournal.com.