Lesen der Ausgabe eines Befehls in ein Array in Bash

109

Ich muss die Ausgabe eines Befehls in meinem Skript in ein Array lesen. Der Befehl lautet zum Beispiel:

ps aux | grep | grep | x 

und es gibt die Ausgabe Zeile für Zeile wie folgt:

10
20
30

Ich muss die Werte aus der Befehlsausgabe in ein Array lesen, und dann werde ich einige Arbeiten ausführen, wenn die Größe des Arrays weniger als drei beträgt.

barp
quelle
5
Hey @barp, BEANTWORTEN SIE IHRE FRAGEN, damit Ihr Typ nicht die gesamte Community belastet.
James
9
@ James das Problem ist nicht mit der Tatsache, dass er seine Frage nicht beantwortet ... dies ist eine Q / A-Site. Er hat sie einfach nicht als beantwortet markiert . Er sollte sie markieren. Hinweis. @ barp
DDPWNAGE
4
Bitte @barp, markieren Sie die Frage als beantwortet.
Smonff
Verwandte Themen : Durchlaufen des Inhalts einer Datei in Bash, da das Lesen der Ausgabe eines Befehls durch Prozessersetzung dem Lesen aus einer Datei ähnelt.
Codeforester

Antworten:

159

Die anderen Antworten werden brechen , wenn Ausgabe von Kommando Leerzeichen enthält (was ziemlich häufig ist) oder glob Zeichen wie *, ?, [...].

Um die Ausgabe eines Befehls in einem Array mit einer Zeile pro Element zu erhalten, gibt es im Wesentlichen drei Möglichkeiten:

  1. Mit Bash≥4 ist mapfilees am effizientesten:

    mapfile -t my_array < <( my_command )
  2. Ansonsten eine Schleife, die die Ausgabe liest (langsamer, aber sicher):

    my_array=()
    while IFS= read -r line; do
        my_array+=( "$line" )
    done < <( my_command )
    
  3. Wie von Charles Duffy in den Kommentaren vorgeschlagen (danke!), Könnte Folgendes besser sein als die Schleifenmethode in Nummer 2:

    IFS=$'\n' read -r -d '' -a my_array < <( my_command && printf '\0' )

    Bitte stellen Sie sicher, dass Sie genau dieses Formular verwenden, dh stellen Sie sicher, dass Sie Folgendes haben:

    • IFS=$'\n' in derselben Zeile wie die readAnweisung: Dadurch wird nur die Umgebungsvariable IFS für die readAnweisung festgelegt. Es wirkt sich also überhaupt nicht auf den Rest Ihres Skripts aus. Der Zweck dieser Variablen besteht darin, anzugeben read, dass der Stream beim EOL-Zeichen unterbrochen werden soll \n.
    • -r: das ist wichtig. Es wird empfohlen read , die Backslashes nicht als Escape-Sequenzen zu interpretieren.
    • -d '': Bitte beachten Sie den Abstand zwischen der -dOption und ihrem Argument ''. Wenn Sie hier kein Leerzeichen lassen, ''wird das nie angezeigt, da es im Schritt zum Entfernen von Anführungszeichen verschwindet, wenn Bash die Anweisung analysiert. Dies weist readan, das Lesen beim Null-Byte zu beenden. Einige Leute schreiben es als -d $'\0', aber es ist nicht wirklich notwendig. -d ''ist besser.
    • -a my_arrayweist readan, das Array my_arraybeim Lesen des Streams zu füllen.
    • Sie müssen die printf '\0'Anweisung nach verwenden my_command , damit readzurückgegeben wird 0. Es ist eigentlich keine große Sache, wenn Sie dies nicht tun (Sie erhalten nur einen Rückkehrcode 1, der in Ordnung ist, wenn Sie ihn nicht verwenden set -e- was Sie sowieso nicht sollten), aber denken Sie daran. Es ist sauberer und semantisch korrekter. Beachten Sie, dass dies anders ist als das printf '', was nichts ausgibt. printf '\0'Gibt ein Null-Byte aus, das benötigt wird read, um dort glücklich mit dem Lesen aufzuhören (erinnern Sie sich an die -d ''Option?).

Wenn Sie können, dh wenn Sie sicher sind, dass Ihr Code auf Bash≥4 ausgeführt wird, verwenden Sie die erste Methode. Und Sie können sehen, dass es auch kürzer ist.

Wenn Sie verwenden möchten, readhat die Schleife (Methode 2) möglicherweise einen Vorteil gegenüber Methode 3, wenn Sie beim Lesen der Zeilen eine Verarbeitung durchführen möchten: Sie haben direkten Zugriff darauf (über die $lineVariable in dem von mir angegebenen Beispiel) und Sie haben auch Zugriff auf die bereits gelesenen Zeilen (über das Array ${my_array[@]}in dem Beispiel, das ich gegeben habe).

Beachten Sie, dass dies mapfileeine Möglichkeit bietet, einen Rückruf für jede gelesene Zeile auswerten zu lassen. Sie können sogar festlegen, dass dieser Rückruf nur alle N gelesenen Zeilen aufgerufen wird . Schauen Sie sich help mapfiledie Optionen -Cund die -cdarin enthaltenen an. (Meine Meinung dazu ist, dass es ein bisschen klobig ist, aber manchmal verwendet werden kann, wenn Sie nur einfache Dinge zu tun haben - ich verstehe nicht wirklich, warum dies überhaupt implementiert wurde!).


Jetzt werde ich Ihnen erklären, warum die folgende Methode:

