Jak zapisać docelową lokalizację / dev / stdout w skrypcie bash?

12

Mam pewien skrypt bash, który chce zachować oryginalną /dev/stdoutlokalizację przed zamianą 1. deskryptora pliku na inną lokalizację.

Oczywiście napisałem coś takiego

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

I to nie zadziałało. Bardzo szybko rozumiem na czym polegał problem:

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

Oczywiście $()działa w podpowłoce, która jest potokowana do powłoki nadrzędnej.

Pytanie brzmi: czy istnieje niezawodny (w zakresie przenośności między dystrybucjami Linuksa) sposób na zapisanie /dev/stdoutlokalizacji jako łańcucha w skrypcie bash?

alexey.e.egorov
źródło
Brzmi to trochę jak problem XY . Jaki jest podstawowy problem?
Kusalananda
Podstawowym problemem jest pewien skrypt instalacyjny, który działa w dwóch trybach - cichym, w którym rejestruje wszystkie dane wyjściowe do pliku i szczegółowe, w którym nie tylko loguje się do pliku, ale także drukuje wszystko do terminala. Ale w obu trybach skrypt chce wchodzić w interakcje z użytkownikiem, tj. Drukować do terminala i czytać odpowiedź użytkownika. Pomyślałem więc, że oszczędzanie /dev/stdoutrozwiąże problem z drukowaniem wiadomości w trybie cichym. Alternatywą jest przekierowywanie każdej innej akcji, która generuje dane wyjściowe, i jest ich całkiem sporo. Około 100 razy więcej niż komunikaty interakcji użytkownika.
alexey.e.egorov
Standardowym sposobem interakcji z użytkownikiem jest drukowanie stderr. Właśnie dlatego stderrdomyślnie wyświetlane są monity .
Kusalananda
Niestety stderrnależy również przekierować i zapisać, ponieważ skrypt wywołuje wiele programów zewnętrznych, a wszystkie możliwe komunikaty o błędach należy zebrać i zarejestrować.
alexey.e.egorov

Odpowiedzi:

14

Aby zapisać deskryptor pliku, powiel go na innym dysku. Zapisanie ścieżki do odpowiedniego pliku nie wystarczy, trzeba zapisać tryb otwierania, flagi otwarcia, bieżącą pozycję w pliku i tak dalej. I oczywiście dla anonimowych rur lub gniazd nie działałoby, ponieważ nie mają one ścieżki. To, co chcesz zapisać, to otwarty opis pliku , do którego odnosi się fd, a powielenie fd faktycznie zwraca nowy fd do tego samego opisu otwartego pliku .

Aby powielić deskryptor pliku na innym, w powłoce Bourne'a, składnia jest następująca:

exec 3>&1

Powyżej, fd 1 jest duplikowane na fd 3.

Cokolwiek fd 3 było już otwarte wcześniej, zostanie zamknięte, ale należy pamiętać, że fds 3 do 9 (zwykle więcej, do 99 z yash) są zarezerwowane do tego celu (i nie mają specjalnego znaczenia sprzecznego z 0, 1 lub 2), Shell wie, że nie będzie ich używać do własnych celów wewnętrznych. Jedynym powodem, dla którego fd 3 byłby wcześniej otwarty, było to, że zrobiłeś to w skrypcie 1 lub wyciekł z niego dzwoniący.

Następnie możesz zmienić stdout na coś innego:

exec > /dev/null

A później, aby przywrócić standardowe wyjście:

exec >&3 3>&-

( 3>&-zamykanie deskryptora pliku, którego już nie potrzebujemy).

Problem polega na tym, że z wyjątkiem ksh każde kolejne polecenie exec 3>&1odziedziczy fd 3. To przeciek fd. Zasadniczo nie jest to wielka sprawa, ale może to powodować problemy.

kshustawia flagę close-on-exec na tych fds (dla fds powyżej 2), ale nie inne powłoki i inne powłoki nie mają możliwości ręcznego ustawienia tej flagi.

Obejściem drugiej powłoki jest zamknięcie fd 3 dla każdego polecenia, na przykład:

exec 3>&-

exec > file.log

ls 3>&-
uname 3>&-

exec >&3 3>&-

Nieporęczny. Tutaj najlepszym sposobem byłoby w ogóle nie używać exec, ale przekierowywać grupy poleceń:

{
  ls
  uname
} > file.log

Tam jest to powłoka, która dba o zapisanie stdout i przywrócenie go później (i robi to wewnętrznie, powielając go na fd (powyżej 9, powyżej 99 dla yash) z ustawioną flagą close-on-exec ).

