920 Stimmen

Wie teilt man eine Zeichenkette in ein Array in Bash?

In einem Bash-Skript möchte ich eine Zeile in Teile aufteilen und diese in einem Array speichern.

Zum Beispiel bei der Zeile:

Paris, France, Europe

Ich möchte, dass das resultierende Array wie folgt aussieht:

array[0] = Paris
array[1] = France
array[2] = Europe

Eine einfache Umsetzung ist vorzuziehen; die Geschwindigkeit spielt keine Rolle. Wie kann ich das tun?

1487voto

Dennis Williamson Punkte 322329
IFS=', ' read -r -a array <<< "$string"

Beachten Sie, dass die Zeichen in $IFS werden einzeln als Trennzeichen behandelt, so dass in diesem Fall die Felder getrennt werden können durch entweder ein Komma oder ein Leerzeichen und nicht die Reihenfolge der beiden Zeichen. Interessanterweise werden jedoch keine leeren Felder erstellt, wenn ein Komma oder ein Leerzeichen in der Eingabe erscheint, da das Leerzeichen speziell behandelt wird.

Um auf ein einzelnes Element zuzugreifen:

echo "${array[0]}"

Um über die Elemente zu iterieren:

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

Um sowohl den Index als auch den Wert zu erhalten:

for index in "${!array[@]}"
do
    echo "$index ${array[index]}"
done

Das letzte Beispiel ist nützlich, weil Bash-Arrays spärlich sind. Mit anderen Worten, Sie können ein Element löschen oder ein Element hinzufügen und dann sind die Indizes nicht zusammenhängend.

unset "array[1]"
array[42]=Earth

Um die Anzahl der Elemente in einem Array zu ermitteln:

echo "${#array[@]}"

Wie oben erwähnt, können Arrays spärlich sein, so dass Sie die Länge nicht verwenden sollten, um das letzte Element zu erhalten. Hier ist, wie Sie in Bash 4.2 und höher können:

echo "${array[-1]}"

in jeder Version der Bash (ab 2.05b):

echo "${array[@]: -1:1}"

Größere negative Offsets wählen weiter vom Ende des Arrays entfernt. Beachten Sie das Leerzeichen vor dem Minuszeichen in der älteren Form. Es ist erforderlich.

670voto

bgoldst Punkte 32246

Alle Antworten auf diese Frage sind auf die eine oder andere Weise falsch.


Falsche Antwort #1

IFS=', ' read -r -a array <<< "$string"

1: Dies ist eine missbräuchliche Verwendung von $IFS . Der Wert der $IFS Variable ist no angenommen als einfache variable Länge Stringtrennzeichen, sondern wird als einstellen. von Einzelbuchstaben Stringtrennzeichen, wobei jedes Feld, das read von der Eingangsleitung abzweigt, kann durch jede Zeichen in der Menge (Komma o Raum, in diesem Beispiel).

Für die ganz Hartgesottenen da draußen: Die volle Bedeutung von $IFS ist etwas komplizierter. Vom Bash-Handbuch :

Die Shell behandelt jedes Zeichen von IFS als Begrenzungszeichen und teilt die Ergebnisse der anderen Expansionen in Wörter auf, wobei diese Zeichen als Feldbegrenzer verwendet werden. Wenn IFS nicht gesetzt ist, oder sein Wert ist genau <space><tab><newline> die Standardeinstellung, dann werden Sequenzen von <Raum> , <tab> y <Newline> am Anfang und am Ende der Ergebnisse der vorangegangenen Expansionen werden ignoriert, und jede Folge von IFS Zeichen, die nicht am Anfang oder Ende stehen, dienen der Abgrenzung von Wörtern. Wenn IFS einen anderen Wert als den Standardwert hat, dann werden Sequenzen von Leerzeichen <Raum> , <tab> y <Newline> werden am Anfang und am Ende des Wortes ignoriert, solange das Leerzeichen im Wert von IFS (an IFS Whitespace-Zeichen). Jedes Zeichen in IFS das ist nicht IFS Leerzeichen, zusammen mit allen angrenzenden IFS Leerzeichen, grenzt ein Feld ab. Eine Folge von IFS Leerzeichen werden ebenfalls als Begrenzungszeichen behandelt. Wenn der Wert von IFS Null ist, findet keine Worttrennung statt.

Grundsätzlich gilt, dass für nicht standardmäßige Nicht-Null-Werte von $IFS können Felder entweder mit (1) einer Folge von einem oder mehreren Zeichen getrennt werden, die alle aus dem Satz der "IFS-Whitespace-Zeichen" stammen (d. h. je nachdem, welches der folgenden Zeichen <Raum> , <tab> y <Newline> ("Zeilenumbruch" bedeutet Zeilenvorschub (LF) ) sind überall in $IFS ), oder (2) jedes Nicht-IFS-Whitespace-Zeichen, das in $IFS zusammen mit allen "IFS-Whitespace-Zeichen", die es in der Eingabezeile umgeben.

