W kodzie „{exec> / dev / null; }> / dev / null ”co się dzieje pod maską?

15

Gdy przekierowujesz listę poleceń zawierającą przekierowanie exec, exec> / dev / null nie wydaje się być później stosowane, na przykład z:

{ exec >/dev/null; } >/dev/null; echo "Hi"

Drukowane jest „Cześć”.

Miałem wrażenie, że {}lista poleceń nie jest uważana za podpowłokę, chyba że jest ona częścią potoku, więc exec >/dev/nullpowinna być nadal stosowana w bieżącym środowisku powłoki.

Teraz, jeśli zmienisz to na:

{ exec >/dev/null; } 2>/dev/null; echo "Hi"

wyniki nie są zgodne z oczekiwaniami; deskryptor pliku 1 pozostaje wskazany na / dev / null również dla przyszłych poleceń. Pokazuje to ponowne uruchomienie:

{ exec >/dev/null; } >/dev/null; echo "Hi"

co nie da wyniku.

Próbowałem napisać scenariusz i wyszukać go, ale wciąż nie jestem pewien, co dokładnie się tutaj dzieje.

Co dzieje się z deskryptorem pliku STDOUT w każdym punkcie tego skryptu?

EDYCJA: Dodanie mojego wyjścia strace:

read(255, "#!/usr/bin/env bash\n{ exec 1>/de"..., 65) = 65
open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
fcntl(1, F_GETFD)                       = 0
fcntl(1, F_DUPFD, 10)                   = 10
fcntl(1, F_GETFD)                       = 0
fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
dup2(3, 1)                              = 1
close(3)                                = 0
close(10)                               = 0
open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
fcntl(1, F_GETFD)                       = 0
fcntl(1, F_DUPFD, 10)                   = 10
fcntl(1, F_GETFD)                       = 0
fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
dup2(3, 1)                              = 1
close(3)                                = 0
dup2(10, 1)                             = 1
fcntl(10, F_GETFD)                      = 0x1 (flags FD_CLOEXEC)
close(10)                               = 0
fstat(1, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0
ioctl(1, TCGETS, 0x7ffee027ef90)        = -1 ENOTTY (Inappropriate ioctl for device)
write(1, "hi\n", 3)                     = 3
Joey Pabalinas
źródło
To jest dziwne; Nie mogę odtworzyć close(10). Czy możesz również opublikować całą zawartość skryptu, na którym uruchomiłeś strace?
DepressedDaniel
@DepressedDaniel Oto pełny skrypt i strace: skrypt strace
Joey Pabalinas
Błąkasz się ;po }, co zmienia znaczenie, > /dev/nullby nie stosować się do listy złożonej {}.
DepressedDaniel,
@DepressedDaniel Ah, masz całkowitą rację! Teraz wynik jest tym, czego oczekuję; Dziękuję Ci za Twoje odpowiedzi!
Joey Pabalinas,

Odpowiedzi:

17

Podążajmy

{ exec >/dev/null; } >/dev/null; echo "Hi"

krok po kroku.

  1. Istnieją dwa polecenia:

    za. { exec >/dev/null; } >/dev/null, śledzony przez

    b. echo "Hi"

    Powłoka wykonuje najpierw polecenie (a), a następnie polecenie (b).

  2. Wykonanie { exec >/dev/null; } >/dev/nullprzebiega następująco:

    za. Po pierwsze, powłoka wykonuje przekierowanie >/dev/null i pamięta o cofnięciu go po zakończeniu polecenia .

    b. Następnie wykonywana jest powłoka { exec >/dev/null; }.

    do. Na koniec powłoka przełącza standardowe wyjście z powrotem tam, gdzie było. (Jest to ten sam mechanizm, co ls -lR /usr/share/fonts >~/FontList.txtprzekierowania wewnętrzne - dokonywane są tylko na czas trwania polecenia, do którego należą).

  3. Po wykonaniu pierwszego polecenia powłoka jest wykonywana echo "Hi". Standardowe wyjście jest tam, gdzie było przed pierwszym poleceniem.

