la tête mange des caractères supplémentaires


15

La commande shell suivante était censée imprimer uniquement les lignes impaires du flux d'entrée:

echo -e "aaa\nbbb\nccc\nddd\n" | (while true; do head -n 1; head -n 1 >/dev/null; done)

Mais au lieu il imprime juste la première ligne: aaa.

La même chose ne se produit pas lorsqu'elle est utilisée avec l' option -c( --bytes):

echo 12345678901234567890 | (while true; do head -c 5; head -c 5 >/dev/null; done)

Cette commande sort 1234512345comme prévu. Mais cela ne fonctionne que dans l' implémentation coreutils de l' headutilitaire. L' implémentation de busybox mange toujours des caractères supplémentaires, donc la sortie est juste 12345.

Je suppose que ce mode de mise en œuvre spécifique est effectué à des fins d'optimisation. Vous ne pouvez pas savoir où se termine la ligne, vous ne savez donc pas combien de caractères vous devez lire. La seule façon de ne pas consommer de caractères supplémentaires dans le flux d'entrée est de lire le flux octet par octet. Mais la lecture du flux un octet à la fois peut être lente. Je suppose donc qu'il headlit le flux d'entrée dans un tampon suffisamment grand, puis compte les lignes dans ce tampon.

On ne peut pas en dire autant du cas où l' --bytesoption est utilisée. Dans ce cas, vous savez combien d'octets vous devez lire. Vous pouvez donc lire exactement ce nombre d'octets et pas plus que cela. L' implémentation de corelibs utilise cette opportunité, mais pas celle de busybox , elle lit toujours plus d'octets que nécessaire dans un tampon. Cela est probablement fait pour simplifier la mise en œuvre.

Donc la question. Est-il correct que l' headutilitaire consomme plus de caractères du flux d'entrée qu'il ne lui a été demandé? Existe-t-il une sorte de standard pour les utilitaires Unix? Et s'il y en a un, spécifie-t-il ce comportement?

PS

Vous devez appuyer sur Ctrl+Cpour arrêter les commandes ci-dessus. Les utilitaires Unix n'échouent pas à la lecture au-delà EOF. Si vous ne voulez pas appuyer, vous pouvez utiliser une commande plus complexe:

echo 12345678901234567890 | (while true; do head -c 5; head -c 5 | [ `wc -c` -eq 0 ] && break >/dev/null; done)

que je n'ai pas utilisé pour plus de simplicité.


2
Neardupe unix.stackexchange.com/questions/48777/… et unix.stackexchange.com/questions/84011/… . Aussi, si ce titre avait été sur les films.SX ma réponse serait Zardoz :)
dave_thompson_085

Réponses:


30

Est-il correct que l'utilitaire head consomme plus de caractères du flux d'entrée qu'il ne lui a été demandé?

Oui, c'est autorisé (voir ci-dessous).

Existe-t-il une sorte de standard pour les utilitaires Unix?

Oui, POSIX volume 3, Shell & Utilities .

Et s'il y en a un, spécifie-t-il ce comportement?

Il fait, dans son introduction:

Lorsqu'un utilitaire standard lit un fichier d'entrée recherché et se termine sans erreur avant d'atteindre la fin du fichier, l'utilitaire doit s'assurer que le décalage de fichier dans la description du fichier ouvert est correctement positionné juste après le dernier octet traité par l'utilitaire. Pour les fichiers qui ne peuvent pas être recherchés, l'état du décalage de fichier dans la description du fichier ouvert pour ce fichier n'est pas spécifié.

headest l'un des utilitaires standard , donc une implémentation conforme à POSIX doit implémenter le comportement décrit ci-dessus.

GNU head ne tentent de quitter le descripteur de fichier dans la bonne position, mais il est impossible de chercher sur les tuyaux, donc dans votre test , il ne parvient pas à rétablir la position. Vous pouvez le voir en utilisant strace:

$ echo -e "aaa\nbbb\nccc\nddd\n" | strace head -n 1
...
read(0, "aaa\nbbb\nccc\nddd\n\n", 8192) = 17
lseek(0, -13, SEEK_CUR)                 = -1 ESPIPE (Illegal seek)
...

Le readretourne 17 octets (toutes les entrées disponibles), en headtraite quatre et essaie ensuite de reculer de 13 octets, mais il ne peut pas. (Vous pouvez également voir ici que GNU headutilise un tampon de 8 Ko.)

