Як зберегти / dev / stdout цільове місце розташування у скрипті bash?


12

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

Тож, природно, я написав щось подібне

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

Навчально, $()працює в нижній частині корпусу, який прокладений до батьківської оболонки.

Отже, питання: чи існує надійний спосіб (збереження портативності між дистрибутивами Linux), щоб зберегти /dev/stdoutмісцеположення у вигляді рядка в bash-скрипті?


Це звучить трохи як проблема XY . Що є основним питанням?
Кусалаланда

Основна проблема - це певний сценарій встановлення, який працює в двох режимах - безшумному, в якому він реєструє весь вихід у файл та багатослівний текст, в якому він не тільки записує файли, але й друкує все до терміналу. Але в обох режимах сценарій хоче взаємодіяти з користувачем, тобто друкувати на термінал та читати відповідь користувача. Тому я думав, що збереження /dev/stdoutвирішить проблему з друком повідомлень у беззвучному режимі. Альтернативою є перенаправлення будь-якої іншої дії, яка дає результат, і їх існує досить багато. Приблизно в 100 разів більше, ніж повідомлення про взаємодію користувачів.
alexey.e.egorov

Стандартним способом взаємодії з користувачем є друк на stderr. Це, наприклад, чому підказки збираються stderrза замовчуванням.
Kusalananda

На жаль, stderrнеобхідно також переадресувати та зберегти, оскільки скрипт викликає низку зовнішніх програм, а всі можливі повідомлення про помилки мають бути зібрані та записані.
alexey.e.egorov

Відповіді:


14

Щоб зберегти дескриптор файлу, ви дублюєте його на іншому fd. Збереження шляху до відповідного файлу недостатньо, вам потрібно буде зберегти режим відкриття, прапорці відкриття, поточне положення у файлі тощо. І звичайно, для анонімних труб або розеток це не спрацює, оскільки до них немає шляху. Те, що ви хочете зберегти, - це відкритий опис файлу , на який посилається fd, і копіювання fd насправді повертає новий fd до того ж опису відкритого файлу .

Для дублювання дескриптора файлу на інший, з оболонкою Борна, синтаксис:

exec 3>&1

Вище fd 1 дублюється на fd 3.

Що б fd 3 вже не було відкрито, було б закрито, але зауважте, що FDS 3 до 9 (зазвичай більше, до 99 с yash) зарезервовані для цієї мети (і не мають особливого значення всупереч 0, 1 або 2), shell знає, що не використовує їх для власного внутрішнього бізнесу. Єдина причина, що fd 3 була б відкрита заздалегідь, це те, що ви зробили це в сценарії 1 , або його витік абонент.

Потім ви можете змінити stdout на щось інше:

exec > /dev/null

І пізніше, щоб відновити stdout:

exec >&3 3>&-

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

Тепер проблема полягає в тому, що за винятком ksh, кожна команда, яку ви запускаєте після цього exec 3>&1, успадковуватиме цей fd 3. Це fd leak. Зазвичай це не велика справа, але це може спричинити проблеми.

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

Там саме оболонка дбає про збереження stdout та відновлення його після цього (і це робиться всередині, дублюючи його на fd (вище 9, вище 99 для yash) із встановленим прапором close-on-exec ).

Примітка 1

Тепер управління цими FDS 3 на 9 може бути громіздким і проблематичним, якщо ви використовуєте їх широко або у функціях, особливо якщо ваш сценарій використовує якийсь сторонній код, який, в свою чергу, може використовувати ці файли.

Деякі оболонки ( 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чи щось. Але це підтримується лише у басі 4, що справді сумно. Тож це насправді не портативно.
alexey.e.egorov

@ alexey.e.egorov, див. редагувати.
Стефан Шазелас

Bash 3. * не підтримує цю функцію, і ця версія використовується в Centos 5, який все ще підтримується та використовується. І знайти вільний дескриптор, а потім eval "exec $i>&1"- річ, яку я хотів би уникнути, через це громіздкість. Чи можу я реально розраховувати на те, що FDS вище 9 буде тоді безкоштовним?
alexey.e.egorov

@ alexey.e.egorov, ні, ви дивитесь на це назад. FDS від 3 до 9 можуть користуватися безкоштовно (і вам належить керувати ними, як хочете) і призначені для цього. fds вище 9 може використовуватися оболонкою внутрішньо, і закриття їх може спричинити неприємні наслідки. Більшість оболонок не дозволять вам їх використовувати. bashдозволить вам стріляти собі в ногу.
Стефан Шазелас

2
@ alexey.e.egorov, якщо після запуску сценарію відкрито кілька fds (3..9), це тому, що ваш абонент забув їх закрити або встановив на них прапор close-on-exec. Ось що я називаю fd leak. Тепер, можливо, абонент мав намір передати вам ці файли, щоб ви могли прочитати та / або записати дані з / до них, але тоді ви дізнаєтесь про це. Якщо ви не знаєте про них, то вам не байдуже, тоді ви можете їх вільно закрити (зауважте, що це просто закриває процес вашого сценарію 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

1
Хоча це функціонально, покладання на певну /procструктуру спричиняє проблеми переносимості, як і використання, /dev/stdoutяк зазначено в питанні.
Джулі Пелтьє

1
@JuliePelletier Покладаючись на специфічну /procструктуру? Він буде працювати на будь-якому Linux, який має procfs..
heemayl

1
Так, ми можемо узагальнити для Linux, як procfsце майже завжди, але ми часто бачимо питання переносимості, і хороша методологія розробки включає врахування переносимості в інші системи. bashможе працювати на безлічі операційних систем.
Джулі Пелтьє
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.