Für den Auftraggeber ist es möglich, dass der zweite Trennungsmodus, den ich im vorigen Absatz beschrieben habe, genau das ist, was er für seine Eingabezeichenfolge will, aber wir können ziemlich sicher sein, dass der erste Trennungsmodus, den ich beschrieben habe, überhaupt nicht korrekt ist. Was wäre zum Beispiel, wenn seine Eingabezeichenfolge wäre 'Los Angeles, United States, North America' ?

IFS=', ' read -ra a <<<'Los Angeles, United States, North America'; declare -p a;
## declare -a a=([0]="Los" [1]="Angeles" [2]="United" [3]="States" [4]="North" [5]="America")

2: Selbst wenn Sie diese Lösung mit einem Ein-Zeichen-Trennzeichen (z. B. einem Komma für sich, d. h. ohne nachfolgendes Leerzeichen oder anderen Ballast) verwenden würden, würde der Wert der Option $string Variable zufällig irgendwelche LFs enthält, dann read bricht die Verarbeitung ab, sobald es auf den ersten LF trifft. Die read verarbeitet nur eine Zeile pro Aufruf. Dies gilt auch dann, wenn Sie Eingaben über die Pipeline weiterleiten oder umleiten sólo zum read Anweisung, wie wir es in diesem Beispiel mit der here-string Mechanismus, so dass unbearbeitete Eingaben garantiert verloren gehen. Der Code, mit dem die read builtin hat keine Kenntnis vom Datenfluss innerhalb der enthaltenen Befehlsstruktur.

Man könnte argumentieren, dass dies wahrscheinlich kein Problem darstellt, aber dennoch ist es eine subtile Gefahr, die nach Möglichkeit vermieden werden sollte. Sie wird durch die Tatsache verursacht, dass die read builtin teilt die Eingabe auf zwei Ebenen auf: zuerst in Zeilen, dann in Felder. Da der Auftraggeber nur eine Ebene der Aufteilung wünscht, ist diese Verwendung des read ist nicht angemessen und sollte vermieden werden.

3: Ein nicht offensichtliches mögliches Problem bei dieser Lösung ist, dass read lässt das letzte Feld immer weg, wenn es leer ist, obwohl es ansonsten leere Felder beibehält. Hier ist eine Demo:

string=', , a, , b, c, , , '; IFS=', ' read -ra a <<<"$string"; declare -p a;
## declare -a a=([0]="" [1]="" [2]="a" [3]="" [4]="b" [5]="c" [6]="" [7]="")

Vielleicht ist dies dem Auftraggeber egal, aber es ist dennoch eine Einschränkung, die man kennen sollte. Sie verringert die Robustheit und Allgemeinheit der Lösung.

Dieses Problem kann gelöst werden, indem ein Dummy-Begrenzer am Ende der Eingabezeichenkette angehängt wird, bevor diese an read , wie ich später noch zeigen werde.


Falsche Antwort #2

