web-dev-qa-db-fra.com

Replier un pipeline dans son origine

On pourrait penser que

echo foo >a
cat a | rev >a

laisserait a contenant oof; mais à la place, il est laissé vide.

  1. Pourquoi?
  2. Comment appliquer autrement rev à a?
24
Toothrot

Il y a une application pour ça! La commande sponge de moreutils est conçue précisément pour cela. Si vous utilisez Linux, il est probablement déjà installé, sinon recherchez les référentiels de votre système d'exploitation pour sponge ou moreutils. Ensuite, vous pouvez faire:

echo foo >a
cat a | rev | sponge a

Ou, en évitant le oC :

rev a | sponge a

La raison de ce comportement est liée à l'ordre dans lequel vos commandes sont exécutées. Le > a est en fait la toute première chose exécutée et > file vide le fichier. Par exemple:

$ echo "foo" > file
$ cat file
foo
$ > file
$ cat file
$

Ainsi, lorsque vous exécutez cat a | rev >a ce qui se passe réellement, c'est que le > a est exécuté en premier, vidant le fichier, donc quand cat a est exécuté le fichier est déjà vide. C'est précisément pourquoi sponge a été écrit (à partir de man sponge, c'est moi qui souligne):

l'éponge lit l'entrée standard et l'écrit dans le fichier spécifié. Contrairement à une redirection Shell, l'éponge absorbe toutes ses entrées avant d'écrire le fichier de sortie. Cela permet de construire des pipelines qui lisent et écrivent dans le même fichier.

31
terdon
  1. La troncature de sortie est effectuée très tôt, donc cat voit un fichier vide.
  2. Soit le premier fichier est construit comme un temporaire, soit la sortie de rev est dirigée vers un temporaire que vous renommez ensuite.
10
stolenmoment

une autre façon de résoudre ce problème consiste à utiliser une méthode d'écriture qui ne tronque pas

  rev a | dd conv=notrunc of=a

cela ne fonctionne que parce que:

  1. rev lit le contenu avant de produire la sortie et la sortie n'est jamais plus longue que la quantité déjà lue

  2. le nouveau contenu du fichier est de la même taille ou plus grand que l'original (dans ce cas, même taille)

  3. dd ouvre le fichier à écrire sans le tronquer.

Cette approche peut être utile pour la modification sur place de fichiers trop volumineux pour conserver des copies temporaires de.

7
Jasen
cat a | rev > a

Pourquoi [est a laissé vide]?

Dans le pipeline ci-dessus, le shell bifurque deux sous-processus, un pour chacune des deux parties du pipeline. Ces sous-processus exécutent ensuite les commandes en question, traitant d'abord toutes les redirections, puis appelant l'une des fonctions exec*() pour démarrer l'utilitaire externe. Les sous-processus s'exécutent en parallèle, et il n'y a aucune garantie de synchronisation entre eux.

L'exécution d'un processus n'est pas très rapide, donc ce qui se passe généralement, c'est que le Shell sur le côté droit parvient à configurer la redirection avant que cat n'ait la possibilité de lire le fichier. La redirection de sortie > a Tronque le fichier, donc cat n'a rien à lire, rev ne reçoit aucune donnée et ne produit aucune donnée. Même si vous avez également utilisé une redirection du côté gauche (cat < a | rev > a), a pourrait être ouvert en lecture avant d'être tronqué, mais cat n'aurait probablement pas le temps de le lire avant cela.

D'un autre côté, cela imprime de manière assez constante a contains: foo Sur mon système:

echo foo > a; cat < a | tee a > /dev/null ; echo "a contains: $(cat a)"

Ici, c'est tee qui tronque le fichier, donc cela se produit après que la exec() et cat a une meilleure chance d'avoir le temps de lire le fichier. Mais si le fichier était suffisamment volumineux, il pourrait être tronqué au milieu de sa lecture.

J'ai dit que pourrait et probablement là-bas, car en effet l'exact opposé peut se produire , si le système d'exploitation décide de planifier les processus d'une autre manière.

Comment appliquer autrement rev à a?

La solution habituelle consiste à utiliser un fichier temporaire:

cat a | rev > b && mv b a

Bien qu'il y ait le problème habituel de remplacer éventuellement un fichier existant, sauf si vous pouvez être sûr que le nom du fichier temporaire est disponible. Vous devriez probablement utiliser mktemp:

f=$(mktemp ./tmp.XXXXXX)
cat a | rev > "$f" && mv "$f" a || rm "$f"

Alternativement, vous pouvez utiliser l'outil sponge , qui s'assure de lire toutes les entrées qu'il obtient avant d'ouvrir le fichier de sortie (sinon c'est comme cat):

cat a | rev | sponge a

ou juste

rev < a | sponge a

sponge > a Serait une erreur pour la même raison que la commande d'origine ne fonctionne pas.


L'éponge vient de moreutils , et n'est pas un outil standard. Certaines alternatives sont listées dans Tamponner complètement la sortie de la commande avant de passer à une autre commande?

Certains utilitaires peuvent implémenter eux-mêmes une fonctionnalité similaire, par exemple sort -o outputfile Ouvre le fichier de sortie seulement après la fin, voir Est-ce que le tri prend en charge le tri d'un fichier sur place, comme `sed --in-place`?

3
ilkkachu

Vous pouvez utiliser Vim en mode Ex:

ex -s -c '%!rev' -c x a.txt
  • % sélectionner toutes les lignes
  • ! exécuter la commande
  • x enregistrer et fermer
0
Fourteen