Quelle est la difference entre “cat file | ./binary ”et“ ./binary <fichier ”?

102

J'ai un binaire (que je ne peux pas modifier) ​​et je peux faire:

./binary < file

Je peux aussi faire:

./binary << EOF
> "line 1 of file"
> "line 2 of file"
...
> "last line of file"
> EOF

Mais

cat file | ./binary

me donne une erreur. Je ne sais pas pourquoi ça ne marche pas avec un tuyau. Dans les 3 cas, le contenu du fichier est attribué à l'entrée standard de binaire (de différentes manières):

  1. bash lit le fichier et le donne à stdin de binaire
  2. bash lit les lignes de stdin (jusqu'à EOF) et le donne à stdin de binaire
  3. cat lit et met les lignes du fichier sur stdout, bash les redirige vers stdin de binaire

Le binaire ne devrait pas remarquer la différence entre ces 3 dans la mesure où je l’ai compris. Quelqu'un peut-il expliquer pourquoi le 3ème cas ne fonctionne pas?

BTW: L'erreur donnée par le binaire est:

20170116 / 125624.689 - U3000011 Impossible de lire le fichier de script '', code d'erreur '14'.

Mais ma question principale est la suivante: quelle différence y a-t-il entre un programme comportant 3 options?

Voici quelques détails supplémentaires: J'ai réessayé avec strace et il y avait en fait des erreurs ESPIPE (recherche illégale) de lseek suivies de EFAULT (adresse incorrecte) de lecture juste avant le message d'erreur.

Le binaire que j'ai essayé de contrôler avec un script ruby ​​(sans utiliser de fichiers temporaires) fait partie du callapi d' Automic (UC4) .

Boris
la source
25
Cool, il y a un détecteur UUOC intégré dans votre binaire. Je le veux.
xhienne
4
De quel système d'exploitation s'agit-il (afin que nous puissions savoir ce qu'il en est si c'est censé être une erreur)?
Stéphane Chazelas
6
Même s'il est possible pour un programme de réagir de cette manière, ce serait un programme étrangement buggy qui le ferait. Chaque programme non-fou qui attend une entrée de stdin doit fonctionner quand stdin est un tty, et s'il peut fonctionner à la fois avec un tty et un fichier, il y a peu de raison de ne pas supporter les pipes aussi. L'auteur du programme a probablement eu une hémorragie temporaire et bien que tout ce qui isatty()retourne faux sera un fichier à rechercher ou à masquer ...
Henning Makholm
9
Le code d'erreur 14 correspond à EFAULT. Sur une lecture qui se produit si le tampon que vous avez déclaré est invalide. Je voudrais stracer le programme mais je soupçonne qu’il cherche à obtenir une taille de tampon pour la lecture des données jusqu’à la fin du fichier, gérant mal le fait que la recherche ne fonctionne pas et essayant d’attribuer une taille négative (ne traitant pas un mauvais malloc) . Passer le tampon pour lire quelles erreurs étant donné que le tampon n'est pas valide.
Matthew Ife
3
@xhienne Non, il y a un catobturateur. Il semble que vous ne puissiez pas l'utiliser pour combiner deux fichiers, contrairement à l'utilisation prévue.
jpmc26

Réponses:

150

Dans

./binary < file

binary'stdin' est le fichier ouvert en mode lecture seule. Notez que bashcela ne lit pas le fichier du tout, il l’ouvre simplement pour le lire sur le descripteur de fichier 0 (stdin) du processus binarydans lequel il s’exécute .

Dans:

./binary << EOF
test
EOF

En fonction du shell, binaryle stdin sera soit un fichier temporaire supprimé (AT & T ksh, zsh, bash ...) qui contient ce qui test\nest mis ici par le shell ou l'extrémité de lecture d'un tuyau ( dash, yash; et le shell écrit test\nen parallèle à l'autre bout du tuyau). Dans votre cas, si vous utilisez bash, ce sera un fichier temporaire.

Dans:

cat file | ./binary

Selon le shell, binaryle stdin sera soit l'extrémité de lecture d'un tuyau, soit l'une des extrémités d'une paire de sockets dont le sens d'écriture a été arrêté (ksh93) et catécrit le contenu de filel'autre extrémité.

Lorsque stdin est un fichier normal (temporaire ou non), il est utilisable. binarypeut aller au début ou à la fin, rembobiner, etc. Il peut également ioctl()scréer une carte , en faire comme FIEMAP / FIBMAP (si vous utilisez <>plutôt que de <, il pourrait tronquer / perforer des trous, etc.).

Les pipes et les paires de sockets, quant à eux, sont un moyen de communication inter-processus, il n’ya pas grand binarychose à faire à côté readdes données (bien qu’il existe aussi des opérations telles que des ioctl()s spécifiques à un pipe que cela pourrait faire sur eux et non sur des fichiers normaux) .

La plupart du temps, il est la capacité manquante à seekqui provoque des applications à l' échec / se plaignent lorsque l'on travaille avec des tuyaux, mais il pourrait être l' un des autres appels système qui sont valides sur des fichiers normaux mais pas sur les différents types de fichiers (comme mmap(), ftruncate(), fallocate()) . Sous Linux, il existe également une grande différence de comportement lorsque vous ouvrez /dev/stdinlorsque le fd 0 est sur un canal ou sur un fichier normal.

Il y a beaucoup de commandes là - bas qui ne peut traiter que adressable fichiers, mais quand c'est le cas, qui est généralement pas pour les fichiers ouverts sur leur stdin.

$ unzip -l file.zip
Archive:  file.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
       11  2016-12-21 14:43   file
---------                     -------
       11                     1 file
$ unzip -l <(cat file.zip)
     # more or less the same as cat file.zip | unzip -l /dev/stdin
Archive:  /proc/self/fd/11
  End-of-central-directory signature not found.  Either this file is not
  a zipfile, or it constitutes one disk of a multi-part archive.  In the
  latter case the central directory and zipfile comment will be found on
  the last disk(s) of this archive.
unzip:  cannot find zipfile directory in one of /proc/self/fd/11 or
        /proc/self/fd/11.zip, and cannot find /proc/self/fd/11.ZIP, period.

unzipdoit lire l'index stocké à la fin du fichier, puis chercher dans le fichier pour lire les membres de l'archive. Mais ici, le fichier (normal dans le premier cas, pipe dans le second) est donné comme argument de chemin unzipet l' unzipouvre lui-même (généralement sur fd autre que 0) au lieu d'hériter d'un fd déjà ouvert par le parent. Il ne lit pas les fichiers zip à partir de son stdin. stdin est principalement utilisé pour l'interaction utilisateur.

Si vous l'exécutez binarysans redirection à l'invite d'un shell interactif s'exécutant dans un émulateur de terminal, alors binarystdin sera hérité de son parent le shell, qui l'aura lui-même hérité de son parent l'émulateur de terminal et sera un pty périphérique ouvert en mode lecture + écriture (quelque chose comme /dev/pts/n).

Ces appareils ne sont pas recherchés non plus. Donc, si cela binaryfonctionne correctement lors de la saisie du terminal, le problème n’est peut-être pas lié à la recherche.

Si ce numéro 14 est censé être un errno (un code d'erreur défini par des appels système défaillants), sur la plupart des systèmes, il s'agirait de EFAULT( adresse incorrecte ). L' read()appel système échouerait avec cette erreur s'il était invité à lire dans une adresse mémoire inscriptible. Cela serait indépendant du fait que le fd lise les données des points dans un pipe ou un fichier normal et indiquerait généralement un bogue 1 .

binarydétermine éventuellement le type de fichier ouvert sur son stdin (avec fstat()) et se heurte à un bogue alors qu'il ne s'agit ni d'un fichier normal ni d'un périphérique tty.

Difficile à dire sans en savoir plus sur l'application. L'exécuter sous strace(ou truss/ tuscéquivalent sur votre système) pourrait nous aider à voir quel est l'appel système, s'il y en a un qui échoue ici.


1 Le scénario envisagé par Matthew Ife dans le commentaire de votre question semble fort plausible ici. En le citant:

Je soupçonne qu’il cherche en fin de fichier à obtenir une taille de mémoire tampon pour la lecture des données, gérant mal le fait que la recherche ne fonctionne pas et essayant d’allouer une taille négative (ne traitant pas un mauvais malloc). Passer le tampon pour lire quelles erreurs étant donné que le tampon n'est pas valide.

Stéphane Chazelas
la source
14
Très intéressant ... c'est la première fois que j'ai entendu dire qu'une entrée standard redirigée dans le style de ./binary < fileest recherchée!
David Z
2
@DavidZ c'est un fichier qui a été openédité et qui se comporte comme n'importe quel fichier qui a été openédité. Il se trouve que cela a simplement été hérité d'un processus parent, mais ce n'est pas si rare.
Hobbs
3
Si le système contient strace ou un outil similaire, il peut être utilisé pour vérifier quel appel système le binaire a échoué.
Pabouk
2
"Il peut également le tronquer, le masquer, percer des trous, etc." - Et bien non. Le fichier est ouvert en mode lecture seule. Le programme devrait l'ouvrir en mode écriture pour le faire. Mais il ne peut pas l'ouvrir en mode écriture, car il n'y a pas d'interface pour le faire directement, pas plus qu'il n'y a d'interface pour trouver "l'entrée" du répertoire qui correspond à un fichier ouvert (que se passe-t-il s'il y a deux dentries de ce type ou zéro?) . Il devrait alors stater le fichier, puis scanner le système de fichiers à la recherche d'un objet portant le même numéro d'inode. Ce serait excessivement lent.
Kevin
1
@ StéphaneChazelas: ah oui, ça open("/proc/self/fd/0", O_RDWR)marche même sur des fichiers supprimés. Silly moi: P. echo foo>foo; (sleep 0.5; ll -L /proc/self/fd/0; strace ./a.out; ll -L /proc/self/fd/0) < foo & sleep 0.1 && rm fooannule les liens fooavant que a.out soit exécuté avec son stdin redirigé foo.
Peter Cordes
46

Voici un exemple de programme simple qui illustre la réponse de Stéphane Chazelas en utilisant lseek(2):

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
    int c;
    off_t off;
    off = lseek(0, 10, SEEK_SET);
    if (off == -1)
    {
        perror("Error");
        return -1;
    }
    c = getchar();
    printf("%c\n", c);
}

Essai:

$ make seek
cc     seek.c   -o seek
$ cat foo
abcdefghijklmnopqrstuwxyz
$ ./seek < foo
k
$ ./seek <<EOF
> abcdefghijklmnopqrstuvwxyz
> EOF
k
$ cat foo | ./seek
Error: Illegal seek

Les pipes ne sont pas recherchées, et c'est un endroit où un programme peut se plaindre de pipes.

muru
la source
21

Le tuyau et la redirection sont des animaux différents, pour ainsi dire. Lorsque vous utilisez here-docredirection ( <<) ou stdin, < le texte ne sort pas de nulle part - il entre dans un descripteur de fichier (ou dans un fichier temporaire, si vous préférez), et c’est là que le stdin du binaire sera pointé.

Plus précisément, voici un extrait du bash'scode source, fichier redir.c (version 4.3):

/* Create a temporary file holding the text of the here document pointed to
   by REDIRECTEE, and return a file descriptor open for reading to the temp
   file.  Return -1 on any error, and make sure errno is set appropriately. */
static int
here_document_to_fd (redirectee, ri)

Ainsi, étant donné que la redirection peut en principe être traitée comme un fichier, les fichiers binaires peuvent y naviguer ou seek()passer facilement d’un fichier à l’autre en passant à n’importe quel octet du fichier.

Les pipes, étant donné qu’elles sont des tampons de 64 Ko (au moins sous Linux) avec des écritures de 4096 octets ou moins garanties comme étant atomiques, ne peuvent pas être recherchées, c’est-à-dire que vous ne pouvez pas y naviguer librement - vous ne pouvez les lire que séquentiellement. Une fois, j'ai implémenté la tailcommande en python. 29 millions de lignes de texte peuvent être recherchées en microsecondes si elles sont redirigées, mais si catvous le faites via un tuyau, eh bien, rien ne peut être fait - tout doit donc être lu de manière séquentielle.

Une autre possibilité est que le binaire puisse vouloir ouvrir un fichier spécifiquement et ne pas recevoir d'entrée d'un tuyau. Cela se fait généralement via fstat()un appel système et en vérifiant si l'entrée provient d'un S_ISFIFOtype de fichier (ce qui signifie un canal / un canal nommé).

Votre binaire spécifique, puisque nous ne savons pas ce que c'est, tente probablement de chercher, mais ne peut pas chercher de canal. Il est recommandé de consulter sa documentation pour savoir ce que signifie exactement le code d'erreur 14.

REMARQUE : Certains shells, tels que dash (Debian Almquist Shell, par défaut /bin/shsous Ubuntu), implémentent la here-docredirection avec les canaux en interne . Il est donc possible qu'ils ne soient pas accessibles. Le point reste le même - les canaux sont séquentiels et ne peuvent pas être parcourus facilement, ce qui entraînerait des erreurs.

Sergiy Kolodyazhnyy
la source
La réponse de Stéphane indique que la documentation ici peut être mise en œuvre avec des tuyaux, et que certains coquillages communs le dashfont. Cette réponse explique le comportement observé avec bash, mais ce comportement n'est apparemment pas garanti pour les autres coques.
Peter Cordes
@PeterCordes, c'est tout à fait vrai, et je viens de le vérifier dashsur mon système. Je n'étais pas au courant de cela auparavant. Merci de signaler
Sergiy Kolodyazhnyy
Autre commentaire: vous utiliseriez fstat()stdin pour vérifier s’il s’agissait d’un tuyau. statprend un chemin. Mais en réalité, lseekle moyen le plus raisonnable de déterminer si un fd est recherché après s’être déjà ouvert est d’essayer de le faire.
Peter Cordes
5

La principale différence réside dans la gestion des erreurs.

Dans le cas suivant, l'erreur est signalée

$ /bin/cat < z.txt
-bash: z.txt: No such file or directory
$ echo $?
1

Dans le cas suivant, l'erreur n'est pas signalée.

$ cat z.txt | /bin/cat
cat: z.txt: No such file or directory
$ echo $?
0

Avec bash, vous pouvez toujours utiliser PIPESTATUS:

$ cat z.txt | /bin/cat
cat: z.txt: No such file or directory
$ echo ${PIPESTATUS[0]}
1

Mais il n'est disponible qu'immédiatement après l'exécution de la commande:

$ cat z.txt | /bin/cat
cat: z.txt: No such file or directory
$ echo $?
0
$ echo ${PIPESTATUS[0]}
0
# oops !

Il existe une autre différence lorsque nous utilisons des fonctions de shell à la place des fichiers binaires. Dans bash, les fonctions faisant partie d'un pipeline sont exécutées dans des sous-shells (à l'exception du dernier composant de pipeline si l' lastpipeoption est activée et qu'elle bashn'est pas interactive), le changement de variables n'a donc aucun effet dans le shell parent:

$ a=a
$ b=b
$ x(){ a=x;}
$ y(){ b=y;}

$ echo $a $b
a b

$ x | y
$ echo $a $b
a b

$ cat t.txt | y
$ echo $a $b
a b

$ x | cat
$ echo $a $b
a b

$ x < t.txt
$ y < t.txt
$ echo $a $b
x y
Vouze
la source
4
Donc, vous montrez que la gestion des erreurs avec >est effectuée par le shell, mais avec pipe, elle est effectuée par une commande qui produit du texte. D'ACCORD. Mais dans cette question spécifique, OP utilise un fichier existant, ce n'est donc pas le problème, et l'erreur est clairement produite par le binaire.
Sergiy Kolodyazhnyy
1
Bien que la plupart des questions ne soient pas pertinentes, cette réponse a une certaine pertinence par rapport à cette Q & R dans le cas général et est généralement correcte, alors je ne pense pas qu'elle mérite ces critiques négatives.
Stéphane Chazelas
@Serg: Lorsque vous utilisez shell en tant que ligne de commande, ce n'est pas important. Mais dans les scripts, le traitement des erreurs peut être très important.
Vouze