Wie könnte ich zweimal in einem Durchgang grep?

7

Gibt es eine Möglichkeit, grepzweimal in der Datei zu vermeiden und die Variablen nur in einem Durchgang zu füllen? Die Datei ist klein, es ist also keine große Sache, dass ich mich nur gefragt habe, ob ich es in einem Durchgang schaffen könnte

FIRST_NAME=$(grep "$customer_id" customer-info|cut -f5 -d,)
LAST_NAME=$(grep "$customer_id" customer-info|cut -f6 -d,)
Jim
quelle
3
Wie werden die Variablen später im Code verwendet? Es kann sein, dass sie überhaupt nicht benötigt werden.
Kusalananda
4
@Kusalananda Warum möchtest du X machen? Mach kein X. Mach Y.
user1717828

Antworten:

13

Sie können einmal grep und zweimal teilen, indem Sie die Shell-Zeichenfolge ersetzen:

NAME=$(grep "$customer_id" customer-info | cut -f5,6 -d,)
FIRST_NAME=${NAME%,*}
LAST_NAME=${NAME#*,}

Oder mit Bash die Prozesssubstitution verwenden:

IFS=, read FIRST_NAME LAST_NAME < <(grep "$customer_id" customer-info | cut -f5,6 -d,)

readteilt die Eingabe auf IFSund weist den ersten Wert FIRST_NAMEund den Rest zu LAST_NAME. Prozess - Substitution und Umleitung < <(...)können Sie die Ausgabe von passieren grep ... | cut ...zu , readohne Verwendung eines Subshell.

Olorin
quelle
Dies hängt davon ab, ob die Felder sequentiell sind. Was ist, wenn es auf 5 und 7 geändert wurde?
Jim
@Jim Führen Sie dann die cutOperation nach dem nicht aus grepund verwenden Sie read, um die Zeichenfolge zu brechen, indem Sie eine benutzerdefinierte Angabe angeben IFSund die benötigten Spalten aus einem Array abrufen.
111 ---
@ Jim tut es nicht. Da Sie angeben können, mit welchen Feldern Sie möchten -f- dies -fm,nist eine Liste von Feldern, kein Bereich -, funktioniert dies auch dann, wenn die Felder nicht nebeneinander liegen. Es erfordert, dass sie in Ordnung sind, aber wenn die Reihenfolge umgekehrt ist, tauschen Sie einfach die Variablen aus.
Olorin
1
Nitpick: Bei der Prozessersetzung wird eine Unterschale verwendet - dies geschieht jedoch readin der Haupt-Shell, sodass die Variableneinstellung nicht verloren geht, während eine Pipeline something | read var vardies readin einer Unterschale abhängig von Ihrer Shell-Variante und manchmal Optionen / Modi tun kann .
Dave_thompson_085
@dave ja, natürlich bezieht sich der Punkt über Subshells nur auf den readBefehl, nicht auf die grep ... | cutPipeline, die sich in Bash immer in einer Subshell befindet, da es sich um eine Pipeline handelt.
Olorin
4

Am einfachsten wäre es, den gesamten Datensatz in eine Variable zu integrieren und diese dann zu verwenden cut.

RECORD=$(grep "$customer_id" customer-info)
FIRST_NAME=$(echo "$RECORD"|cut -f5 -d,)
LAST_NAME=$(echo "$RECORD"|cut -f6 -d,)

Auch persönlich würde ich empfehlen, einen spezifischeren regulären Ausdruck zu verwenden. Wenn sich Ihre Kunden-IDs immer am Zeilenanfang befinden, können Sie schreiben, grep '^'"$customer_id"anstatt grep "$customer_id"zu verlangen, dass die Übereinstimmung am Zeilenanfang steht. Andernfalls können Sie Datensätze abrufen, bei denen Text, der mit der Kunden-ID übereinstimmt, an anderer Stelle im Datensatz angezeigt wird.

Micheal Johnson
quelle
4

Sie können awkin Kombination mit Bash verwenden read:

read -r FIRST_NAME LAST_NAME <<< $(awk -F, -v cid="$customer_id" '$0~cid{print $5,$6}' customer-info)

-F Weist awk an, das Komma als Feldtrennzeichen zu verwenden

-vSetzt die awk-Variable cidauf die Shell-Variable$customer_id

Wenn die Zeile mit der übereinstimmt $customer_id, druckt awk das 5. und 6. Feld und diesen werden die Variablen FIRST_NAMEund zugewiesen LAST_NAME.

Wenn Vorname ($ 5) je Raum enthält (Beispiel: a, b, c, d, Sarah Jane Smith) hinzufügen -v OFS=,haben awkAusgang Komma zwischen Feldern und Präfix readmit IFS=,ihm bei comma gespalten zu haben.

Darüber hinaus awkkann nur in einem bestimmten Feld wie gesucht werden '$3~cid{print..}'- und das gesamte Feld kann mit übereinstimmen, '$3~"^"cid"$"{print...}'wenn dies für Ihre IDs von Bedeutung ist.

oliv
quelle
Funktioniert nicht wie geschrieben; Bearbeiten vorgeschlagen, mit Ergänzungen, da ich dabei bin.
Dave_thompson_085
@ dave_thompson_085 Ihre Änderungen sind gültig. Vielen Dank. Ich sehe jedoch keinen Unterschied zwischen der vorherigen Version /.../und Ihrer Version $0~.... Können Sie auch erklären, was bei Ihnen nicht funktioniert hat?
Oliv
Wenn Sie nur cidals Muster verwenden, stimmt awk nicht mit der Zeile mit der regulären Ausdrucks-CID überein. Es wird nur geprüft, ob die Variable cid nicht leer ist, obwohl dies immer der Fall ist. Daher gibt awk alle Zeilen aus, nicht die gewünschte einzelne Zeile. Deshalb müssen Sie $0 ~ cid- die Linie ($ 0) mit dem regulären Ausdruck in cid abgleichen.
Dave_thompson_085
Wenn Sie eine wörtliche regexp schreiben /regexp/ {action}die Schrägstriche sind nicht Teil des regulären Ausdruck, sie sind spezielle Syntax , die sagt , es ist ein regulärer Ausdruck. Wenn Sie den regulären Ausdruck in einer Variablen setzen , wenn Sie einen Schrägstrich enthalten , dass Slash ist ein Datum Zeichen , das die Eingabedaten übereinstimmen muß (hier neben dem customer_id Wert) , die es wird mit ziemlicher Sicherheit nicht (obwohl möglicherweise könnte die OP - Daten hat gezeigt, dass hat immer Schrägstriche um die Werte von customer_id).
Dave_thompson_085
2

Kleine Datei, große Datei. Eine Angewohnheit, die ich habe, besteht darin, Festplatten-E / A immer so weit wie möglich zu entfernen. Eine Möglichkeit, dies zu tun, besteht darin, die Datei in ein Array zu verschieben. Dies setzt natürlich voraus, dass env $ IFS für die Datei entsprechend eingestellt ist, eliminiert jedoch die E / A.

data=( $(cat customer-info) )

Dann können Sie daraus Kirsche pflücken ...

FIRST_NAME=$(echo "${data[@]}" | tr ' ' '\n' | grep "$customer_id" | cut -f5 -d,)

Eine andere Methode könnte darin bestehen, einem Array wie diesem nur die beiden gewünschten Bits zuzuweisen ...

data=( $(grep "${customer_id}" customer-info | cut -d, -f5,6) )

jas-
quelle
6
-1 zum Schlürfen des gesamten Datensatzes in eine Bash-Variable, obwohl der Filter vorher bekannt ist.
David Foerster
1
Woher wissen Sie, dass für die Operation keine zusätzlichen Daten erforderlich sind? Nehmen Sie nichts an und eliminieren Sie so viel Overhead wie möglich für zukünftige Operationen. Vielen Dank für die Besorgnis und Meinung jedoch
jas-
Erinnert mich an den Typ, der zwei SQL-Tabellen in eine Variale kopiert, sie dann in einem Eins-zu-Viele-Feld referenziert und dann den einzelnen Eintrag ausgewählt hat, den sie benötigen. Aber ich kannte ASP nicht, also wen sollte ich kritisieren?
Grump
Weee, dies war der konstruktivste Dialog, den ich jemals geführt habe. Jemand hat den Brunnen vergiftet.
Jas-
0

Die vorhandenen Antworten speichern alle die Ausgabe im Speicher (in einer Variablen) und spielen sie zweimal ab. Dies ist ein Problem, wenn Sie einen generischen Wrapper erstellen möchten, der eine beliebig große Eingabe annehmen und zwei Aufgaben ausführen kann. Stattdessen kann der Ausgabestream dupliziert und in zwei Befehle gestreamt werden.

In meinem Fall besteht der Zweck darin, sowohl den Header (erste Zeile) als auch eine bestimmte (Gruppe von) Zeile (n) in einem Ausgabestream zu filtern, der beliebig lang sein kann. Ein einfaches Beispiel wäre die Anzeige der Speicherplatznutzung:

$ df -h | tee >(head -1 >&2) | grep '/$'
Filesystem    Size  Used Avail Use% Mounted on
/dev/sda1     202G  145G   57G  72% /

Ersetzen Sie df -hmit dem Befehl , den Sie verwenden möchten, und ersetzen head -1und grep '/$'mit den beiden Befehlen möchten Sie sie anzuwenden. Die Ausgabe von beiden wird in Ihrem Terminal angezeigt, obwohl möglicherweise die Ausgabe des ersteren Befehls nach dem letzteren angezeigt wird.

Wie funktioniert das?

  • Das Programm tee"[kopiert] die Standardeingabe in jedes [Argument] und auch in die Standardausgabe." So kann es die Ausgabe von stdin sowohl an stdout als auch an stderr senden, indem es verwendet command | tee /dev/stderr.
  • Die command >(command2)Syntax wird durch ein Argument durch bash ersetzt und command /dev/fd/63wird ausgeführt. Wenn commandversucht wird, darauf zu schreiben /dev/fd/63, landet es in der Eingabe (stdin) von command2. Dies wird als Prozesssubstitution bezeichnet (siehe man bash).
  • Da teesowohl in das Argument (wir übergeben eine Befehlssubstitution als Argument) als auch in stdout geschrieben wird, können wir einfach eine weitere Pipe hinzufügen und einen weiteren Befehl ausführen. Also jetzt haben wir command | tee >(command2) | command3.
  • Da Befehl2 an stdout ausgegeben wird und stdout an weitergeleitet wird command3, würden wir (in meinem Beispiel) die Kopfzeile erfassen. Das wollen wir nicht: Wir wollen es anzeigen. Da wir stderr nicht durchleiten, ist die Umleitung der Ausgabe zu stderr eine einfache Möglichkeit, sie in unserem Terminal anzuzeigen, dh wir fügen hinzu >&2, was dazu führt command | tee >(command2 >&2) | command3.

Es gibt ein Problem: Die Ausgabe kann in beliebiger Reihenfolge erfolgen. Abhängig von der kosmischen Strahlung können wir entweder das Obige oder das Folgende sehen:

$ df -h | tee >(head -1 >&2) | grep '/$'
/dev/sda1     202G  145G   57G  72% /
Filesystem    Size  Used Avail Use% Mounted on

Eine hackige, aber zuverlässige Möglichkeit, dies zu beheben (anstelle einer überentwickelten Methode, die nicht hackig ist), besteht darin, dem zweiten Befehl einen kurzen Ruhezustand hinzuzufügen. etwas wie:

$ df -h | tee >(head -1 >&2) | sleep 1; grep '/$'

Aber warten Sie , das bricht den zweiten Befehl ( grep), weil jetzt die Ausgabe von geleitet wird teezu sleepund grepwird für die Eingabe auf unbestimmte Zeit warten. Um dies zu beheben, fügen wir eine Unterschale hinzu:

$ df -h | tee >(head -1 >&2) | (sleep 0.01; grep '/$')
Filesystem    Size  Used Avail Use% Mounted on
/dev/sda1     202G  145G   57G  72% /

Jetzt wird die Ausgabe nicht zu, grepsondern zu unserer Subshell umgeleitet . Da sleepes nicht daraus liest (es verbraucht den Stream nicht), steht es weiterhin grepzum Lesen zur Verfügung. Jetzt funktioniert es zuverlässig, solange die headAusgabe innerhalb von 0,01 Sekunden erfolgt (plus ein wenig Overhead auf der Grep-Seite). Dies ist eine faire Wette auf ein modernes System und kurz genug, um für den Benutzer nicht erkennbar zu sein.

Da ich etwas machen wollte, das sowohl den Header als auch die Ausgabe eines Befehls benötigt, können wir dies verallgemeinern auf:

function grabheader {
    tee >(head -1 >&2)
}

Da der teeBefehl in der Funktion nur von stdin liest und an stdout ausgibt, funktioniert dies genauso wie unser früherer Befehl außerhalb der Reihenfolge, wenn Sie ihn als verwenden df -h | grabheader | grep '/$'. Aber da wir wollen, dass es in Ordnung ist, müssen wir es verzögern, es über den Standard zu senden:

function grabheader {
    tee >(head -1 >&2) | (sleep 0.01; cat)
}

cathier wird nur sichergestellt, dass alles, was an den stdin übergeben wird, wieder auf den stdout gelangt. Wenn Sie keine Argumente übergeben und keine Umleitungen hinzufügen, wird genau das getan. Verwendungszweck:

$ df -h | grabheader | grep '/$'
Filesystem    Size  Used Avail Use% Mounted on
/dev/sda1     202G  145G   57G  72% /

Im speziellen Fall von dfkann dies natürlich viel einfacher gemacht werden:

$ df -h /
Filesystem    Size  Used Avail Use% Mounted on
/dev/sda1     202G  145G   57G  72% /

Aber jetzt haben wir eine allgemeine Möglichkeit, dies mit jedem Befehl zu tun.

Luc
quelle