Übergeben Sie stdin an mehrere Programme, ohne sie zu schließen

1

Ich möchte eine große Datei aufteilen und GZipen und diese Antwort schien das zu sein, wonach ich suchte, und es schien mir eine sehr nützliche Möglichkeit zu sein, Dinge zu tun, an die ich nie gedacht hatte, also möchte ich es verallgemeinern; Das einzige Problem ist: Es scheint nicht zu funktionieren.

Angenommen, ich möchte meine Eingabe aufteilen und weiterverarbeiten (ich weiß split aber ich möchte es direkt in meinem Skript herumspielen!)

Dies nutzt read eine Zeile in eine Variable einlesen

#!/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

Es druckt

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

Was ist fast das, was ich will, abgesehen von der Tatsache, dass die Leerzeichen von Anfang bis Ende gekürzt werden (und die Zeile möglicherweise auf andere Weise geändert wird? Funktioniert es mit willkürlichem UTF-8-codiertem Inhalt?) bearbeiten gelöst

Und ich stelle mir vor, es könnte ziemlich langsam sein. bearbeiten Benchmarking: mindestens 3000x langsamer.

Also habe ich versucht, es durchzuleiten head (Ich bekomme das Ergebnis mit awk wie die Antwort schon sagt, scheint es nichts anders zu machen)

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

Das druckt

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

Und hört da auf head schließt offenbar seine Eingabe beim Beenden. Ich habe kein Programm gefunden, das dies nicht tut, und vielleicht wird es tatsächlich vom System durchgesetzt? (Ich bin auf OS X)

Verwenden head -n 2 <&0 was (laut den bash docs) den file descriptor zuerst kopiert, funktioniert auch nicht.

Muss ich eine Named Pipe verwenden? Gibt es eine Beschwörung, um diese Arbeit zu machen?

pascal
quelle
Woher wissen Sie, dass Leerzeichen entfernt werden? Sie sollten so etwas wie setzen echo "..$l.." um zu sehen, was in gespeichert wurde l , wie Echo ignoriert führende und nachfolgende Leerzeichen.
AFH
read streift es ab, echo ".$l." druckt .a.. Ich denke, die Shell entfernt Leerzeichen beim Aufteilen der Argumente, l=" a "; echo $l druckt a aber l=" a "; echo "$l" druckt die Leerzeichen `a`. ( read kann auch die Eingabe auch durch Leerzeichen und mehrere Variablen füllen, deshalb wahrscheinlich)
pascal
Fühlen Sie sich wohl in einer anderen Sprache wie Perl? Es wäre einfach, eine Datei einzulesen, die Zeilen zu durchlaufen und Ausgabedateien (oder interne Daten / Variablen) aufzubauen, um jeden der "Chunks" aufzunehmen. Lassen Sie mich wissen, ob Sie ein Beispiel in Perl nehmen und es so ändern können, dass Sie die Daten im Rest des Skripts "verteilen" können. Wenn Sie damit fertig werden, kann ich das anfängliche Perl schreiben, um es aufzuteilen.
jimtut
Nein, ich würde wissen, wie man es in einer anderen Sprache macht (Python für mich), aber ich hatte gehofft, dass es einen einfachen Weg gibt, dies in Bash zu tun, den ich vermisse. Einige "Programm daran hindern, stdin zu schließen" -Flag?
pascal
Es tut mir leid, einige Besucher sind eingetroffen, und ich habe versucht, meinen Kommentar zusammenzufassen, aber anscheinend zu hastig. Ich habe eine Alternative in der gefunden Linie Befehl: l = "` line '" Liest eine vollständige Zeile aus der Standardeingabe und weist sie zu l , komplett mit allen Leerzeichen. Jeder Aufruf von Linie liest eine andere Eingabezeile. Sie können dies anstelle von verwenden read l.
AFH

Antworten:

1

Das Problem ist hier nicht genau das head oder awk sind "Schließen der Eingabe". Sie haben keine Wahl; Jedes Programm schließt seine Eingabe, wenn es beendet wird. Dies wird vom Betriebssystem erzwungen.

