Passer stdin à plusieurs programmes sans fermer

1

Je veux diviser et GZip un fichier volumineux, et cette réponse semblait être ce que je cherchais, et cela me semblait une manière très utile de faire des choses auxquelles je n’avais jamais pensé, alors je voudrais les généraliser; le seul problème est: cela ne semble pas fonctionner.

Dites que je veux diviser mon entrée et la traiter plus avant (je sais split mais je veux le diriger directement dans mon script!)

Cette utilise read lire une ligne dans une variable

#!/bin/bash
printf " a \n b \n c \n d " |
for ((i = 0 ; i < 2 ; i++)) ; do
  echo "<< $i >>"
  for ((j = 0 ; j < 2 ; j++)) ; do
    read l
    echo "$l"
  done
done

Il imprime

<< 0 >>
a
b
<< 1 >>
c
d

Ce qui est presque ce que je veux, mis à part le fait qu'il réduit les espaces au début et à la fin (et peut-être modifie la ligne d'une autre manière? Cela fonctionnera-t-il avec du contenu codé UTF-8 arbitraire?) modifier résolu

Et j'imagine que cela pourrait être assez lent. modifier Le comparé: au moins 3000x plus lent.

Alors j'ai essayé de le faire passer à travers head (Je reçois le résultat en utilisant awk comme le suggère la réponse, cela ne semble pas faire autre chose)

#!/bin/bash
printf " a \n b \n c \n d " |
for ((i = 0 ; i < 2 ; i++)) ; do
  echo "<< $i >>"
  head -n 2
done

Qui imprime

<< 0 >>
 a 
 b 
<< 1 >>

Et arrête parce que head ferme apparemment son entrée en sortie. Je n'ai pas trouvé de programme qui ne le fasse pas, et peut-être est-il réellement imposé par le système? (Je suis sous OS X)

En utilisant head -n 2 <&0 qui (selon les bash docs) copie le descripteur de fichier ne fonctionne pas non plus.

Dois-je utiliser un tuyau nommé? Y a-t-il une incantation pour faire ce travail?

pascal
la source
Comment savez-vous que les flans sont en train d'être enlevés? Vous devriez mettre quelque chose comme echo "..$l.." pour voir ce qui a été stocké dans l comme écho ignore les blancs de début et de fin.
AFH
read le dépouille, echo ".$l." empreintes .a.. Je pense que le shell supprime les espaces lors de la division des arguments, l=" a "; echo $l empreintes a mais l=" a "; echo "$l" affiche les espaces `a`. ( read peut également saisir des espaces et remplir plusieurs variables, c’est probablement pour cela)
pascal
Êtes-vous à l'aise d'utiliser une autre langue, comme Perl? Il serait facile de lire dans un fichier, de parcourir les lignes, de créer des fichiers de sortie (ou des données / variables internes) pour contenir chacun des "morceaux". Faites-moi savoir si vous pouvez prendre un exemple en Perl et le modifier afin que vous puissiez "canaliser" les données dans le reste du script. Si vous pouvez gérer cela, je peux écrire le Perl initial pour le scinder.
jimtut
Non, je saurais le faire dans une autre langue (Python pour moi) mais j'espérais qu'il y avait un moyen simple de le faire en bash qui me manque. Certains "empêcher le programme de fermer stdin" drapeau?
pascal
Désolé, des visiteurs sont arrivés et j'ai essayé de conclure mon commentaire, mais un peu trop vite, semble-t-il. J'ai trouvé une alternative dans le ligne commander: l = "` line` " lit une ligne complète à partir de l'entrée standard et l'assigne à l , complet avec tous les blancs. Chaque invocation de ligne va lire une autre ligne d'entrée. Vous pouvez l'utiliser à la place de votre read l.
AFH

Réponses:

1

Le problème ici n'est pas exactement ça head ou awk sont "fermer l'entrée". Ils n'ont pas le choix. tout programme ferme son entrée quand il se termine, et cela est imposé par le système d'exploitation.

Le problème est que l'entrée standard est un canal et que les programmes effectuent des lectures en mémoire tampon. Il n'y a aucun moyen de ne pas lire à partir d'un tube, donc les données contenues dans le readahead ont disparu. Si au lieu d'utiliser un canal, vous utilisez un fichier, vous verrez probablement que cela fonctionne bien:

#!/bin/bash
printf " %s \n" a b c d > /tmp/abcd
for ((i = 0 ; i < 2 ; i++)) ; do
    echo "<< $i >>"
    for ((j = 0 ; j < 2 ; j++)) ; do
        read
        echo "$REPLY"
    done
done < /tmp/abcd