string="1:2:3:4:5"
set -f                     # avoid globbing (expansion of *).
array=(${string//:/ })

Ähnliche Idee:

t="one,two,three"
a=($(echo $t | tr ',' "\n"))

(Hinweis: Ich habe die fehlenden Klammern um die Befehlssubstitution hinzugefügt, die der Antwortende offenbar ausgelassen hat).

Ähnliche Idee:

string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)

Diese Lösungen nutzen die Worttrennung in einer Array-Zuweisung, um die Zeichenfolge in Felder aufzuteilen. Seltsamerweise, genau wie read verwendet die allgemeine Worttrennung auch die $IFS spezielle Variable, obwohl in diesem Fall davon ausgegangen wird, dass sie auf ihren Standardwert von <space><tab><newline> und daher wird jede Folge von einem oder mehreren IFS-Zeichen (die jetzt alle Leerzeichen sind) als Feldbegrenzer betrachtet.

Dies löst das Problem der Aufspaltung auf zwei Ebenen, die von read da die Worttrennung an sich nur eine Ebene der Aufteilung darstellt. Aber auch hier besteht das Problem darin, dass die einzelnen Felder der Eingabezeichenfolge bereits enthalten können $IFS Zeichen und würden daher bei der Worttrennung nicht korrekt getrennt werden. Dies ist zufällig bei keiner der Beispieleingabezeichenfolgen der Fall, die von diesen Beantwortern zur Verfügung gestellt werden (wie praktisch...), aber das ändert natürlich nichts an der Tatsache, dass jede Codebasis, die dieses Idiom verwendet, Gefahr läuft, zu explodieren, wenn diese Annahme irgendwann einmal verletzt wird. Noch einmal, betrachten Sie mein Gegenbeispiel von 'Los Angeles, United States, North America' (o 'Los Angeles:United States:North America' ).

Außerdem wird die Worttrennung normalerweise gefolgt von Dateinamenerweiterung ( alias Pfadnamen-Erweiterung alias globbing), die, wenn sie durchgeführt wird, möglicherweise Wörter beschädigen würde, die die Zeichen * , ? o [ gefolgt von ] (und, falls extglob gesetzt ist, werden eingeklammerte Fragmente mit vorangestelltem ? , * , + , @ o ! ), indem sie mit Dateisystemobjekten abgeglichen und die Wörter ("Globs") entsprechend erweitert werden. Der erste dieser drei Antwortenden hat dieses Problem geschickt umgangen, indem er set -f vorher, um das Globbing zu deaktivieren. Technisch gesehen funktioniert dies (obwohl Sie wahrscheinlich hinzufügen sollten set +f um Globbing für nachfolgenden Code, der davon abhängt, wieder zu aktivieren), aber es ist nicht wünschenswert, mit globalen Shell-Einstellungen herumspielen zu müssen, um eine grundlegende String-zu-Array-Parsing-Operation in lokalem Code zu hacken.

Ein weiteres Problem bei dieser Antwort ist, dass alle leeren Felder verloren gehen. Dies kann je nach Anwendung ein Problem darstellen oder auch nicht.

Hinweis: Wenn Sie diese Lösung verwenden, ist es besser, die ${string//:/ } Form der "Mustersubstitution" von Parametererweiterung anstatt sich die Mühe zu machen, eine Befehlssubstitution aufzurufen (die die Shell aufspaltet), eine Pipeline zu starten und eine externe ausführbare Datei auszuführen ( tr o sed ), da die Parametererweiterung eine rein shellinterne Operation ist. (Auch für die tr y sed Lösungen sollte die Eingabevariable innerhalb der Befehlssubstitution in Anführungszeichen gesetzt werden; andernfalls würde die Worttrennung in der echo und möglicherweise die Feldwerte durcheinander bringen. Außerdem ist der $(...) Form der Befehlsersetzung dem alten Verfahren vorzuziehen ist `...` Form, da sie die Verschachtelung von Befehlssubstitutionen vereinfacht und eine bessere Syntaxhervorhebung durch Texteditoren ermöglicht).


Falsche Antwort #3

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

Diese Antwort ist fast die gleiche wie #2 . Der Unterschied besteht darin, dass der Beantworter davon ausgeht, dass die Felder durch zwei Zeichen getrennt sind, von denen eines im Standard $IFS und das andere nicht. Er hat diesen recht speziellen Fall gelöst, indem er das nicht IFS-repräsentierte Zeichen mit Hilfe einer Mustersubstitutionserweiterung entfernte und dann die Felder mit Hilfe von Wortsplitting auf das verbleibende IFS-repräsentierte Begrenzungszeichen aufteilte.

Dies ist keine sehr generische Lösung. Außerdem kann man argumentieren, dass das Komma hier wirklich das "primäre" Trennzeichen ist und dass es einfach falsch ist, es zu entfernen und sich dann auf das Leerzeichen für die Feldaufteilung zu verlassen. Betrachten Sie noch einmal mein Gegenbeispiel: 'Los Angeles, United States, North America' .

Auch hier könnte die Dateinamenexpansion die expandierten Wörter beschädigen, aber das kann verhindert werden, indem man das Globbing für die Zuweisung mit set -f und dann set +f .

Außerdem gehen wieder alle leeren Felder verloren, was je nach Anwendung ein Problem darstellen kann oder auch nicht.


Falsche Antwort #4

string='first line
second line
third line'

oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"

Dies ist vergleichbar mit #2 y #3 in dem er die Worttrennung verwendet, um die Aufgabe zu erledigen, nur dass der Code jetzt ausdrücklich die $IFS nur das einstellige Feldbegrenzungszeichen enthalten, das in der Eingabezeichenfolge enthalten ist. Es sollte noch einmal darauf hingewiesen werden, dass dies bei mehrstelligen Feldbegrenzern wie dem Komma-Leerzeichen-Begrenzer aus dem OP nicht funktioniert. Aber für ein einstelliges Begrenzungszeichen wie das in diesem Beispiel verwendete LF ist es nahezu perfekt. Die Felder können nicht versehentlich in der Mitte geteilt werden, wie wir es bei früheren falschen Antworten gesehen haben, und es gibt nur eine Ebene der Aufteilung, wie erforderlich.

Ein Problem ist, dass die Dateinamenexpansion die betroffenen Wörter beschädigt, wie zuvor beschrieben, obwohl auch dies gelöst werden kann, indem die kritische Anweisung in set -f y set +f .

Ein weiteres potenzielles Problem ist, dass, da LF als "IFS-Whitespace-Zeichen" gilt, wie zuvor definiert, alle leeren Felder verloren gehen, genau wie in #2 y #3 . Dies wäre natürlich kein Problem, wenn das Trennzeichen kein "IFS-Whitespace-Zeichen" ist, und je nach Anwendung spielt es vielleicht ohnehin keine Rolle, aber es beeinträchtigt die Allgemeinheit der Lösung.

Zusammenfassend lässt sich also sagen, dass Sie ein Ein-Zeichen-Begrenzungszeichen haben und es entweder ein Nicht-IFS-Whitespace-Zeichen ist oder Sie sich nicht um leere Felder kümmern und Sie die kritische Anweisung in set -f y set +f dann funktioniert diese Lösung, ansonsten aber nicht.

(Zur Information: Das Zuweisen eines LF an eine Variable in der Bash lässt sich einfacher mit der $'...' Syntax, z.B. IFS=$'\n'; .)


Falsche Antwort #5

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

Ähnliche Idee:

IFS=', ' eval 'array=($string)'

Diese Lösung ist praktisch eine Kreuzung aus #1 (indem es die $IFS zu Komma-Leerzeichen) und #2-4 (indem es die Zeichenkette durch Worttrennung in Felder aufteilt). Aus diesem Grund leidet sie an den meisten Problemen, die alle oben genannten falschen Antworten betreffen, sozusagen an der schlechtesten aller Welten.

Was die zweite Variante betrifft, so mag es so aussehen, als ob die eval Aufruf ist völlig unnötig, da sein Argument ein String-Literal in einfachen Anführungszeichen ist und daher statisch bekannt ist. Aber es gibt tatsächlich einen sehr nicht offensichtlichen Vorteil bei der Verwendung von eval auf diese Weise. Normalerweise, wenn Sie einen einfachen Befehl ausführen, der aus einer Variablenzuweisung besteht sólo , d.h. ohne dass ein eigentliches Befehlswort folgt, wird die Zuweisung in der Shell-Umgebung wirksam:

IFS=', '; ## changes $IFS in the shell environment

Dies gilt auch dann, wenn der einfache Befehl mehrere Variablenzuweisungen; auch hier wirken sich alle Variablenzuweisungen auf die Shell-Umgebung aus, solange es kein Befehlswort gibt:

IFS=', ' array=($countries); ## changes both $IFS and $array in the shell environment

Wenn die Variablenzuweisung jedoch an einen Befehlsnamen angehängt ist (ich nenne dies gerne "Präfix-Zuweisung"), dann ist sie no auf die Shell-Umgebung auswirken, sondern nur auf die Umgebung des ausgeführten Befehls, unabhängig davon, ob es sich um ein builtin oder ein externes Kommando handelt:

IFS=', ' :; ## : is a builtin command, the $IFS assignment does not outlive it
IFS=', ' env; ## env is an external command, the $IFS assignment does not outlive it

Einschlägiges Zitat aus der Bash-Handbuch :

Wenn sich kein Befehlsname ergibt, wirken sich die Variablenzuweisungen auf die aktuelle Shell-Umgebung aus. Andernfalls werden die Variablen der Umgebung des ausgeführten Befehls hinzugefügt und wirken sich nicht auf die aktuelle Shell-Umgebung aus.

Es ist möglich, diese Eigenschaft der Variablenzuweisung auszunutzen, um die $IFS nur vorübergehend, was es uns ermöglicht, das ganze Speichern-und-Wiederherstellen-Gambit zu vermeiden, wie es mit dem $OIFS Variable in der ersten Variante. Die Herausforderung besteht darin, dass der auszuführende Befehl selbst eine bloße Variablenzuweisung ist und daher kein Befehlswort erforderlich ist, um die $IFS befristeter Auftrag. Vielleicht denken Sie sich, warum nicht einfach ein No-Op-Befehlswort an die Anweisung anhängen, wie das : builtin um die $IFS Zuweisung vorübergehend? Das funktioniert nicht, denn dann würde die $array Zuweisung auch vorübergehend:

IFS=', ' array=($countries) :; ## fails; new $array value never escapes the : command

Wir befinden uns also praktisch in einer Sackgasse, in einer Art Zwickmühle. Aber, wenn eval seinen Code ausführt, führt er ihn in der Shell-Umgebung aus, als ob es sich um normalen, statischen Quellcode handeln würde. $array Zuordnung innerhalb der eval um sie in der Shell-Umgebung wirksam werden zu lassen, während das Argument $IFS Präfix-Zuweisung, die der eval Befehls nicht überleben wird, die eval Befehl. Dies ist genau der Trick, der in der zweiten Variante dieser Lösung angewendet wird:

IFS=', ' eval 'array=($string)'; ## $IFS does not outlive the eval command, but $array does

Wie Sie also sehen können, ist dies ein ziemlich cleverer Trick, der genau das erreicht, was erforderlich ist (zumindest in Bezug auf die Ausführung der Zuweisung), und das auf eine ziemlich unauffällige Weise. Ich bin eigentlich nicht gegen diesen Trick im Allgemeinen, trotz der Beteiligung von eval Achten Sie darauf, die Argumente in einfache Anführungszeichen zu setzen, um Sicherheitsbedrohungen zu vermeiden.

Aber auch hier ist dies aufgrund der "schlimmsten aller Welten" eine falsche Antwort auf die Forderung des Auftraggebers.


Falsche Antwort #6

IFS=', '; array=(Paris, France, Europe)

IFS=' ';declare -a array=(Paris France Europe)

Ähm... was? Der OP hat eine String-Variable, die in ein Array geparst werden muss. Diese "Antwort" beginnt mit dem wortwörtlichen Inhalt der Eingabezeichenkette, die in ein Array-Literal eingefügt wird. Ich denke, das ist eine Möglichkeit, es zu tun.

Es sieht so aus, als hätte der Antwortende angenommen, dass die $IFS Variable das gesamte Bash-Parsing in allen Kontexten beeinflusst, was aber nicht der Fall ist. Aus dem Bash-Handbuch:

IFS Das interne Feldtrennzeichen, das für die Worttrennung nach der Expansion und für die Aufteilung von Zeilen in Wörter mit dem lesen eingebauten Befehl. Der Standardwert ist <space><tab><newline> .

Also die $IFS spezielle Variable wird eigentlich nur in zwei Zusammenhängen verwendet: (1) Worttrennung, die durchgeführt wird nach der Erweiterung (Bedeutung no beim Parsen von Bash-Quellcode) und (2) zum Aufteilen von Eingabezeilen in Wörter durch die read eingebaut.

Lassen Sie mich versuchen, dies deutlicher zu machen. Ich denke, es könnte gut sein, eine Unterscheidung zu treffen zwischen Parsing y Ausführung . Bash muss zuerst zerlegen. den Quellcode, was natürlich eine Parsing Ereignis, und später dann führt aus. der Code, und hier kommt die Erweiterung ins Spiel. Die Erweiterung ist wirklich eine Ausführung Veranstaltung. Außerdem widerspreche ich der Beschreibung der $IFS Variable, die ich gerade oben zitiert habe; anstatt zu sagen, dass die Worttrennung durchgeführt wird nach der Erweiterung würde ich sagen, dass die Worttrennung durchgeführt wird. während Die Erweiterung oder, vielleicht noch genauer, die Worttrennung ist Teil von den Expansionsprozess. Der Ausdruck "Wortsplitting" bezieht sich nur auf diesen Schritt der Expansion; er sollte niemals verwendet werden, um sich auf das Parsen von Bash-Quellcode zu beziehen, obwohl die Dokumentation leider sehr oft mit den Worten "split" und "words" um sich wirft. Hier ist ein relevanter Auszug aus der linux.die.net-Version des Bash-Handbuchs:

Die Expansion wird in der Befehlszeile durchgeführt, nachdem diese in Wörter zerlegt wurde. Es werden sieben Arten der Expansion durchgeführt: Spreizung , Tilde-Erweiterung , Parameter- und Variablenerweiterung , Befehlsersetzung , arithmetische Expansion , Worttrennung y Pfadnamen-Erweiterung .

Die Reihenfolge der Expansionen ist: Klammer-Expansion; Tilde-Expansion, Parameter- und Variablen-Expansion, arithmetische Expansion und Befehlssubstitution (von links nach rechts); Worttrennung; und Pfadnamen-Expansion.

Man könnte argumentieren, dass die GNU-Version des Handbuchs ist etwas besser, da im ersten Satz des Abschnitts "Erweiterung" das Wort "Token" anstelle von "Wörtern" verwendet wird:

Die Expansion wird in der Befehlszeile durchgeführt, nachdem sie in Token aufgeteilt wurde.

Der wichtige Punkt ist, $IFS ändert nicht die Art und Weise, wie die Bash den Quellcode analysiert. Das Parsen von Bash-Quellcode ist eigentlich ein sehr komplexer Prozess, der die Erkennung der verschiedenen Elemente der Shell-Grammatik beinhaltet, wie z.B. Befehlssequenzen, Befehlslisten, Pipelines, Parametererweiterungen, arithmetische Ersetzungen und Befehlssubstitutionen. In den meisten Fällen kann der Bash-Parsing-Prozess nicht durch Aktionen auf Benutzerebene, wie z.B. Variablenzuweisungen, verändert werden (tatsächlich gibt es einige kleinere Ausnahmen von dieser Regel; siehe z.B. die verschiedenen compatxx Shell-Einstellungen die bestimmte Aspekte des Parsing-Verhaltens on-the-fly ändern kann). Die vorgelagerten "Wörter"/"Token", die sich aus diesem komplexen Parsing-Prozess ergeben, werden dann gemäß dem allgemeinen Prozess der "Expansion" expandiert, wie er in den obigen Dokumentationsauszügen beschrieben wird, wobei die Worttrennung des expandierten (expandierenden?) Textes in nachgelagerte Wörter nur ein Schritt dieses Prozesses ist. Die Worttrennung betrifft nur Text, der in einem vorangegangenen Expansionsschritt ausgespuckt wurde; sie wirkt sich nicht auf wörtlichen Text aus, der direkt aus dem Quell-Bytestream geparst wurde.


Falsche Antwort #7

string='first line
        second line
        third line'

while read -r line; do lines+=("$line"); done <<<"$string"

Dies ist eine der besten Lösungen. Beachten Sie, dass wir wieder die read . Habe ich nicht vorhin gesagt, dass read unangemessen ist, weil es zwei Splittungsstufen durchführt, obwohl wir nur eine brauchen? Der Trick dabei ist, dass Sie die Funktion read so zu gestalten, dass es effektiv nur eine Ebene der Aufteilung vornimmt, und zwar indem es nur ein Feld pro Aufruf aufteilt, was die Kosten für den wiederholten Aufruf in einer Schleife verursacht. Es ist ein kleiner Taschenspielertrick, aber es funktioniert.

Aber es gibt Probleme. Erstens: Wenn Sie mindestens eine NAME Argument zu read ignoriert es automatisch führende und nachfolgende Leerzeichen in jedem Feld, das von der Eingabezeichenfolge abgetrennt wird. Dies geschieht unabhängig davon $IFS auf den Standardwert gesetzt ist oder nicht, wie weiter oben in diesem Beitrag beschrieben. Nun, der OP mag sich für seinen speziellen Anwendungsfall nicht darum kümmern, und in der Tat mag es ein wünschenswertes Merkmal des Parsing-Verhaltens sein. Aber nicht jeder, der eine Zeichenkette in Felder parsen will, wird dies wollen. Es gibt jedoch eine Lösung: Eine etwas nicht offensichtliche Verwendung von read ist es, Null zu passieren NAME Argumente. In diesem Fall, read speichert die gesamte Eingabezeile, die es aus dem Eingabestrom erhält, in einer Variablen namens $REPLY und, als Bonus, kann es no entfernt führende und nachgestellte Leerzeichen aus dem Wert. Dies ist eine sehr robuste Verwendung von read die ich in meiner Karriere als Shell-Programmierer häufig genutzt habe. Hier ist eine Demonstration des Unterschieds im Verhalten:

string=$'  a  b  \n  c  d  \n  e  f  '; ## input string

a=(); while read -r line; do a+=("$line"); done <<<"$string"; declare -p a;
## declare -a a=([0]="a  b" [1]="c  d" [2]="e  f") ## read trimmed surrounding whitespace

a=(); while read -r; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="  a  b  " [1]="  c  d  " [2]="  e  f  ") ## no trimming

Das zweite Problem bei dieser Lösung ist, dass sie den Fall eines benutzerdefinierten Feldtrenners, wie z. B. das Komma-Leerzeichen des Auftraggebers, nicht berücksichtigt. Nach wie vor werden mehrstellige Trennzeichen nicht unterstützt, was eine unglückliche Einschränkung dieser Lösung darstellt. Wir könnten versuchen, zumindest ein Komma als Trennzeichen zu verwenden, indem wir das Trennzeichen in der -d Option, aber schauen Sie, was passiert:

string='Paris, France, Europe';
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France")

Es ist vorhersehbar, dass die nicht berücksichtigten Leerzeichen in die Feldwerte hineingezogen werden, so dass dies nachträglich durch Trimmoperationen korrigiert werden muss (dies könnte auch direkt in der while-Schleife geschehen). Aber es gibt einen weiteren offensichtlichen Fehler: Europa fehlt! Was ist damit passiert? Die Antwort ist, dass read gibt einen fehlgeschlagenen Rückgabewert zurück, wenn es auf das Ende der Datei trifft (in diesem Fall können wir es als Ende der Zeichenkette bezeichnen), ohne dass es im letzten Feld auf ein Endfeld-Terminator trifft. Dies führt zu einem vorzeitigen Abbruch der while-Schleife und zum Verlust des letzten Feldes.

Technisch gesehen trat derselbe Fehler auch in den vorherigen Beispielen auf; der Unterschied besteht darin, dass das Feldtrennzeichen als LF angenommen wurde, was der Standard ist, wenn Sie nicht das -d und die Option <<< ("here-string") hängt automatisch ein LF an die Zeichenkette an, bevor sie als Eingabe in den Befehl eingegeben wird. Daher werden wir in diesen Fällen sozusagen versehentlich löste das Problem eines weggelassenen letzten Feldes, indem es unwissentlich ein zusätzliches Dummy-Terminator an die Eingabe anhängte. Nennen wir diese Lösung die "Dummy-Terminator"-Lösung. Wir können die Dummy-Terminator-Lösung manuell für jedes benutzerdefinierte Trennzeichen anwenden, indem wir es bei der Instanziierung in der here-Zeichenfolge selbst an die Eingabezeichenfolge anhängen:

a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -p a;
declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

So, Problem gelöst. Eine andere Lösung ist, die while-Schleife nur zu unterbrechen, wenn sowohl (1) read einen Fehler zurückgegeben und (2) $REPLY leer ist, was bedeutet read war nicht in der Lage, irgendwelche Zeichen zu lesen, bevor es das Ende der Datei erreicht hat. Demo:

a=(); while read -rd,|| [[ -n "$REPLY" ]]; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

Dieser Ansatz enthüllt auch das geheime LF, das automatisch an die here-Zeichenfolge durch die <<< Umleitungsoperator. Er könnte natürlich auch separat durch eine explizite Trimm-Operation entfernt werden, wie soeben beschrieben, aber offensichtlich löst der manuelle Dummy-Terminator-Ansatz das Problem direkt, so dass wir uns damit begnügen können. Die manuelle Dummy-Terminator-Lösung ist insofern recht praktisch, als sie beide Probleme (das Problem des weggelassenen Endfeldes und das Problem des angehängten LF) in einem Zug löst.

Insgesamt ist dies also eine recht leistungsfähige Lösung. Der einzige verbleibende Schwachpunkt ist die fehlende Unterstützung für mehrstellige Begrenzungszeichen, auf die ich später eingehen werde.


Falsche Antwort #8

string='first line
        second line
        third line'

readarray -t lines <<<"$string"

(Dies ist eigentlich aus dem gleichen Beitrag wie #7 Der Antwortende hat zwei Lösungen im selben Beitrag angegeben).

En readarray builtin, das ein Synonym ist für mapfile ist ideal. Es handelt sich um einen eingebauten Befehl, der einen Bytestream auf einen Schlag in eine Array-Variable umwandelt, ohne Schleifen, Konditionierungen, Ersetzungen oder ähnliches. Und er entfernt nicht heimlich Leerzeichen aus dem Eingabestring. Und (wenn -O nicht angegeben ist), wird das Zielarray vor der Zuweisung an dieses bequem gelöscht. Aber es ist immer noch nicht perfekt, daher meine Kritik daran als "falsche Antwort".

Zunächst einmal ist zu beachten, dass, genau wie bei dem Verhalten von read beim Feld-Parsing, readarray lässt das hintere Feld weg, wenn es leer ist. Auch dies ist wahrscheinlich nicht ein Anliegen für die OP, aber es könnte für einige Anwendungsfälle sein. Ich werde gleich darauf zurückkommen.

Zweitens werden nach wie vor keine mehrstelligen Begrenzungszeichen unterstützt. Auch hierfür werde ich in Kürze eine Lösung anbieten.

Drittens analysiert die Lösung, so wie sie geschrieben ist, den Eingabe-String des OPs nicht, und tatsächlich kann sie in ihrer jetzigen Form nicht zum Parsen verwendet werden. Auch darauf werde ich gleich noch eingehen.

Aus den oben genannten Gründen halte ich dies nach wie vor für eine "falsche Antwort" auf die Frage des Fragestellers. Im Folgenden werde ich die meiner Meinung nach richtige Antwort geben.


Richtige Antwort

Hier ist ein naiver Versuch, eine #8 funktionieren, indem man einfach die -d Option:

string='Paris, France, Europe';
readarray -td, a <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

Wir sehen, dass das Ergebnis identisch ist mit dem Ergebnis, das wir mit dem doppelbedingten Ansatz der Schleifenbildung erhalten haben read Lösung diskutiert in #7 . Wir können fast lösen Sie dies mit dem manuellen Dummy-Terminator-Trick:

readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'\n')

Das Problem dabei ist, dass readarray das hintere Feld erhalten, da die <<< Umleitungsoperator das LF an die Eingabezeichenfolge angehängt, und daher wurde das hintere Feld no leer (sonst wäre sie weggefallen). Wir können das Problem lösen, indem wir das letzte Array-Element nachträglich explizit löschen:

readarray -td, a <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

Die einzigen beiden Probleme, die bleiben und die eigentlich miteinander zusammenhängen, sind (1) die überflüssigen Leerzeichen, die entfernt werden müssen, und (2) die fehlende Unterstützung für mehrstellige Trennzeichen.

Die Leerzeichen können natürlich auch nachträglich abgeschnitten werden (siehe zum Beispiel Wie schneidet man Leerzeichen aus einer Bash-Variablen? ). Aber wenn wir ein mehrstelliges Trennzeichen hacken können, dann würde das beide Probleme auf einen Schlag lösen.

Leider gibt es keine direkt wie man ein mehrstelliges Trennzeichen zum Funktionieren bringt. Die beste Lösung, die mir eingefallen ist, ist die Vorverarbeitung der Eingabezeichenfolge, um das mehrstellige Begrenzungszeichen durch ein einstelliges Begrenzungszeichen zu ersetzen, das garantiert nicht mit dem Inhalt der Eingabezeichenfolge kollidiert. Das einzige Zeichen, das diese Garantie bietet, ist das NUL-Byte . Das liegt daran, dass Variablen in der Bash (übrigens nicht in der Zsh) das NUL-Byte nicht enthalten dürfen. Dieser Vorverarbeitungsschritt kann inline in einer Prozess-Substitution durchgeführt werden. Hier sehen Sie, wie man es macht, indem man awk :

readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; }' <<<"$string, "); unset 'a[-1]';
declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

