Как сохранить целевое местоположение / dev / stdout в bash-скрипте?

12

У меня есть определенный bash-скрипт, который хочет сохранить исходное /dev/stdoutместоположение перед заменой первого файлового дескриптора другим местоположением.

Поэтому, естественно, я написал что-то вроде

old_stdout=$(readlink -f /dev/stdout)

И это не сработало. Очень быстро я понимаю, в чем проблема:

test@ubuntu:~$ echo $(readlink -f /dev/stdout)
/proc/5175/fd/pipe:[31764]
test@ubuntu:~$ readlink -f /dev/stdout
/dev/pts/18

Obvioulsly, $()работает в подоболочке, которая передается в родительскую оболочку.

Поэтому возникает вопрос: есть ли надежный (ограниченный переносимость между дистрибутивами Linux) способ сохранить /dev/stdoutместоположение в виде строки в скрипте bash?

alexey.e.egorov
источник
Это звучит немного как проблема XY . Что является основной проблемой?
Кусалананда
Основной проблемой является некий сценарий установки, который выполняется в двух режимах - без вывода сообщений, в котором он записывает весь вывод в файл, и в подробном виде, в котором он не только записывает в файл, но и печатает все на терминал. Но в обоих режимах скрипт хочет взаимодействовать с пользователем, то есть печатать на терминал и читать ответ пользователя. Поэтому я подумал, что сохранение /dev/stdoutрешит проблему с печатью сообщений в режиме без вывода сообщений. Альтернативой является перенаправление любого другого действия, которое производит выходные данные, и их довольно много. Примерно в 100 раз больше сообщений взаимодействия с пользователем.
alexey.e.egorov
Стандартный способ взаимодействия с пользователем - это печать на stderr. Это, например, почему приглашения собираются stderrпо умолчанию.
Кусалананда
К сожалению, stderrони также должны быть перенаправлены и сохранены, так как скрипт вызывает несколько внешних программ, и все возможные сообщения об ошибках должны собираться и регистрироваться.
alexey.e.egorov

Ответы:

14

Чтобы сохранить дескриптор файла, вы дублируете его на другом диске. Сохранение пути к соответствующему файлу недостаточно, вам нужно сохранить режим открытия, флаги открытия, текущую позицию в файле и так далее. И, конечно же, для анонимных каналов или сокетов это не сработает, поскольку у них нет пути. Вы хотите сохранить описание открытого файла , на которое ссылается fd, а дублирование fd фактически возвращает новый fd к тому же описанию открытого файла .

Чтобы дублировать файловый дескриптор на другой с помощью Bourne-подобной оболочки, синтаксис:

exec 3>&1

Выше fd 1 дублируется на fd 3.

То, что fd 3 уже было открыто до того, будет закрыто, но учтите, что fds 3–9 (обычно больше, до 99 с yash) зарезервированы для этой цели (и не имеют специального значения, противоположного 0, 1 или 2), Оболочка знает, чтобы не использовать их для собственного внутреннего бизнеса. Единственная причина, по которой fd 3 был бы открыт заранее - это то, что вы сделали это в сценарии 1 или он был пропущен вызывающей стороной.

Затем вы можете изменить стандартный вывод на другое:

exec > /dev/null

И позже, чтобы восстановить стандартный вывод:

exec >&3 3>&-

( 3>&-чтобы закрыть дескриптор файла, который нам больше не нужен).

Теперь проблема в том, что кроме ksh, каждая команда, которую вы запускаете после этого exec 3>&1, наследует этот fd 3. Это утечка fd. Как правило, не имеет большого значения, но это может вызвать проблемы.

kshустанавливает флаг close-on-exec для этих fds (для fds более 2), но никакие другие оболочки и другие оболочки не имеют никакого способа установить этот флаг вручную.

Обходной путь для другой оболочки - закрыть fd 3 для каждой команды, например:

exec 3>&-

exec > file.log

ls 3>&-
uname 3>&-

exec >&3 3>&-

Громоздкие. Здесь лучшим способом было бы вообще не использовать exec, а перенаправлять группы команд:

{
  ls
  uname
} > file.log

Там это оболочка, которая заботится о сохранении стандартного вывода и его последующем восстановлении (и делает это внутренне, дублируя его на fd (выше 9, выше 99 для yash) с установленным флагом close-on-exec ).

Примечание 1