Au moins, cela fonctionne bien sur Ubuntu. Vous pouvez le faire fonctionner avec un tuyau si vous désactivez la mise en mémoire tampon - mais cela ralentira probablement les choses. Voici un petit programme en C qui désactive la mise en mémoire tampon, puis répète son caractère entrée par caractère jusqu'à ce qu'il consomme le nombre de lignes demandé:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv) {
  int n = 1000;
  if (argc > 1) n = atoi(argv[1]);
  setvbuf(stdin, NULL, _IONBF, 0);
  for (int ch = getchar(); ch != EOF; ch = getchar()) {
    putchar(ch);
    if (ch == '\n' && --n <= 0) break;
  }
  return n > 0;
}

Cela a bien fonctionné pour moi (encore sur Ubuntu - et vous devez le compiler avec -std=c99 ou -std=c11 afin que le compilateur ne se plaint pas). C'est vrai que le programme n'appelle pas fclose(stdin), mais ajouter ne fera aucune différence. D'autre part, supprimer l'appel à setvbuf va probablement vous ramener au symptôme que vous avez observé avec head. (Et cela fera aussi que le programme fonctionne lot plus rapide.)

Si vous aviez GNU split au lieu de la version BSD livrée avec OS X, vous pourrez utiliser l’utile --filter=COMMAND une syntaxe qui fait à peu près exactement ce que vous voulez; au lieu de créer des fichiers fractionnés, il dirige chaque section de fichier vers un appel de la commande spécifiée (et définit la variable d'environnement $FILE au nom de fichier attendu).

rici
la source
Agh, oui, n'a pas pensé à la mise en mémoire tampon. Je ne savais pas à propos de --filter; c'est le genre de flexibilité que je cherchais.
pascal
1

En spécifiant une variable à read vous lui ordonnez de séparer les mots. Ne faites pas ça, et les espaces resteront intacts:

#!/bin/bash
printf " a \n b \n c \n d " |
for ((i = 0 ; i < 2 ; i++)) ; do
    echo "<< $i >>"
    for ((j = 0 ; j < 2 ; j++)) ; do
        read
        echo "$REPLY"
    done
done

Sortie:

<< 0 >>
 a  
 b  
<< 1 >>
 c  
 d  

Cela semble être très simple, mais en fait, vous avez posé une très bonne question car cette fonctionnalité n’est pas expliquée clairement dans l’homme.

P. S. Je voudrais utiliser un -r flag (ne pas traiter \ comme échappé) pour read également.

Dmitry Alexandrov
la source
gentil, ne savait pas $REPLY. L'alternative que j'ai vue est IFS= read var. Cependant, en utilisant read dans une boucle bash est incroyablement lent (1e3 lignes prennent 6,3 s) par rapport à head (1e6 lignes prend 1,8 s), ce qui n’est utile que pour les petits fichiers.
pascal
@pascal Oui, Bash n'est pas un outil approprié pour gérer des fichiers volumineux. J'ai ajouté une réponse avec une autre solution.
Dmitry Alexandrov
@pascal Et non! Au premier coup d’œil, j’ai manqué le point: 1000 lignes prennent 6 s (pas ms?) Pour être imprimées, vous avez dit? Vous faites quelque chose d'extrêmement mauvais.
Dmitry Alexandrov
Certaines lignes sont assez longues, je pense…
pascal
0

Mais si vous voulez écrire un script autonome pour exploiter des fichiers volumineux, AWK conviendrait beaucoup mieux que Bash pour des raisons d'efficacité. Un one-liner:

$ awk 'NR%2 { print "<< " int(NR/2) " >>" }; 1' <<< $' a \n b \n c \n d '
<< 0 >>
 a 
 b 
<< 1 >>
 c 
 d 

La même chose qu'un script:

#!/usr/bin/awk -f

# where (number of line) mod 2 == 1, i. e. every odd line
NR%2 == 1 {
    # print (number of line) div 2
    print "<< " int(NR/2) " >>"
}

{  
    # print input stream
    print
} 

La même chose qu'un script Bash:

#!/bin/bash

while read; do
    let lnum++
    ((lnum % 2 == 1)) && \
        echo "<< $((lnum / 2)) >>"
    echo "$REPLY"
done

Un repère avec un million de lignes:

$ awk 'BEGIN { for (i=1; i<=10^6; i++) print i }' >> 1e6

$ time ./pascal.awk < 1e6 > /dev/null

real    0m0.663s
user    0m0.656s
sys     0m0.004s

$ time ./pascal.sh < 1e6 > /dev/null

real    0m31.293s
user    0m29.410s
sys     0m1.852s

Vous voyez, pourquoi Bash n'est pas un interprète préférable ici.

Dmitry Alexandrov
la source