Практическое использование для перемещения файловых дескрипторов

16

Согласно странице руководства bash:

Оператор перенаправления

   [n]<&digit-

перемещает дескриптор файла в дескриптор digitфайла nили стандартный ввод (дескриптор файла 0), если nон не указан. digitзакрывается после дублирования на n.

Что значит «переместить» дескриптор файла в другой? Каковы типичные ситуации для такой практики?

Quentin
источник

Ответы:

14

3>&4-это расширение ksh93, также поддерживаемое bash, и оно сокращенно 3>&4 4>&-, то есть 3 теперь указывает на то место, где раньше использовалось 4, а 4 теперь закрыто, поэтому то, на что указывал 4, теперь переместилось на 3.

Типичное использование будет в тех случаях, когда вы дублируете stdinили stdoutсохраняете копию и хотите восстановить ее, как в:

Предположим, вы хотите перехватить stderr команды (и только stderr), оставив stdout в одной переменной.

Подстановка команды var=$(cmd), создает канал. Конец записи канала становится cmdstdout (дескриптор файла 1), а другой конец читается оболочкой для заполнения переменной.

Теперь, если вы хотите , stderrчтобы перейти к переменной, вы можете сделать: var=$(cmd 2>&1). Теперь и fd 1 (stdout), и 2 (stderr) переходят в канал (и, в конечном итоге, к переменной), что составляет лишь половину того, что мы хотим.

Если мы делаем var=$(cmd 2>&1-)(сокращение от var=$(cmd 2>&1 >&-), теперь только cmdканал 's stderr' идет в канал, но fd 1 закрыт. Если cmdпопытается записать какой-либо вывод, который вернется с EBADFошибкой, если он откроет файл, он получит первый свободный fd, и открытый файл будет назначен ему, stdoutесли только команда не защитит от этого! Не то, что мы хотим.

Если мы хотим, чтобы стандартный вывод cmdбыл оставлен в покое, то есть указывал на тот же ресурс, на который он указывал вне подстановки команд, то нам нужно каким-то образом перенести этот ресурс в подстановку команд. Для этого мы можем сделать копию stdout внешней подстановки команды, чтобы взять ее внутрь.

{
  var=$(cmd)
} 3>&1

Какой способ написания чище:

exec 3>&1
var=$(cmd)
exec 3>&-

(который также имеет преимущество восстановления fd 3 вместо закрытия в конце).

Затем на {(или exec 3>&1) и до }, оба fd 1 и 3 указывают на один и тот же ресурс, на который fd 1 указал изначально. fd 3 также будет указывать на этот ресурс внутри подстановки команд (подстановка команд перенаправляет только fd 1, stdout). Итак, выше, cmdу нас есть для fds 1, 2, 3:

  1. труба к вар
  2. нетронутый
  3. так же, как то, что 1 указывает на замещение команды

Если мы изменим это на:

{
  var=$(cmd 2>&1 >&3)
} 3>&1-

Тогда это становится:

  1. так же, как то, что 1 указывает на замещение команды
  2. труба к вар
  3. так же, как то, что 1 указывает на замещение команды

Теперь у нас есть то, что мы хотели: stderr идет в трубу, а stdout остается нетронутым. Тем не менее, мы просочились в фд 3 к cmd.

Хотя команды (по соглашению) предполагают, что fds 0 - 2 открыты и являются стандартным вводом, выводом и ошибкой, они не предполагают ничего из других fds. Скорее всего, они оставят этот 3 нетронутым. Если им нужен другой файловый дескриптор, они просто сделают, open()/dup()/socket()...который вернет первый доступный файловый дескриптор. Если (как и скрипт оболочки, который делает это exec 3>&1) им нужно использовать это fdспециально, они сначала назначат его чему-то (и в этом процессе ресурс, удерживаемый нашим fd 3, будет освобожден этим процессом).

Рекомендуется закрывать этот fd 3, поскольку cmdон не используется, но нет ничего страшного, если мы оставим его назначенным до вызова cmd. Проблемы могут заключаться в том, что cmd(и, возможно, другие процессы, которые он порождает) имеет меньше доступного fd. Потенциально более серьезная проблема заключается в том, что ресурс, на который указывает этот fd, может в конечном итоге удерживаться процессом, порожденным этим cmdв фоновом режиме. Это может быть проблемой, если этот ресурс является каналом или другим каналом межпроцессного взаимодействия (например, когда ваш скрипт выполняется как script_output=$(your-script)), так как это будет означать, что чтение процесса с другого конца никогда не будет видеть конец файла до тех пор, пока фоновый процесс завершается.

Итак, здесь лучше написать:

{
  var=$(cmd 2>&1 >&3 3>&-)
} 3>&1

Который bashможет быть сокращен до:

{
  var=$(cmd 2>&1 >&3-)
} 3>&1

Подводя итог, почему это редко используется:

  1. это нестандартный и просто синтаксический сахар. Вы должны сбалансировать сохранение нескольких нажатий клавиш, чтобы сделать ваш сценарий менее переносимым и менее очевидным для людей, не привыкших к этой необычной функции.
  2. Необходимость закрытия исходного fd после дублирования часто упускается из виду, потому что большую часть времени мы не страдаем от последствий, поэтому мы просто делаем >&3вместо >&3-или >&3 3>&-.

Доказательством того, что он редко используется, как вы узнали, является то, что это фальшивка в bash . В bash compound-command 3>&4-или any-builtin 3>&4-листья fd 4 закрыты даже после compound-commandили any-builtinвернулся. Патч , чтобы исправить эту проблему сейчас (2013-02-19) доступен.

Стефан Шазелас
источник
Спасибо, теперь я знаю, что такое перемещение fd. У меня есть 4 основных вопроса относительно второго фрагмента (и fds в целом): 1) В cmd1 вы делаете 2 (stderr) как копию 3, что, если команда внутренне будет использовать этот 3 fd? 2) Почему 3> & 1 и 4> & 1 работают? Дублирование 3 и 4 действует только в этих двух cmds, влияет ли текущая оболочка? 3) Почему вы закрываете 4 в cmd1 и 3 в cmd2? Эти команды не используют упомянутые fds, не так ли? 4) В последнем фрагменте, что произойдет, если fd будет дублирован на несуществующий (в cmd1 и cmd2), я имею в виду 3 и 4 соответственно?
Квентин
@Quentin, я использовал более простой пример и объяснил немного больше, надеясь, что теперь он вызывает меньше вопросов, чем ответов. Если у вас все еще есть вопросы, не связанные напрямую с синтаксисом fd moving, я предлагаю вам задать отдельный вопрос
Стефан Шазелас
{ var=$(cmd 2>&1 >&3) ; } 3>&1-Разве это не опечатка в закрытии 1?
Квентин
@Quentin, это опечатка в том смысле, что я не думаю, что я намеревался включить ее, однако это не имеет значения, так как ничто внутри фигурных скобок не использует этот fd 1 (это был весь смысл дублировать его на fd 3: потому что оригинал 1 иначе не было бы доступно внутри $(...)).
Стефан Шазелас
1
@Quentin При входе {...}fd 3 указывает на то, что fd 1 использовалось для указания, а fd 1 закрывается, затем при входе $(...)fd 1 устанавливается на трубу, которая подает$var , затем также на cmd2 на эту, а затем 1 на 3 точки к, это внешний 1. Тот факт, что 1 остается закрытым, является ошибкой в ​​bash, я сообщу об этом. ksh93, откуда взялась эта особенность, не имеет этой ошибки.
Стефан Шазелас
4

