Dans le code «{exec> / dev / null; }> / dev / null »que se passe-t-il sous le capot?

15

Lorsque vous redirigez une liste de commandes contenant une redirection exec, l'exec> / dev / null ne semble pas être appliqué par la suite, comme avec:

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

"Salut" est imprimé.

J'avais l'impression que la {}liste de commandes n'est pas considérée comme un sous-shell à moins qu'elle ne fasse partie d'un pipeline, donc elle exec >/dev/nulldevrait toujours être appliquée dans l'environnement shell actuel dans mon esprit.

Maintenant, si vous le changez en:

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

il n'y a pas de sortie comme prévu; le descripteur de fichier 1 reste également pointé sur / dev / null pour les futures commandes. Cela est illustré par la réexécution:

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

qui ne donnera aucune sortie.

J'ai essayé de créer un script et de le structurer, mais je ne sais toujours pas exactement ce qui se passe ici.

À chaque étape de ce script, qu'arrive-t-il au descripteur de fichier STDOUT?

EDIT: Ajout de ma sortie 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
la source
C'est étrange; Je ne peux pas reproduire le close(10). Pouvez-vous également publier l'intégralité du contenu de votre script sur lequel vous avez exécuté Strace?
DepressedDaniel
@DepressedDaniel Voici le script complet et la strace: script strace
Joey Pabalinas
Vous avez un parasite ;après }, ce qui change le sens de > /dev/nullne pas s'appliquer à la liste composée {}après tout.
DepressedDaniel
@DepressedDaniel Ah, vous avez tout à fait raison! Maintenant, la sortie est ce que j'attends; Merci pour vos réponses!
Joey Pabalinas

Réponses:

17

Suivons

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

pas à pas.

  1. Il existe deux commandes:

    une. { exec >/dev/null; } >/dev/null, suivi par

    b. echo "Hi"

    Le shell exécute d'abord la commande (a) puis la commande (b).

  2. L'exécution du { exec >/dev/null; } >/dev/nullproduit se déroule comme suit:

    une. Tout d'abord, le shell effectue la redirection >/dev/null et se souvient de l'annuler à la fin de la commande .

    b. Ensuite, le shell s'exécute { exec >/dev/null; }.

    c. Enfin, le shell ramène la sortie standard à son emplacement d'origine. (Il s'agit du même mécanisme que dans ls -lR /usr/share/fonts >~/FontList.txt- les redirections sont effectuées uniquement pour la durée de la commande à laquelle elles appartiennent.)

  3. Une fois la première commande effectuée, le shell s'exécute echo "Hi". La sortie standard est là où elle était avant la première commande.

AlexP
la source
Y a-t-il une raison pour laquelle 2a est exécuté avant 2b? (de droite à gauche)
Joey Pabalinas
5
Les redirections doivent être exécutées avant la commande à laquelle elles s'appliquent, non? Comment pourraient-ils fonctionner autrement?
AlexP
Aha, je n'y ai jamais pensé de cette façon! Les deux premières sont de bonnes réponses; en donnant un peu avant de me décider, mais j'apprécie les deux explications!
Joey Pabalinas
Malheureusement, je ne peux choisir qu'une seule réponse, donc je vais avec celle-ci car elle est un peu moins technique et donc je pense qu'elle pourrait aider même les utilisateurs les moins avertis. Cependant, @DepressedDaniel a eu une réponse tout aussi excellente ici qui offre une explication plus approfondie.
Joey Pabalinas
14

Afin de ne pas utiliser de sous-shell ou de sous-processus, lorsque la sortie d'une liste composée {}est canalisée >, le shell enregistre le descripteur STDOUT avant d'exécuter la liste composée et le restaure après. Ainsi, exec >dans la liste composée ne porte pas son effet au-delà du point où l'ancien descripteur est rétabli en tant que STDOUT.

Jetons un coup d'œil à la partie pertinente de 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

Vous pouvez voir comment, à la ligne 134, descriptor 1( STDOUT) est copié sur un autre descripteur avec index au moins 10(c'est ce qui F_DUPFDfait; il retourne le descripteur disponible le plus bas en commençant au nombre donné après la duplication sur ce descripteur). Voyez également comment, à la ligne 137, le résultat de open("/dev/null")(descriptor 3) est copié dans descriptor 1( STDOUT). Enfin, en ligne 147, l'ancien STDOUTdescripteur enregistré sur 10est recopié sur descriptor 1( STDOUT). L'effet net est d'isoler le changement en STDOUTligne 144(ce qui correspond à l'intérieur exec >/dev/null).

DépriméDaniel
la source
Puisque FD 1 est écrasé par FD 3 à la ligne 137, pourquoi la ligne 141 ne pointe-t-elle pas 10 vers / dev / null?
Joey Pabalinas
@JoeyPabalinas Line 141 duplique FD 1 (c'est-à-dire stdout) vers le prochain descripteur disponible après 10 , qui se révèle être 11, comme vous pouvez le voir dans la valeur de retour de cet appel système. 10 est simplement codé en dur dans bash afin que la sauvegarde des descripteurs de bash n'interfère pas avec les descripteurs à un chiffre que vous pourriez manipuler dans votre script exec.
DepressedDaniel
Donc, fcntl (1, F_DUPFD, 10) fera toujours référence à STDOUT, peu importe où FD 1 pointe actuellement?
Joey Pabalinas
@JoeyPabalinas Je ne sais pas quelle est votre question. FD 1 EST STDOUT. Ce sont les mêmes choses.
DepressedDaniel
Ajout d'une sortie strace complète à mon message d'origine.
Joey Pabalinas
8

La différence entre { exec >/dev/null; } >/dev/null; echo "Hi"et { exec >/dev/null; }; echo "Hi"est que la double redirection fait dup2(10, 1);avant de fermer fd 10 qui est la copie de l'original stdout, avant d'exécuter la commande suivante ( echo).

Cela se produit de cette façon car la redirection externe recouvre en fait la redirection interne. C'est pourquoi il copie le stdoutfd original une fois terminé.

Julie Pelletier
la source
+1 pour expliquer la différence de manière simple. La réponse d'AlexP manque de cette explication.
Kamil Maciorowski