my_array=( $( my_command) )

ist kaputt, wenn Leerzeichen vorhanden sind:

$ # I'm using this command to test:
$ echo "one two"; echo "three four"
one two
three four
$ # Now I'm going to use the broken method:
$ my_array=( $( echo "one two"; echo "three four" ) )
$ declare -p my_array
declare -a my_array='([0]="one" [1]="two" [2]="three" [3]="four")'
$ # As you can see, the fields are not the lines
$
$ # Now look at the correct method:
$ mapfile -t my_array < <(echo "one two"; echo "three four")
$ declare -p my_array
declare -a my_array='([0]="one two" [1]="three four")'
$ # Good!

Dann empfehlen einige Leute, das Problem IFS=$'\n'zu beheben:

$ IFS=$'\n'
$ my_array=( $(echo "one two"; echo "three four") )
$ declare -p my_array
declare -a my_array='([0]="one two" [1]="three four")'
$ # It works!

Aber jetzt verwenden wir einen anderen Befehl mit Globs :

$ echo "* one two"; echo "[three four]"
* one two
[three four]
$ IFS=$'\n'
$ my_array=( $(echo "* one two"; echo "[three four]") )
$ declare -p my_array
declare -a my_array='([0]="* one two" [1]="t")'
$ # What?

Das liegt daran, dass ich eine Datei tim aktuellen Verzeichnis habe… und dieser Dateiname mit dem Glob übereinstimmt [three four]… an dieser Stelle würden einige Leute empfehlen set -f, Globbing zu deaktivieren. Aber sehen Sie es sich an: Sie müssen es ändern IFSund verwenden set -f, um a zu reparieren kaputte Technik (und Sie reparieren sie nicht einmal wirklich)! Dabei kämpfen wir wirklich gegen die Shell und arbeiten nicht mit der Shell .

$ mapfile -t my_array < <( echo "* one two"; echo "[three four]")
$ declare -p my_array
declare -a my_array='([0]="* one two" [1]="[three four]")'

hier arbeiten wir mit der Shell!

gniourf_gniourf
quelle
4
Das ist großartig, ich habe noch nie davon gehört mapfile, es ist genau das, was ich seit Jahren vermisst habe. Ich denke, die neuesten Versionen von bashhaben so viele nette neue Funktionen, dass ich nur ein paar Tage damit verbringen sollte, die Dokumente zu lesen und ein schönes Cheatsheet aufzuschreiben.
Gene Pavlovsky
5
Übrigens, um diese Syntax < <(command)in Shell-Skripten zu verwenden, sollte die Shebang-Zeile lauten #!/bin/bash- wenn sie als ausgeführt wird #!/bin/sh, wird bash mit einem Syntaxfehler beendet.
Gene Pavlovsky
1
In Erweiterung der hilfreichen Anmerkung von @ GenePavlovsky muss das Skript auch mit dem Befehl bash bash my_script.shund nicht mit dem Befehl sh ausgeführt werdensh my_script.sh
Vito
2
@Vito: in der Tat, diese Antwort ist nur für Bash, aber das sollte kein Problem sein, da streng konforme POSIX Schalen auch nicht Arrays implementieren ( shund dashnicht wissen , über Arrays überhaupt, außer natürlich, für das Positionsparameter- $@Array).
gniourf_gniourf
3
Als weitere Alternative, für die Bash 4.0 nicht erforderlich ist, sollten Sie berücksichtigen, IFS=$'\n' read -r -d '' -a my_array < <(my_command && printf '\0')dass beide in Bash 3.x ordnungsgemäß funktionieren und auch einen fehlgeschlagenen Exit-Status von my_commandbis zum durchlaufen read.
Charles Duffy
87

Sie können verwenden

my_array=( $(<command>) )

um die Ausgabe des Befehls <command>im Array zu speichern my_array.

Sie können mit auf die Länge dieses Arrays zugreifen

my_array_length=${#my_array[@]}

Jetzt wird die Länge in gespeichert my_array_length.

Michael Schlottke-Lakemper
quelle
18
Was ist, wenn die Ausgabe von $ (Befehl) Leerzeichen und mehrere Zeilen mit Leerzeichen enthält? Ich habe "$ (Befehl)" hinzugefügt und alle Ausgaben aller Zeilen werden in das erste [0] -Element des Arrays eingefügt.
Ikwyl6
3
@ ikwyl6 Eine Problemumgehung besteht darin, die Befehlsausgabe einer Variablen zuzuweisen und dann ein Array damit zu erstellen oder sie einem Array hinzuzufügen. VAR="$(<command>)"und dann my_array=("$VAR")odermy_array+=("$VAR")
Vito
10

Stellen Sie sich vor, Sie werden die Dateien und Verzeichnisnamen (unter dem aktuellen Ordner) in ein Array einfügen und dessen Elemente zählen. Das Skript wäre wie;

my_array=( `ls` )
my_array_length=${#my_array[@]}
echo $my_array_length

Sie können dieses Array auch durchlaufen, indem Sie das folgende Skript hinzufügen:

for element in "${my_array[@]}"
do
   echo "${element}"
done

Bitte beachten Sie, dass dies das Kernkonzept ist und die Eingabe zuvor als bereinigt betrachtet wird, dh zusätzliche Zeichen entfernen, leere Zeichenfolgen behandeln usw. (was nicht zum Thema dieses Threads gehört).

Youness
quelle
2
Schreckliche Idee aus den in der obigen Antwort genannten Gründen
Hubert Grzeskowiak