Uwaga 1

Teraz zarządzanie tymi fds 3 do 9 może być kłopotliwe i problematyczne, jeśli używasz ich intensywnie lub w funkcjach, szczególnie jeśli twój skrypt używa kodu innej firmy, który z kolei może z nich korzystać.

Niektóre muszle ( zsh, bash, ksh93, wszystkie dodane funkcji ( sugerowane przez Oliver Kiddle zzsh ) w tym samym czasie w 2005 roku po to zostało omówione między ich twórców) mają alternatywną składnię przypisywania pierwszego wolnego fd powyżej 10 zamiast co pomaga w tym przypadku:

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}>&-
}
Stéphane Chazelas
źródło
Ponadto, twój kod jest błędny w tym sensie, że fd 3 może być już zajęty, jak to się dzieje, gdy skrypt uruchamia się z rc.localusługi, np. Więc naprawdę powinieneś użyć czegoś takiego exec {FD}>&1lub czegoś takiego. Ale jest to obsługiwane tylko w bash 4, co jest naprawdę smutne. To nie jest tak naprawdę przenośne.
alexey.e.egorov
@ alexey.e.egorov, patrz edycja.
Stéphane Chazelas,
Bash 3. * nie obsługuje tej funkcji, a ta wersja jest używana w Centos 5, który jest nadal obsługiwany i nadal używany. I znajdowanie wolnego deskryptora, a następnie eval "exec $i>&1"jest rzeczą, której chciałbym uniknąć, ze względu na nieporęczność. Czy naprawdę mogę polegać na tym, że fds powyżej 9 byłby wtedy darmowy?
alexey.e.egorov
@ alexey.e.egorov, nie, patrzysz na to wstecz. fds od 3 do 9 są bezpłatne (i możesz nimi zarządzać według własnego uznania) i są przeznaczone do tego celu. fds powyżej 9 może być używane wewnętrznie przez powłokę, a ich zamknięcie może mieć paskudne konsekwencje. Większość pocisków nie pozwala ci ich używać. bashpozwoli ci strzelić sobie w stopę.
Stéphane Chazelas
2
@ alexey.e.egorov, jeśli na starcie skrypt ma otwarte fds w (3..9), to dlatego, że dzwoniący zapomniał je zamknąć lub ustawić na nich flagę close-on-exec. Tak nazywam wyciek FD. Być może dzwoniący zamierzał przekazać ci te fds, abyś mógł czytać i / lub zapisywać dane z / do nich, ale wtedy będziesz o tym wiedział. Jeśli nie wiesz o nich, to cię to nie obchodzi, możesz je dowolnie zamykać (pamiętaj, że to po prostu zamyka proces skryptu fd, a nie proces wywołujący).
Stéphane Chazelas
3

Jak widać, skrypty bash nie są jak zwykły język programowania, w którym można przypisywać deskryptory plików.

Najprostszym rozwiązaniem jest użycie podpowłoki do uruchomienia tego, co chcesz przekierować, aby przetwarzanie mogło zostać przywrócone do najwyższej powłoki, która ma nienaruszone standardowe I / O.

Alternatywnym rozwiązaniem byłoby ttyzidentyfikowanie urządzenia TTY i kontrolowanie operacji we / wy w skrypcie. Na przykład:

dev=$(tty)

i wtedy możesz ...

echo message > $dev
Julie Pelletier
źródło
> Alternatywnym rozwiązaniem byłoby użycie tty do identyfikacji urządzenia TTY i kontrolowania I / O w twoim skrypcie. Jak to się robi?
alexey.e.egorov
1
W odpowiedzi podałem tylko przykład.
Julie Pelletier
1

$$ dostarczy ci bieżący PID procesu, w przypadku powłoki interaktywnej lub skryptu odpowiedni PID powłoki.

Możesz więc użyć:

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

Przykład:

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

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

% echo $var                       
/dev/pts/33
heemayl
źródło
1
Chociaż jest funkcjonalny, poleganie na określonej /procstrukturze powoduje problemy z przenośnością, podobnie jak używanie, /dev/stdoutjak wspomniano w pytaniu.
Julie Pelletier
1
@JuliePelletier Opierasz się na określonej /procstrukturze? Działa na każdym Linuksie, który ma procfs...
heemayl
1
Racja, więc możemy uogólnić na Linuksa, ponieważ procfsprawie zawsze jest obecny, ale często widzimy pytania dotyczące przenośności, a dobra metodologia programowania obejmuje rozważenie możliwości przenoszenia na inne systemy. bashmoże działać na wielu systemach operacyjnych.
Julie Pelletier