[Перевод] Bash Co-Processes

Одной из новых функций в Bash 4.0 является coproc. Оператор coproc позволяет создавать со-процесс, который связан с командной оболочкой с помощью двух каналов: один для отправки данных в со-процесс, второй для получения из со-процесса.

Впервые я нашёл применение этому пытаясь писать лог используя перенаправление exec. Цель состояла в том, чтобы опционально разрешить запись вывода скрипта в лог-файл после запуска сценария (например, вследствие опции --log командной строки).

Основная проблема с логированием вывода после того как скрипт стартовал связана с тем, что его вывод уже мог быть перенаправлен (в файл или канал). Если мы перенаправим уже перенаправленный вывод, то не сможем выполнить команду так, как это было задумано пользователем.
Предыдущая реализация была сделана с использованием именованных каналов:

#!/bin/bash

echo hello

if test -t 1; then
    # Stdout is a terminal.
    exec >log
else
    # Stdout is not a terminal.
    npipe=/tmp/$$.tmp
    trap "rm -f $npipe" EXIT
    mknod $npipe p
    tee <$npipe log &
    exec 1>&-
    exec 1>$npipe
fi

echo goodbye


Из прошлой статьи:

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


Мы можем сделать то же самое с помощью со-процессов:

echo hello

if test -t 1; then
    # Stdout is a terminal.
    exec >log
else
    # Stdout is not a terminal.
    exec 7>&1
    coproc tee log 1>&7
    #echo Stdout of coproc: ${COPROC[0]} >&2
    #echo Stdin of coproc: ${COPROC[1]} >&2
    #ls -la /proc/$$/fd
    exec 7>&-
    exec 7>&${COPROC[1]}-
    exec 1>&7-
    eval "exec ${COPROC[0]}>&-"
    #ls -la /proc/$$/fd
fi
echo goodbye
echo error >&2


В случае, если наш стандартный вывод идет на терминал, мы просто используем exec, чтобы перенаправить наш вывод в нужный log-файл. Если же вывод не связан с терминальным устройством, то мы используем coproc для запуска tee как со-процесс и перенаправляем наш вывод на вход tee и выход tee перенаправляем туда, куда изначально и планировалось.
Запуск tee с использование coproc по сути работает также как tee в фоновом режиме (например, tee log &), основное отличие заключается в том, что к нему подключены оба канала (вход и выход). По умолчанию, Bash помещает файловые дескрипторы этих каналов в массив COPROC.

  • COPROC[0] это файловый дескриптор канала подключенного к стандартному выводу со-процесса;
  • COPROC[1] подключен к стандартному вводу со-процесса.


Учитывайте, что эти каналы должны быть созданы до использования редиректов в команде.
Обратите внимание на ту часть скрипта, в которой вывод сценария не связан с терминалом. Следующая строка дублирует наш стандартный вывод в файловый дескриптор 7.

exec 7>&1


Затем мы стартуем tee с перенаправлением его вывода в файловый дескриптор 7.

coproc tee log 1>&7


Теперь tee будет писать, всё что он считывает со стандартного ввода в файл под именем log и файловый дескриптор 7, который является нашим текущим стандартным выводом.
Теперь мы закроем дескриптор файла 7 (помните, что tee ещё и «file», который открыт на 7 в качестве стандартного вывода):

exec 7>&-


Так как мы закрыли 7 мы можем вновь использовать его, поэтому мы переносим канал присоединённый ко входу 7 tee:

exec 7>&${COPROC[1]}-


Затем мы перемещаем наш стандартный вывод в канал присоединённый к стандартному вводу tee (наш файловый дескриптор 7) посредством:

exec 1>&7-


И, наконец, мы закрываем канал присоединённый к выходу tee, так как у нас больше в нём нет необходимости:

eval "exec ${COPROC[0]}>&-"


В данном случае eval здесь необходим, поскольку в противном случае Bash считает, что значение ${COPROC[0]} является командой. С другой стороны, это не требуется выше (exec 7>&${COPROC[1]}-), потому что Bash знает, что »7» инициирует работу с файловым дескриптором, а не считается командой.
Также обратите внимание на закомментированную строку:

#ls -la /proc/$$/fd


Это полезно для просмотра файлов, открытых текущим процессом.

Теперь мы добились желаемого эффекта: наш стандартный вывод будет направлен в tee. У tee есть «вход» в наш лог-файл и запись идёт и в канал и в файл, что изначально и планировалось.
Пока я не могу придумать никаких других задач для со-процессов, по крайней мере не являющихся надуманными. Смотрите страницу man bash для получения дополнительной информации о со-процессах.

© Habrahabr.ru