Lorsque vous dites headde compter les octets (ce qui n'est pas standard), il sait combien d'octets à lire, il peut donc (s'il est implémenté de cette façon) limiter sa lecture en conséquence. C'est pourquoi votre head -c 5test fonctionne: GNU headne lit que cinq octets et n'a donc pas besoin de chercher à restaurer la position du descripteur de fichier.

Si vous écrivez le document dans un fichier et que vous l'utilisez à la place, vous obtiendrez le comportement que vous recherchez:

$ echo -e "aaa\nbbb\nccc\nddd\n" > file
$ < file (while true; do head -n 1; head -n 1 >/dev/null; done)
aaa
ccc

2
On peut utiliser les utilitaires line(maintenant supprimés de POSIX / XPG mais toujours disponibles sur de nombreux systèmes) ou read( IFS= read -r line) qui lisent un octet à la fois pour éviter le problème.
Stéphane Chazelas

3
Notez que la head -c 5lecture de 5 octets ou d'un tampon complet dépend de l'implémentation (notez également que ce head -cn'est pas standard), vous ne pouvez pas vous fier à cela. Vous devez dd bs=1 count=5avoir la garantie que pas plus de 5 octets seront lus.
Stéphane Chazelas

Merci @ Stéphane, j'ai mis à jour la -c 5description.
Stephen Kitt

Notez que la fonction headintégrée ksh93lit un octet à la fois head -n 1lorsque l'entrée n'est pas recherchée.
Stéphane Chazelas

1
@anton_rh, ddne fonctionne correctement qu'avec les canaux avec bs=1si vous utilisez un countas reads sur les canaux peut retourner moins que demandé (mais au moins un octet à moins que eof ne soit atteint). GNU dda iflag=fullblockqui peut atténuer ce bien.
Stéphane Chazelas

6

de POSIX

L' utilitaire head doit copier ses fichiers d'entrée sur la sortie standard, terminant la sortie de chaque fichier à un point désigné.

Il ne dit rien sur la quantité de données à head lire depuis l'entrée. Exiger qu'il soit lu octet par octet serait idiot, car il serait extrêmement lent dans la plupart des cas.

Ceci est, cependant, traité dans l' readutilitaire intégré /: tous les shells que je peux trouver readdans les tuyaux un octet à la fois et le texte standard peut être interprété comme signifiant que cela doit être fait, pour pouvoir lire uniquement une seule ligne:

L' utilitaire de lecture doit lire une seule ligne logique de l'entrée standard dans une ou plusieurs variables de shell.

Dans le cas de read, qui est utilisé dans les scripts shell, un cas d'utilisation courant serait quelque chose comme ceci:

read someline
if something ; then 
    someprogram ...
fi

Ici, l'entrée standard de someprogramest la même que celle du shell, mais on peut s'attendre someprogramà ce que tout ce qui vient après la première ligne d'entrée consommée par le readet non ce qui reste après une lecture tamponnée puisse être lu read. En revanche, utiliser headcomme dans votre exemple est beaucoup plus rare.


Si vous voulez vraiment supprimer toutes les autres lignes, il serait préférable (et plus rapide) d'utiliser un outil capable de gérer l'intégralité de l'entrée en une seule fois, par exemple

$ seq 1 10 | sed -ne '1~2p'   # GNU sed
$ seq 1 10 | sed -e 'n;d'     # works in GNU sed and the BSD sed on macOS

$ seq 1 10 | awk 'NR % 2' 
$ seq 1 10 | perl -ne 'print if $. % 2'

Mais voir la section "INPUT FILES" de l'introduction POSIX au volume 3 ...
Stephen Kitt

1
POSIX dit: "Lorsqu'un utilitaire standard lit un fichier d'entrée recherché et se termine sans erreur avant d'atteindre la fin du fichier, l'utilitaire doit s'assurer que l'offset de fichier dans la description du fichier ouvert est correctement positionné juste après le dernier octet traité par l'utilitaire. Pour les fichiers qui ne peuvent pas être recherchés, l'état du décalage de fichier dans la description du fichier ouvert pour ce fichier n'est pas spécifié. "
AlexP

2
Notez que, sauf si vous utilisez -r, readpeut lire plusieurs lignes (sans IFS=cela, il supprimerait également les espaces et les tabulations de début et de fin (avec la valeur par défaut de $IFS)).
Stéphane Chazelas

@AlexP, oui, Stephen vient de relier cette partie.
ilkkachu

Notez que la fonction headintégrée ksh93lit un octet à la fois head -n 1lorsque l'entrée n'est pas recherchée.
Stéphane Chazelas

1
awk '{if (NR%2) == 1) print;}'

Hellóka :-) et bienvenue sur le site! Remarque, nous préférons les réponses plus élaborées. Ils devraient être utiles aux googleurs de l'avenir.
peterh
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.