Das Problem ist, dass die Standardeingabe eine Pipe ist und die Programme gepufferte Lesevorgänge ausführen. Es gibt keine Möglichkeit, Daten aus einer Pipe ungelesen zu lassen. Alle Daten, die sich im Readahead befinden, gehen verloren. Wenn Sie anstelle einer Pipe eine Datei verwenden, werden Sie wahrscheinlich feststellen, dass diese gut funktioniert:

#!/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

Zumindest funktioniert das auf Ubuntu gut. Sie können es mit einer Pipe zum Laufen bringen, wenn Sie die Pufferung ausschalten - aber das macht die Sache sehr langsam. Hier ist ein kleines C-Programm, das die Pufferung abschaltet und dann die Eingabe zeichenweise wiederholt, bis die angeforderte Anzahl von Zeilen verbraucht ist:

#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;
}

Das hat für mich gut funktioniert (wieder auf Ubuntu - und Sie müssen es mit kompilieren -std=c99 oder -std=c11 damit sich der Compiler nicht beschwert). Es ist wahr, dass das Programm nicht aufruft fclose(stdin), aber das Hinzufügen macht keinen Unterschied. Auf der anderen Seite entfernen Sie den Anruf an setvbuf wird Sie wahrscheinlich wieder zu dem Symptom bringen, bei dem Sie es beobachtet haben head. (Und es wird auch das Programm zum Laufen bringen Menge schneller.)

Wenn du GNU hättest split Anstelle der BSD-Version, die mit OS X ausgeliefert wird, können Sie die nützliche verwenden --filter=COMMAND Syntax, die ziemlich genau das tut, was Sie wollen; Anstatt geteilte Dateien zu erstellen, leitet er jeden Dateibereich an einen Aufruf des angegebenen Befehls weiter (und setzt die Umgebungsvariable $FILE auf den erwarteten Dateinamen).

rici
quelle
Agh, ja, dachte nicht über Pufferung nach. Ich wusste es nicht --filter; Das ist die Art von Flexibilität, die ich gesucht habe.
pascal
1

Durch Angabe einer Variablen zu read Sie bestellen es, um eine Wortteilung durchzuführen. Tun Sie das nicht, und Leerzeichen bleiben unberührt:

#!/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

Ausgabe:

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

Es scheint sehr einfach zu sein, aber tatsächlich haben Sie eine sehr gute Frage gestellt, da diese Funktion im Mann nicht klar erklärt wird.

P. S. Ich würde ein verwenden -r Flagge (nicht behandeln \ als Fluchtzeichen) für read ebenfalls.

Dmitry Alexandrov
quelle
nett, wußte nichts davon $REPLY. Die Alternative, die ich gesehen habe, ist IFS= read var. Jedoch mit read In einer Bash-Schleife ist es unglaublich langsam (1e3-Zeilen dauern 6,3 Sekunden) im Vergleich zu head (1e6-Zeilen dauern 1,8 Sekunden), daher ist es nur für kleine Dateien nützlich.
pascal
@pascal Ja, Bash ist kein geeignetes Tool, um große Dateien zu bedienen. Ich habe eine Antwort mit einer anderen Lösung hinzugefügt.
Dmitry Alexandrov
@ Pascal Und nein! Ich habe den Punkt auf den ersten Blick verpasst: 1000 Zeilen brauchen 6 Sekunden (nicht ms?), Um gedruckt zu werden, sagten Sie? Sie machen etwas extrem falsch.
Dmitry Alexandrov
Einige der Zeilen sind ziemlich lang, denke ich…
pascal
0

Wenn Sie jedoch ein eigenständiges Skript zum Bearbeiten großer Dateien schreiben möchten, ist AWK aus Effizienzgründen viel besser geeignet als Bash. Ein Einlage:

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

Das gleiche wie ein Skript:

#!/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
} 

Das gleiche wie ein Bash-Skript:

#!/bin/bash

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

Ein Benchmark mit einer Million Zeilen:

$ 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

Sie sehen, warum Bash hier kein bevorzugter Dolmetscher ist.

Dmitry Alexandrov
quelle