Da, endlich! Diese Lösung teilt die Felder nicht fälschlicherweise in der Mitte auf, schneidet nicht vorzeitig ab, lässt keine leeren Felder fallen, beschädigt sich nicht selbst bei der Erweiterung von Dateinamen, entfernt nicht automatisch führende und nachfolgende Leerzeichen, hinterlässt kein blinden LF am Ende, erfordert keine Schleifen und begnügt sich nicht mit einem Ein-Zeichen-Begrenzer.


Trimmlösung

Zu guter Letzt wollte ich meine eigene, ziemlich komplizierte Lösung für das Beschneiden demonstrieren, indem ich das obskure -C callback Option von readarray . Leider habe ich keinen Platz mehr für die drakonische 30.000-Zeichen-Beschränkung von Stack Overflow, so dass ich es nicht mehr erklären kann. Ich überlasse das dem Leser als Übung.

function mfcb { local val="$4"; "$1"; eval "$2[$3]=\$val;"; };
function val_ltrim { if [[ "$val" =~ ^[[:space:]]+ ]]; then val="${val:${#BASH_REMATCH[0]}}"; fi; };
function val_rtrim { if [[ "$val" =~ [[:space:]]+$ ]]; then val="${val:0:${#val}-${#BASH_REMATCH[0]}}"; fi; };
function val_trim { val_ltrim; val_rtrim; };
readarray -c1 -C 'mfcb val_trim a' -td, <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

