Redirection IO et la commande head

9

J'essayais de modifier rapidement un .hgignorefichier à partir du shell bash Cygwin aujourd'hui, et j'ai ajouté une ligne qui était une erreur. Je ne sais pas si c'était la meilleure façon de le faire, mais j'ai rapidement pensé head -1 .hgignoreà supprimer la ligne incriminée (je n'avais auparavant qu'une seule ligne dans le fichier). Effectivement, une fois exécuté, il donne la première ligne comme seule sortie.

Mais lorsque j'ai essayé de rediriger la sortie et de réécrire le fichier à l'aide head -1 .hgignore > .hgignore, le fichier était vide. Pourquoi cela arrive-t-il? Si j'essaie d'ajouter à la place, head -1 .hgignore >> .hgignorecela s'ajoute correctement, mais ce n'est évidemment pas le résultat souhaité. Pourquoi une redirection tronquée ne fonctionne-t-elle pas dans ce cas?

voithos
la source

Réponses:

10

Lorsque le shell obtient une ligne de commande comme: command > file.outle shell lui-même ouvre (et crée peut-être) le fichier nommé file.out. Le shell définit le descripteur de fichier 0 sur le descripteur de fichier obtenu à partir de l'ouverture. Voilà comment fonctionne la redirection d'E / S: chaque processus connaît les descripteurs de fichiers 0, 1 et 2.