AlexP
źródło
Czy jest jakiś powód, dla którego 2a jest wykonywana przed 2b? (od prawej do lewej)
Joey Pabalinas,
5
Przekierowania należy wykonać przed poleceniem, którego dotyczą, prawda? Jak mogliby działać inaczej?
AlexP
Aha, nigdy nie myślałem o tym w ten sposób! Pierwsze dwa to wspaniałe odpowiedzi; dając trochę, zanim zdecyduję się na jedną, ale doceniam oba wyjaśnienia!
Joey Pabalinas,
Niestety mogę wybrać tylko jedną odpowiedź, więc wybieram tę, ponieważ jest ona nieco mniej techniczna i dlatego myślę, że byłaby w stanie pomóc nawet mniej zaawansowanym użytkownikom. Jednak @DepressedDaniel miał tutaj równie świetną odpowiedź , która oferuje bardziej szczegółowe wyjaśnienia.
Joey Pabalinas,
14

Aby nie używać podpowłoki lub podprocesu, gdy dane wyjściowe z listy złożonej {}są przesyłane potokowo >, powłoka zapisuje deskryptor STDOUT przed uruchomieniem listy złożonej i przywraca ją później. Zatem na exec >liście złożonej nie przenosi efektu poza punkt, w którym stary deskryptor jest przywracany jako STDOUT.

Rzućmy okiem na odpowiednią część strace bash -c '{ exec >/dev/null; } >/dev/null; echo hi' 2>&1 | cat -n:

   132  open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
   133  fcntl(1, F_GETFD)                       = 0
   134  fcntl(1, F_DUPFD, 10)                   = 10
   135  fcntl(1, F_GETFD)                       = 0
   136  fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
   137  dup2(3, 1)                              = 1
   138  close(3)                                = 0
   139  open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
   140  fcntl(1, F_GETFD)                       = 0
   141  fcntl(1, F_DUPFD, 10)                   = 11
   142  fcntl(1, F_GETFD)                       = 0
   143  fcntl(11, F_SETFD, FD_CLOEXEC)          = 0
   144  dup2(3, 1)                              = 1
   145  close(3)                                = 0
   146  close(11)                               = 0
   147  dup2(10, 1)                             = 1
   148  fcntl(10, F_GETFD)                      = 0x1 (flags FD_CLOEXEC)
   149  close(10)                               = 0

Możesz zobaczyć, jak w linii 134 deskryptor 1( STDOUT) jest kopiowany na inny deskryptor z co najmniej indeksem 10(to właśnie F_DUPFDrobi; zwraca najniższy dostępny deskryptor zaczynając od podanej liczby po powieleniu na tym deskryptorze). Zobacz także, jak w linii 137 wynik open("/dev/null")(deskryptor 3) jest kopiowany do descriptor 1( STDOUT). Wreszcie, on-line 147stary STDOUTzapisany w deskryptorze 10jest kopiowany z powrotem do descriptor 1( STDOUT). Efektem netto jest izolacja zmiany STDOUTna linię 144(która odpowiada wewnętrznej exec >/dev/null).

DepressedDaniel
źródło
Skoro FD 1 jest zastępowane przez FD 3 w linii 137, dlaczego linia 141 nie wskazuje 10 na / dev / null?
Joey Pabalinas,
@JoeyPabalinas Linia 141 powiela FD 1 (tj. Stdout) do następnego dostępnego deskryptora po 10 , który okazuje się 11, jak widać w wartości zwracanej z tego wywołania systemowego. 10 jest po prostu zakodowane na bash, aby zapis deskryptora bash nie kolidował z jednocyfrowymi deskryptorami, którymi można manipulować w skrypcie exec.
DepressedDaniel,
Zatem fcntl (1, F_DUPFD, 10) zawsze będzie odnosił się do STDOUT bez względu na to, gdzie obecnie wskazuje FD 1?
Joey Pabalinas,
@JoeyPabalinas Nie wiesz, jakie masz pytanie. FD 1 JEST STDOUT. To są te same rzeczy.
DepressedDaniel,
Dodano pełne wyjście strace do mojego oryginalnego postu.
Joey Pabalinas,
8

Różnica między { exec >/dev/null; } >/dev/null; echo "Hi"i { exec >/dev/null; }; echo "Hi"polega na tym, że podwójne przekierowanie robi dup2(10, 1);przed zamknięciem fd 10, który jest kopią oryginałustdout , przed uruchomieniem następnego polecenia ( echo).

Dzieje się tak, ponieważ przekierowanie zewnętrzne faktycznie nakłada się na przekierowanie wewnętrzne. Dlatego kopiuje oryginalny stdoutFD po zakończeniu.

Julie Pelletier
źródło
+1 za łatwe wyjaśnienie różnicy. Odpowiedź AlexP nie zawiera tego wyjaśnienia.
Kamil Maciorowski