Теперь управление этими fds с 3 по 9 может быть громоздким и проблематичным, если вы используете их широко или в функциях, особенно если ваш скрипт использует какой-то сторонний код, который, в свою очередь, может использовать эти fds.

Некоторые оболочки ( zsh, bash, ksh93, все добавили функцию ( предложенный Oliver Киддл изzsh ) примерно в то же время в 2005 году после того, как он был обсужден среди их разработчиков) имеют альтернативный синтаксис , чтобы назначить первый свободный FD выше 10 вместо , который помогает в этом случае:

myfunction() {
  local fd
  exec {fd}>&1
  # stdout was duplicated onto a new fd above 10, whose actual value
  # is stored in the fd variable
  ...
  # it should even be safe to re-enter the function here
  ...
  exec >&"$fd" {fd}>&-
}
Стефан Шазелас
источник
Кроме того, ваш код неверен в том смысле, что fd 3 может быть уже занят, как это происходит, когда скрипт запускается из rc.localслужбы, например, так что вы действительно должны использовать что-то подобное exec {FD}>&1или что- то подобное . Но это поддерживается только в bash 4, что очень печально. Так что это не совсем переносимо.
alexey.e.egorov
@ alexey.e.egorov, см. редактировать.
Стефан Шазелас
Bash 3. * не поддерживает эту функцию, и эта версия используется в Centos 5, которая все еще поддерживается и все еще используется. И поиск свободного дескриптора, а затем eval "exec $i>&1"- это то, чего я хотел бы избежать из-за громоздкости. Могу ли я на самом деле полагаться на что FDS выше 9 будет нахаляву?
alexey.e.egorov
@ alexey.e.egorov, нет, ты смотришь на это задом наперед. Файлы с 3 по 9 бесплатны (и вы сами можете управлять ими по своему усмотрению) и предназначены для этой цели. fds выше 9 может использоваться оболочкой внутри, и закрытие их может иметь неприятные последствия. Большинство снарядов не позволят вам их использовать. bashпозволит вам выстрелить себе в ногу.
Стефан Шазелас
2
@ alexey.e.egorov, если при запуске у вашего скрипта есть некоторые открытые fds в (3..9), это потому, что ваш вызывающий пользователь забыл закрыть их или установить для них флаг close-on-exec. Это то, что я называю утечкой ФД. Теперь, возможно, вызывающая сторона намеревалась передать вам эти fds, чтобы вы могли читать и / или записывать данные из них в них, но тогда вы узнаете об этом. Если вы не знаете о них, то вам все равно, тогда вы можете свободно их закрывать (обратите внимание, что он просто закрывает процесс fd вашего скрипта, а не вашего вызывающего).
Стефан Шазелас
3

Как видите, скрипты bash не похожи на обычный язык программирования, где вы можете назначать файловые дескрипторы.

Самое простое решение - использовать вспомогательную оболочку для запуска того, что вы хотите перенаправить, чтобы обработка могла быть возвращена к верхней оболочке, которая имеет стандартный ввод / вывод без изменений.

Альтернативным решением будет использование ttyидентификации устройства TTY и управление вводом / выводом в вашем сценарии. Например:

dev=$(tty)

и тогда ты сможешь ..

echo message > $dev
Джули Пеллетье
источник
> Альтернативным решением будет использование tty для идентификации устройства TTY и управления вводом / выводом в вашем сценарии. Как это сделать?
alexey.e.egorov
1
Я просто включил пример в свой ответ.
Джули Пеллетье
1

$$ получит PID текущего процесса, в случае интерактивной оболочки или скрипта соответствующий PID оболочки.

Таким образом, вы можете использовать:

readlink -f /proc/$$/fd/1

Пример:

% readlink -f /proc/$$/fd/1
/dev/pts/33

% var=$(readlink -f /proc/$$/fd/1)

% echo $var                       
/dev/pts/33
heemayl
источник
1
Несмотря на то, что это функционально, использование определенной /procструктуры вызывает проблемы переносимости, как и использование, /dev/stdoutкак упомянуто в вопросе.
Джули Пеллетье
1
@JuliePelletier Опираясь на указанную /procструктуру? Это будет работать на любом Linux, который имеет procfs..
Heemayl
1
Правильно, поэтому мы можем обобщить для Linux, как procfsэто почти всегда присутствует, но мы часто видим вопросы переносимости, и хорошая методология разработки включает рассмотрение переносимости на другие системы. bashможет работать на множестве операционных систем.
Джули Пеллетье