Это означает, что он указывает на то же место, что и другой дескриптор файла. Вы должны сделать это очень редко, кроме очевидной отдельной обработки стандартного дескриптора ошибок ( stderr, fd 2, /dev/stderr -> /proc/self/fd/2). Это может пригодиться в некоторых сложных случаях.

Руководство по расширенному написанию сценариев Bash содержит более длинный пример уровня журнала и этот фрагмент:

# Redirecting only stderr to a pipe.
exec 3>&1                              # Save current "value" of stdout.
ls -l 2>&1 >&3 3>&- | grep bad 3>&-    # Close fd 3 for 'grep' (but not 'ls').
#              ^^^^   ^^^^
exec 3>&-                              # Now close it for the remainder of the script.

В «Волшебстве исходного мага» мы, например, используем его, чтобы различить разные выводы из одного и того же блока кода:

  (
    # everything is set, so run the actual build infrastructure
    run_build
  ) 3> >(tee -a $C_LOG >> /dev/stdout) \
    2> >(tee -a $C_LOG 1>&2 > $VOYEUR_STDERR) \
     > >(tee -a $C_LOG > $VOYEUR_STDOUT)

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

lynxlynxlynx
источник
0

В Unix файлы обрабатываются дескрипторами файлов (маленькие целые числа, например стандартный ввод равен 0, стандартный вывод равен 1, стандартная ошибка равна 2; при открытии других файлов им обычно назначается наименьший неиспользуемый дескриптор). Поэтому, если вы знаете внутреннюю часть программы и хотите отправить выходные данные, которые идут в файловый дескриптор 5, в стандартный вывод, вы должны переместить дескриптор 5 на 1. Вот откуда это 2> errorsпроисходит, и конструкции любят 2>&1дублировать ошибки в выходной поток.

Так что вряд ли когда-либо использовался (я смутно помню, как использовал его один или два раза в гневе за мои 25 с лишним лет почти исключительного использования Unix), но когда это необходимо, абсолютно необходимо.

vonbrand
источник
Но почему бы не дублировать дескриптор файла 1 следующим образом: 5> & 1? Я не понимаю, что толку в перемещении FD, так как ядро ​​собирается закрыть его сразу после ...
Квентин,
Это не дублирует 5 в 1, это посылает 5 туда, куда идет 1. И прежде чем спросить; да, есть несколько различных обозначений, взятых из различных оболочек-прекурсоров.
vonbrand
Все еще не понимаю. Если 5>&1отправляет 5 туда, куда идет 1, то что именно делает 1>&5-, кроме закрытия 5?
Квентин