257voto

Jim Ho Punkte 2752

Hier ist eine Möglichkeit, ohne IFS einzustellen:

string="1:2:3:4:5"
set -f                      # avoid globbing (expansion of *).
array=(${string//:/ })
for i in "${!array[@]}"
do
    echo "$i=>${array[i]}"
done

Die Idee ist die Ersetzung von Zeichenketten:

${string//substring/replacement}

um alle Übereinstimmungen von $substring durch Leerzeichen zu ersetzen und dann die ersetzte Zeichenkette zu verwenden, um ein Array zu initialisieren:

(element1 element2 ... elementN)

Hinweis: In dieser Antwort wird die Split+Glob-Operator . Um die Expansion einiger Zeichen zu verhindern (z. B. * ) ist es eine gute Idee, das Globbing für dieses Skript zu unterbrechen.

152voto

Jmoney38 Punkte 2826
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

Druckt drei

32voto

Die akzeptierte Antwort funktioniert für Werte in einer Zeile.
Wenn die Variable mehrere Zeilen hat:

string='first line
        second line
        third line'

Wir brauchen einen ganz anderen Befehl, um alle Zeilen zu erhalten:

while read -r line; do lines+=("$line"); done <<<"$string"

Oder die viel einfachere bash readarray :

readarray -t lines <<<"$string"

Es ist sehr einfach, alle Zeilen zu drucken, indem man eine printf-Funktion nutzt:

printf ">[%s]\n" "${lines[@]}"

>[first line]
>[        second line]
>[        third line]

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X