Wie man ein Array von Zeilen in zsh richtig sammelt

42

Ich dachte, das Folgende würde die Ausgabe von my_commandin einem Array von Zeilen gruppieren :

IFS='\n' array_of_lines=$(my_command);

Das $array_of_lines[1]würde sich auf die erste Zeile in der Ausgabe von my_command, $array_of_lines[2]auf die zweite und so weiter beziehen .

Der obige Befehl scheint jedoch nicht gut zu funktionieren. Es scheint auch die Ausgabe von my_commandum das Zeichen zu teilen n, wie ich mit überprüft habe print -l $array_of_lines, die ich glaube, druckt Elemente eines Arrays Zeile für Zeile. Ich habe dies auch überprüft mit:

echo $array_of_lines[1]
echo $array_of_lines[2]
...

In einem zweiten Versuch dachte ich, das Hinzufügen evalkönnte helfen:

IFS='\n' array_of_lines=$(eval my_command);

aber ich habe genau das gleiche Ergebnis wie ohne.

Schließlich habe ich nach der Antwort auf Listenelemente mit Leerzeichen in zsh auch versucht, Parametererweiterungsflags zu verwenden, anstatt IFSzsh mitzuteilen, wie die Eingabe aufgeteilt und die Elemente in einem Array gesammelt werden sollen, dh:

array_of_lines=("${(@f)$(my_command)}");

Aber ich habe immer noch das gleiche Ergebnis (Spaltung passiert am n)

Damit habe ich folgende Fragen:

Q1. Was sind die "richtigen" Möglichkeiten, um die Ausgabe eines Befehls in einem Array von Zeilen zu sammeln?

Q2. Wie kann ich festlegen IFS, dass nur Zeilenumbrüche geteilt werden?

Q3. Wenn ich Parametererweiterungsflags wie in meinem dritten Versuch oben (dh mithilfe von @f) verwende, um die Aufteilung festzulegen, ignoriert zsh dann den Wert von IFS? Warum hat es oben nicht funktioniert?

Amelio Vazquez-Reina
quelle

Antworten:

71

TL, DR:

array_of_lines=("${(@f)$(my_command)}")

Erster Fehler (→ Q2): IFS='\n'Stellt IFSdie beiden Zeichen \und ein n. Zum Einstellen IFSauf eine neue Zeile, verwendet IFS=$'\n'.

Zweiter Fehler: eine Variable auf einen Array Wert zu setzen, müssen Sie Klammern um die Elemente: array_of_lines=(foo bar).

Dies würde funktionieren, mit der Ausnahme, dass leere Zeilen entfernt werden, da aufeinanderfolgende Leerzeichen als einzelnes Trennzeichen gelten:

IFS=$'\n' array_of_lines=($(my_command))

Sie können die leeren Zeilen bis auf das Ende beibehalten, indem Sie das Leerzeichen in den folgenden Schritten verdoppeln IFS:

IFS=$'\n\n' array_of_lines=($(my_command))

Um auch weiterhin leere Zeilen zu verfolgen, müssen Sie der Ausgabe des Befehls etwas hinzufügen, da dies in der Befehlsersetzung selbst geschieht und nicht durch Parsen.

IFS=$'\n\n' array_of_lines=($(my_command; echo .)); unset 'array_of_lines[-1]'

(Vorausgesetzt, die Ausgabe von my_commandendet nicht in einer nicht begrenzten Zeile. Beachten Sie außerdem, dass Sie den Exit-Status von verlieren. my_command)

Beachten Sie, dass alle oben genannten Snippets IFSihren nicht standardmäßigen Wert beibehalten, sodass sie möglicherweise nachfolgenden Code durcheinander bringen. Um die Einstellung von IFSlocal beizubehalten, fügen Sie das Ganze in eine Funktion ein, in der Sie IFSlocal deklarieren (hierbei wird auch darauf geachtet, dass der Exit-Status des Befehls erhalten bleibt):

collect_lines() {
  local IFS=$'\n\n' ret
  array_of_lines=($("$@"; ret=$?; echo .; exit $ret))
  ret=$?
  unset 'array_of_lines[-1]'
  return $ret
}
collect_lines my_command

Aber ich empfehle, nicht damit herumzuspielen IFS. Verwenden Sie stattdessen das fErweiterungsflag, um in Zeilenumbrüchen zu teilen (→ Q1):

array_of_lines=("${(@f)$(my_command)}")

Oder um nachgestellte Leerzeilen beizubehalten:

array_of_lines=("${(@f)$(my_command; echo .)}")
unset 'array_of_lines[-1]'

Der Wert von IFSspielt dort keine Rolle. Ich vermute, dass Sie in Ihren Tests einen Befehl verwendet haben, der sich aufteilt, um IFSzu drucken $array_of_lines(→ Q3).

Gilles 'SO - hör auf böse zu sein'
quelle
7
Das ist so kompliziert! "${(@f)...}"ist das gleiche wie ${(f)"..."}, aber auf andere Weise. (@)in doppelten Anführungszeichen bedeutet "Ergib ein Wort pro Array-Element" und (f)bedeutet "durch Zeilenumbruch in ein Array aufteilen". PS: Bitte verlinken Sie zu den Dokumenten
fliegende Schafe
3
@flyingsheep, no ${(f)"..."}würde leere Zeilen überspringen, behält "${(@f)...}"sie bei. Das ist der gleiche Unterschied zwischen $argvund "$argv[@]". Das "$@"Ding, um alle Elemente eines Arrays zu erhalten, stammt aus der Bourne-Shell in den späten 70ern.
Stéphane Chazelas
4

Zwei Probleme: Erstens interpretieren doppelte Anführungszeichen anscheinend auch keine Backslash-Escapes (Entschuldigung :). Verwenden Sie $'...'Anführungszeichen. Und nach man zshparam, zu sammeln Wörter in einem Array müssen Sie sie in Klammern einzuschließen. Das funktioniert also:

% touch 'a b' c d 'e f'
% IFS=$'\n' arr=($(ls)); print -l $arr
a b
c
d
e f
% print $arr[1]
a b

Ich kann dein Q3 nicht beantworten. Ich hoffe, ich muss nie so esoterische Dinge wissen :).

angus
quelle
-2

Sie können auch tr verwenden, um newline durch Leerzeichen zu ersetzen:

lines=($(mycommand | tr '\n' ' '))
select line in ("${lines[@]}"); do
  echo "$line"
  break
done
Jimchao
quelle
3
Was ist, wenn die Zeilen Leerzeichen enthalten?
don_crissti
2
Das macht keinen Sinn. Sowohl SPC als auch NL haben den Standardwert von $IFS. Eine Übersetzung in die andere macht keinen Unterschied.
Stéphane Chazelas
Waren meine Änderungen angemessen? Ich konnte es nicht so zum Laufen bringen, wie es war
John P
(Zeitüberschreitung) Ich gebe zu, dass ich es bearbeitet habe, ohne wirklich zu verstehen, was die Absicht war, aber ich denke, dass die Übersetzung ein guter Anfang für die Trennung ist, die auf der Manipulation von Zeichenfolgen basiert. Ich würde nicht in Leerzeichen übersetzen und diese aufteilen, es sei denn, das erwartete Verhalten wäre eher so echo, dass alle Eingaben mehr oder weniger eine Häufung von Wörtern sind, die durch wen es interessiert, getrennt sind.
John P