Comment enregistrer l'emplacement cible / dev / stdout dans un script bash?

12

J'ai un certain script bash, qui veut conserver l' /dev/stdoutemplacement d' origine avant de remplacer le 1er descripteur de fichier par un autre emplacement.

Alors, naturellement, j'ai écrit quelque chose comme

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

Et ça n'a pas marché. Très vite, je comprends quel était le problème:

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

Évidemment, $()s'exécute dans un sous-shell, qui est canalisé vers le shell parent.

La question est donc: existe-t-il un moyen fiable (limité à la portabilité entre les distributions Linux) d'enregistrer l' /dev/stdoutemplacement en tant que chaîne dans un script bash?

alexey.e.egorov
la source
Cela ressemble un peu à un problème XY . Quel est le problème sous-jacent?
Kusalananda
Le problème sous-jacent est un certain script d'installation qui s'exécute en deux modes - silencieux, dans lequel il enregistre toutes les sorties dans un fichier et verbeux, dans lequel non seulement il se connecte au fichier, mais imprime également tout sur le terminal. Mais dans les deux modes, le script veut interagir avec l'utilisateur, c'est-à-dire imprimer sur le terminal et lire la réponse de l'utilisateur. J'ai donc pensé que l'enregistrement /dev/stdoutrésoudrait le problème d'impression des messages en mode silencieux. L'alternative consiste à rediriger toutes les autres actions qui produisent une sortie, et il y en a un certain nombre. Environ 100 fois plus que les messages d'interaction avec l'utilisateur.
alexey.e.egorov
La manière standard d'interagir avec l'utilisateur est d'imprimer sur stderr. C'est, par exemple, pourquoi les invites vont stderrpar défaut.
Kusalananda
Malheureusement, le stderrdoit également être réorienté et enregistré, car le script appelle un certain nombre de programmes externes et tous les messages d'erreur possibles doivent être collectés et enregistrés.
alexey.e.egorov

Réponses:

14

Pour enregistrer un descripteur de fichier, vous le dupliquez sur un autre fd. L'enregistrement d'un chemin d'accès au fichier correspondant ne suffit pas, vous devez enregistrer le mode d'ouverture, les drapeaux d'ouverture, la position actuelle dans le fichier, etc. Et bien sûr, pour les tuyaux ou les sockets anonymes, cela ne fonctionnerait pas car ceux-ci n'ont pas de chemin. Ce que vous voulez enregistrer, c'est la description du fichier ouvert à laquelle le fd fait référence, et la duplication d'un fd renvoie en fait un nouveau fd à la même description de fichier ouvert .

Pour dupliquer un descripteur de fichier sur un autre, avec un shell de type Bourne, la syntaxe est:

exec 3>&1

Ci-dessus, fd 1 est dupliqué sur fd 3.

Quel que soit le fd 3 qui était déjà ouvert auparavant, il sera fermé, mais notez que les fds 3 à 9 (généralement plus, jusqu'à 99 avec yash) sont réservés à cette fin (et n'ont aucune signification particulière contrairement à 0, 1 ou 2), le shell sait ne pas les utiliser pour ses propres affaires internes. La seule raison pour laquelle fd 3 aurait été ouvert au préalable est que vous l'avez fait dans le script 1 ou qu'il a été divulgué par l'appelant.

Ensuite, vous pouvez remplacer stdout par autre chose:

exec > /dev/null

Et plus tard, pour restaurer stdout:

exec >&3 3>&-

( 3>&-étant de fermer le descripteur de fichier dont nous n'avons plus besoin).

Maintenant, le problème avec cela est que, sauf dans ksh, chaque commande que vous exécutez après exec 3>&1héritera de ce fd 3. C'est une fuite de fd. En général, ce n'est pas grave, mais cela peut poser problème.

kshdéfinit l' indicateur close-on-exec sur ces fds (pour les fds de plus de 2), mais les autres shells et autres shells n'ont aucun moyen de définir ce flag manuellement.

Le travail autour d'un autre shell consiste à fermer le fd 3 pour chaque commande, comme:

exec 3>&-

exec > file.log

ls 3>&-
uname 3>&-

exec >&3 3>&-

Lourd. Ici, la meilleure façon serait de ne pas utiliser execdu tout, mais de rediriger les groupes de commandes:

{
  ls
  uname
} > file.log

Là, c'est le shell qui prend soin de sauvegarder stdout et de le restaurer ensuite (et il le fait en interne en le dupliquant sur un fd (au-dessus de 9, au-dessus de 99 pour yash) avec l' indicateur close-on-exec défini).