La partie difficile à ce sujet est de savoir comment ouvrir file.out. La plupart du temps, vous voulez file.outouvrir pour l'écriture à l'offset 0 (c'est-à-dire tronqué) et c'est ce que le shell a fait pour vous. Il a tronqué .hgignore, l'a ouvert pour l'écriture, a dupé le descripteur de fichier à 0, puis l'a exécuté head. Clobber fichier instantané.

Dans bash shell, vous faites un set noclobberpour changer ce comportement.

Bruce Ediger
la source
Aha je vois. Je pensais que le shell tronquait le fichier avant d'exécuter la commande, mais je ne savais pas pourquoi. Merci pour l'explication!
voithos
10

Je pense que Bruce répond à ce qui se passe ici avec le pipeline shell.

Un de mes petits utilitaires préférés est la spongecommande de moreutils . Il résout exactement ce problème en "absorbant" toutes les entrées disponibles avant d'ouvrir le fichier de sortie cible et d'écrire les données. Il vous permet d'écrire des pipelines exactement comme vous vous attendiez à:

$ head -1 .hgignore | sponge .hgignore

La solution du pauvre est de diriger la sortie vers un fichier temporaire, puis une fois la ligne de repère terminée (par exemple la prochaine commande que vous exécutez) consiste à déplacer le fichier temporaire vers l'emplacement du fichier d'origine.

$ head -1 .hgingore > .hgignore.tmp
$ mv .hgignore{.tmp,}
Caleb
la source
En y réfléchissant quelques années plus tard, une pensée m'est venue à l'esprit: ne pourrions-nous pas simplement le faire head -1 .hgignore | tee .hgignore? teeest dans coreutils, et comme un avantage / effet secondaire, cela écrit également à STDOUT
voithos
@voithos À ma connaissance, teeouvre et tronque le fichier dans lequel il écrit lorsqu'il est instancié comme tout le reste, de sorte qu'il ne résout pas le problème principal ici de la condition de concurrence sur la lecture du contenu du fichier avant de le tronquer avec l'écriture.
Caleb
Vous soulevez un point que je n'étais pas au courant, en fait - à savoir que les commandes canalisées sont démarrées immédiatement, plutôt que séquentiellement. Est-ce exact? Cependant, je l'ai testé et tee semble faire la chose souhaitée. J'ai une version 8.13sur ma machine.
voithos
1
Les commandes @voithos Yes dans une ligne et tous les canaux d'entrée / sortie impliqués sont démarrés dans l'ordre inverse, de sorte que le pipeline est prêt à recevoir des données lorsque le premier commence à les transmettre. Je soupçonne que votre test est défectueux car vous avez probablement utilisé un trop petit bloc de données et il a tout mis en cache dans un tampon de lecture avant d'en avoir besoin. Le teeprogramme tronquera vos fichiers, il n'est pas configuré pour les mettre en double tampon.
Caleb
3

Dans

head -n 1 file > file

fileest tronqué avant le headdémarrage, mais si vous l'écrivez:

head -n 1 file 1<> file

ce n'est pas ce qui fileest ouvert en mode lecture-écriture. Cependant, une fois l' headécriture terminée, il ne tronque pas le fichier, donc la ligne ci-dessus serait un no-op ( headjuste réécrire la première ligne sur elle-même et laisser les autres intactes).

Cependant, après headson retour et pendant que le fdest toujours ouvert, vous pouvez appeler une autre commande qui effectue le truncate.

Par exemple:

{ head -n 1 file; perl -e 'truncate STDOUT, tell STDOUT'; } 1<> file

Ce qui importe ici, c'est que truncateci - dessus, headdéplace simplement le curseur pour fd 1 à l'intérieur du fichier juste après la première ligne. Il réécrit la première ligne dont nous n'avions pas besoin, mais ce n'est pas dangereux.

Avec une tête POSIX, nous pourrions réellement nous en tirer sans réécrire cette première ligne:

{ head -n 1 > /dev/null
  perl -e 'truncate STDIN, tell STDIN'
} <> file

Ici, nous utilisons le fait qui headdéplace la position du curseur dans son stdin. Bien headque lise généralement son entrée par gros morceaux pour améliorer les performances, POSIX l'oblige (dans la mesure du possible) à seekreculer juste après la première ligne s'il l'a dépassée. Notez cependant que toutes les implémentations ne le font pas.

Alternativement, vous pouvez utiliser la readcommande du shell à la place dans ce cas:

{ read -r dummy; perl -e 'truncate STDIN, tell STDIN'; } <> file
Stéphane Chazelas
la source
1
Stéphane, connaissez-vous une commande standard ou coreutils qui peut tronquer STDINsimilaire à ce que vous avez accompli en utilisant perlci
iruvar
2
@ 1_CR, non. ddpeut cependant tronquer à n'importe quel décalage absolu arbitraire dans le fichier. Vous pouvez donc déterminer le décalage en octets de la deuxième ligne et tronquer à partir de là avecdd bs=1 seek="$offset" of=file
Stéphane Chazelas
1

La solution du vrai homme est

ed .hgignore
$d
wq

ou en une ligne

printf '%s\n' '$d' 'wq' | ed .hgignore

Ou avec GNU sed:

sed -i '$d' .hgignore

(Non, je plaisante. J'utiliserais un éditeur interactif. vi .hgignore GddZZ)

Gilles 'SO- arrête d'être méchant'
la source
Je me suis demandé s'il y avait un avantage à utiliser :wqOver ZZ?
voithos
Aussi, :xce que mes doigts font automatiquement
glenn jackman
et ZQest le même que:q!
glenn jackman
ZZ et: x n'écrivent que s'il y a quelque chose à écrire ...: w synchronise toujours le fichier sur le disque, même s'il en a besoin. J'utilise: xa parce que j'utilise des onglets.
xenoterracide
1

Vous pouvez utiliser Vim en mode Ex:

ex -sc '2,d|x' .hgignore
  1. 2, sélectionnez les lignes 2 jusqu'à la fin

  2. d supprimer

  3. x sauver et fermer

Steven Penny
la source
0

Pour l'édition de fichiers sur place, vous pouvez également utiliser l'astuce de gestion des fichiers ouverts comme indiqué par Jürgen Hötzel dans Rediriger la sortie de sed / s / c / d / 'myFile vers myFile .

exec 3<.hgignore
rm .hgignore  # prevent open file from being truncated
head -1 <&3 > .hgignore

ls -l .hgignore  # note that permissions may have changed
dan55
la source
2
Et juste après rm .hgignoreune panne de courant, vous évitez des heures de dur labeur. D'accord, cela n'a pas d'importance .hgignore, mais pourquoi feriez-vous quelque chose de compliqué de toute façon? D'où mon downvote: techniquement correct mais une très mauvaise idée.
Gilles 'SO- arrête d'être méchant'
@Gilles, peut-être pas une si bonne idée, mais c'est par exemple ce que fait perl -i(pour l'édition sur place), et je ne serais pas surpris si certaines implémentations de le sed -ifaisaient aussi (bien que la dernière version de GNU sedne semble pas le faire).
Stéphane Chazelas