Remarque 1

Désormais, la gestion de ces fds 3 à 9 peut être lourde et problématique si vous les utilisez largement ou dans des fonctions, en particulier si votre script utilise du code tiers qui peut à son tour utiliser ces fds.

Quelques coquilles ( zsh, bash, ksh93, tous ajouté la fonction ( suggérée par Oliver Kiddle dezsh ) dans le même temps en 2005 après avoir été discuté parmi leurs développeurs) ont une syntaxe alternative pour attribuer le premier fd libre au- dessus de 10 au lieu qui aide dans ce cas:

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
la source
De plus, votre code est erroné dans le sens où fd 3 pourrait déjà être pris, comme cela arrive quand un script est exécuté à partir d'un rc.localservice, par exemple, donc vous devriez vraiment avoir utilisé quelque chose comme exec {FD}>&1ou quelque chose. Mais cela n'est pris en charge que dans bash 4, ce qui est vraiment triste. Ce n'est donc pas vraiment portable.
alexey.e.egorov
@ alexey.e.egorov, voir modifier.
Stéphane Chazelas
Bash 3. * ne prend pas en charge cette fonctionnalité, et cette version est utilisée dans Centos 5, qui est toujours pris en charge et toujours utilisé. Et trouver un descripteur gratuit, puis eval "exec $i>&1"c'est une chose que j'aimerais éviter, à cause de sa lourdeur. Puis-je vraiment compter sur le fait que les fds au-dessus de 9 seraient gratuits alors?
alexey.e.egorov
@ alexey.e.egorov, non, vous regardez en arrière. Les fds 3 à 9 sont gratuits (et c'est à vous de les gérer comme vous le souhaitez) et sont destinés à cet usage. les fds supérieurs à 9 peuvent être utilisés par le shell en interne et leur fermeture pourrait avoir des conséquences désagréables. La plupart des obus ne vous permettent pas de les utiliser. bashvous permettra de vous tirer une balle dans le pied.
Stéphane Chazelas
2
@ alexey.e.egorov, si au démarrage votre script a des fds dans (3..9) ouverts, c'est parce que votre appelant a oublié de les fermer ou de définir le drapeau close-on-exec sur eux. C'est ce que j'appelle une fuite fd. Maintenant, peut-être que l'appelant avait l'intention de vous transmettre ces fds, afin que vous puissiez lire et / ou écrire des données depuis / vers eux, mais alors vous le sauriez. Si vous ne les connaissez pas, alors peu vous importe, vous pouvez les fermer librement (notez que cela ferme simplement le processus fd de votre script, pas celui de votre appelant).
Stéphane Chazelas
3

Comme vous pouvez le voir, les scripts bash ne sont pas comme un langage de programmation ordinaire où vous pouvez attribuer des descripteurs de fichiers.

La solution la plus simple consiste à utiliser un sous-shell pour exécuter ce que vous voulez rediriger afin que le traitement puisse être rétabli vers le shell supérieur qui a ses E / S standard intactes.

Une autre solution consisterait ttyà identifier le périphérique TTY et à contrôler les E / S dans votre script. Par exemple:

dev=$(tty)

et puis vous pouvez ..

echo message > $dev
Julie Pelletier
la source
> Une autre solution serait d'utiliser tty pour identifier le périphérique TTY et contrôler les E / S dans votre script. Comment on fait ça?
alexey.e.egorov
1
Je viens d'inclure un exemple dans ma réponse.
Julie Pelletier
1

$$ vous obtiendrait le PID de processus actuel, en cas de shell interactif ou de script le PID de shell correspondant.

Vous pouvez donc utiliser:

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

Exemple:

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

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

% echo $var                       
/dev/pts/33
heemayl
la source
1
Bien qu'il soit fonctionnel, s'appuyer sur une /procstructure spécifique entraîne des problèmes de portabilité, tout comme l'utilisation /dev/stdoutcomme mentionné dans la question.
Julie Pelletier
1
@JuliePelletier Vous comptez sur une /procstructure spécifique ? Cela fonctionnerait sur n'importe quel Linux qui a procfs..
heemayl
1
D'accord, nous pouvons donc généraliser pour Linux comme il procfsest presque toujours présent, mais nous voyons souvent des questions de portabilité et une bonne méthodologie de développement inclut la prise en compte de la portabilité vers d'autres systèmes. bashpeut fonctionner sur une multitude de systèmes d'exploitation.
Julie Pelletier