Teilen Sie die Zeichenfolge in Bash in ein Array auf


640

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

Die Linie:

Paris, France, Europe

Ich möchte sie in einem Array wie diesem haben:

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

Ich möchte einfachen Code verwenden, die Geschwindigkeit des Befehls spielt keine Rolle. Wie kann ich es tun?


22
Dies ist der erste Google-Hit, aber die Antwort ist kontrovers, da die Frage leider nach einer Abgrenzung , (Komma-Leerzeichen) und nicht nach einem einzelnen Zeichen wie Komma fragt . Wenn Sie nur an letzterem interessiert sind, sind die Antworten hier einfacher zu folgen: stackoverflow.com/questions/918886/…
antak

Wenn Sie einen String mischen möchten und sich nicht dafür interessieren, ihn als Array zu haben, cutist dies ein nützlicher Bash-Befehl, den Sie ebenfalls berücksichtigen sollten. Separator ist definierbar en.wikibooks.org/wiki/Cut Sie können auch Daten aus einer Datensatzstruktur mit fester Breite extrahieren. en.wikipedia.org/wiki/Cut_(Unix) computerhope.com/unix/ucut.htm
JGFMK

Antworten:


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

Beachten Sie, dass die Zeichen in $IFSeinzeln als Trennzeichen behandelt werden, sodass in diesem Fall Felder entweder durch ein Komma oder ein Leerzeichen getrennt werden können und nicht durch die Reihenfolge der beiden Zeichen. Interessanterweise werden leere Felder nicht erstellt, wenn in der Eingabe ein Komma-Leerzeichen angezeigt wird, da das Leerzeichen speziell behandelt wird.

So greifen Sie auf ein einzelnes Element zu:

echo "${array[0]}"

So durchlaufen Sie die Elemente:

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

So erhalten Sie sowohl den Index als auch den Wert:

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

Das letzte Beispiel ist nützlich, da 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

So ermitteln Sie die Anzahl der Elemente in einem Array:

echo "${#array[@]}"

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

echo "${array[-1]}"

in jeder Version von Bash (von irgendwo nach 2.05b):

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

Größere negative Offsets werden weiter vom Ende des Arrays entfernt ausgewählt. Beachten Sie das Leerzeichen vor dem Minuszeichen in der älteren Form. Es ist notwendig.


15
Verwenden IFS=', 'Sie einfach , dann müssen Sie die Leerzeichen nicht separat entfernen. Test:IFS=', ' read -a array <<< "Paris, France, Europe"; echo "${array[@]}"
10b0

4
@ l0b0: Danke. Ich weiß nicht, was ich gedacht habe. Ich verwende es declare -p arrayübrigens gerne für Testausgaben.
Bis auf weiteres angehalten.

1
Dies scheint Zitate nicht zu respektieren. Zum Beispiel wird France, Europe, "Congo, The Democratic Republic of the"dies nach Kongo aufgeteilt.
Israel Dov

2
@YisraelDov: Bash hat keine Möglichkeit, alleine mit CSV umzugehen. Es kann keinen Unterschied zwischen Kommas in Anführungszeichen und solchen außerhalb von Anführungszeichen erkennen. Sie benötigen ein Werkzeug, das CSV wie eine lib in einer höheren Sprache versteht, zum Beispiel des csv - Modul in Python.
Bis auf weiteres angehalten.

4
str="Paris, France, Europe, Los Angeles"; IFS=', ' read -r -a array <<< "$str"wird array=([0]="Paris" [1]="France" [2]="Europe" [3]="Los" [4]="Angeles")als Notiz aufgeteilt. Dies funktioniert also nur mit Feldern ohne Leerzeichen, da IFS=', 'es sich um eine Reihe einzelner Zeichen handelt - nicht um einen Zeichenfolgenbegrenzer.
Morgengrauen

330

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 ein Missbrauch von $IFS. Der Wert der $IFSVariablen wird nicht als einzelnes Zeichenfolgentrennzeichen mit variabler Länge verwendet, sondern als Satz von Zeichenfolgen-Trennzeichen mit einem Zeichen , wobei jedes Feld, readdas von der Eingabezeile abgespalten wird, durch ein beliebiges Zeichen im Satz abgeschlossen werden kann (Komma oder Leerzeichen in diesem Beispiel).

Tatsächlich ist für die echten Stickler da draußen die volle Bedeutung von $IFSetwas mehr involviert. Aus dem Bash-Handbuch :

Die Shell behandelt jedes Zeichen von IFS als Trennzeichen und teilt die Ergebnisse der anderen Erweiterungen in Wörter auf, wobei diese Zeichen als Feldterminatoren verwendet werden. Wenn IFS nicht gesetzt ist oder sein Wert genau <Leerzeichen> <Tab> <Newline> ist , die Standardeinstellung, dann die Sequenzen von <Leerzeichen> , <Tab> und <Newline> am Anfang und Ende der Ergebnisse der vorherigen Erweiterungen werden ignoriert, und jede Folge von IFS- Zeichen, die nicht am Anfang oder Ende stehen, dient zur Abgrenzung von Wörtern. Wenn IFS einen anderen Wert als den Standardwert hat, werden Sequenzen der Leerzeichen <Leerzeichen> , <Tab> und <verwendetwerden am Anfang und Ende des Wortes ignoriert, solange das Leerzeichen den Wert von IFS (ein IFS- Leerzeichen) hat. Jedes Zeichen in IFS , das kein IFS- Leerzeichen ist, sowie alle benachbarten IFS- Leerzeichen begrenzen ein Feld. Eine Folge von IFS- Leerzeichen wird ebenfalls als Trennzeichen behandelt. Wenn der Wert von IFS null ist, erfolgt keine Wortaufteilung.

Grundsätzlich $IFSkönnen Felder für nicht standardmäßige Nicht-Null-Werte von entweder mit (1) einer Folge von einem oder mehreren Zeichen getrennt werden, die alle aus dem Satz von "IFS-Leerzeichen" stammen (d. H. Welcher von <Leerzeichen>) . <tab> und <newline> ("newline" bedeutet Zeilenvorschub (LF) ) sind überall in $IFS) oder (2) alle Nicht-"IFS-Leerzeichen", die $IFSzusammen mit den "IFS-Leerzeichen" vorhanden sind in der Eingabezeile.

Für das OP ist es möglich, dass der zweite Trennungsmodus, den ich im vorherigen Absatz beschrieben habe, genau das ist, was er für seine Eingabezeichenfolge wünscht, 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: Auch wenn Sie waren diese Lösung mit einem Einzel-Zeichen - Separator (wie ein Komma von selbst, daß ohne folgende Leerzeichen oder anderem Gepäck ist) zu verwenden, wenn der Wert der $stringVariable irgendwelche LFs enthalten passiert, dann readwerden Beenden Sie die Verarbeitung, sobald der erste LF gefunden wird. Das readeingebaute verarbeitet nur eine Zeile pro Aufruf. Dies gilt auch dann, wenn Sie Eingaben nur an die readAnweisung weiterleiten oder umleiten , wie wir es in diesem Beispiel mit dem Here-String- Mechanismus tun , und somit garantiert, dass unverarbeitete Eingaben verloren gehen. Der Code, der das readeingebaute System antreibt, kennt den Datenfluss in seiner enthaltenen Befehlsstruktur nicht.

Sie könnten argumentieren, dass dies wahrscheinlich kein Problem verursacht, aber dennoch eine subtile Gefahr darstellt, die nach Möglichkeit vermieden werden sollte. readDies wird durch die Tatsache verursacht, dass das integrierte Gerät tatsächlich zwei Ebenen der Eingabeaufteilung durchführt: zuerst in Zeilen, dann in Felder. Da das OP nur eine Aufteilungsebene wünscht, ist diese Verwendung des integrierten readSystems nicht angemessen, und wir sollten dies vermeiden.

3: Ein nicht offensichtliches potenzielles Problem bei dieser Lösung besteht darin, dass readdas nachfolgende Feld immer gelöscht wird, wenn es leer ist, obwohl ansonsten leere Felder erhalten bleiben. 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 würde sich das OP nicht darum kümmern, aber es ist immer noch eine Einschränkung, über die es sich zu wissen lohnt. Dies verringert die Robustheit und Allgemeingültigkeit der Lösung.

Dieses Problem kann gelöst werden, indem unmittelbar vor dem Einspeisen ein Dummy-Trennzeichen an die Eingabezeichenfolge angehängt wird read, wie ich später 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 Befehlsersetzung hinzugefügt, die der Antwortende anscheinend weggelassen hat.)

Ähnliche Idee:

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

Diese Lösungen nutzen die Wortaufteilung in einer Array-Zuweisung, um die Zeichenfolge in Felder aufzuteilen. Lustigerweise readverwendet die allgemeine Wortaufteilung ebenso wie die allgemeine $IFSVariable die spezielle Variable, obwohl in diesem Fall impliziert wird, dass sie auf den Standardwert <Leerzeichen> <Tab> <Newline> und damit auf eine beliebige Folge eines oder mehrerer IFS gesetzt ist Zeichen (die jetzt alle Leerzeichen sind) werden als Feldtrennzeichen betrachtet.

Dies löst das Problem von zwei Aufteilungsebenen, die von begangen werden read, da die Wortaufteilung an sich nur eine Aufteilungsebene darstellt. Das Problem besteht jedoch nach wie vor darin, dass die einzelnen Felder in der Eingabezeichenfolge bereits $IFSZeichen enthalten können und daher während des Wortaufteilungsvorgangs nicht ordnungsgemäß aufgeteilt werden. Dies ist bei keiner der von diesen Antwortenden bereitgestellten Beispiel-Eingabezeichenfolgen der Fall (wie praktisch ...), aber das ändert natürlich nichts an der Tatsache, dass eine Codebasis, die diese Redewendung verwendet, dann das Risiko eingehen würde Sprengung, wenn diese Annahme jemals irgendwann auf der ganzen Linie verletzt wurde. Betrachten Sie noch einmal mein Gegenbeispiel von 'Los Angeles, United States, North America'(oder 'Los Angeles:United States:North America').

Auch wird das Wort Aufspalten normalerweise durch gefolgt Dateierweiterung ( aka Pfadnamenerweiterung aka Globbing), die, wenn sie durchgeführt würden potentiell beschädigte Worte die Zeichen enthalten *, ?oder [gefolgt von ](und, falls extglobgesetzt, geklammert Fragmenten mit vorangestellter ?, *, +, @, oder !) indem Sie sie mit Dateisystemobjekten abgleichen und die Wörter ("Globs") entsprechend erweitern. Der erste dieser drei Antwortenden hat dieses Problem geschickt unterboten, indem er set -fzuvor ausgeführt wurde, um das Globbing zu deaktivieren. Technisch funktioniert das (obwohl Sie wahrscheinlich hinzufügen solltenset +f Danach können Sie das Globbing für nachfolgenden Code wieder aktivieren, der möglicherweise davon abhängt. Es ist jedoch unerwünscht, sich mit den globalen Shell-Einstellungen herumschlagen zu müssen, um eine grundlegende Analyseoperation von String zu Array im lokalen Code zu hacken.

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

Hinweis: Wenn Sie diese Lösung verwenden möchten, ist es besser, die ${string//:/ }Form der Parametererweiterung "Mustersubstitution" zu verwenden , als sich die Mühe zu machen, eine Befehlssubstitution (die die Shell teilt) aufzurufen, eine Pipeline zu starten und Ausführen einer externen ausführbaren Datei ( troder sed), da die Parametererweiterung eine rein shellinterne Operation ist. (Außerdem sollte für die trund sed-Lösungen die Eingabevariable innerhalb der Befehlssubstitution in doppelte Anführungszeichen gesetzt werden. Andernfalls würde die Wortaufteilung im echoBefehl wirksam und möglicherweise die Feldwerte beeinträchtigen. Außerdem ist die $(...)Form der Befehlssubstitution der alten vorzuziehen`...` Form, da es das Verschachteln von Befehlsersetzungen 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 Antwortende davon ausgegangen ist, dass die Felder durch zwei Zeichen begrenzt sind, von denen eines in der Standardeinstellung dargestellt $IFSwird und das andere nicht. Er hat diesen ziemlich spezifischen Fall gelöst, indem er das nicht IFS-dargestellte Zeichen unter Verwendung einer Mustersubstitutionserweiterung entfernt und dann die Felder auf dem überlebenden IFS-dargestellten Trennzeichen durch Wortaufteilung aufteilt.

Dies ist keine sehr generische Lösung. Darüber hinaus kann argumentiert werden, dass das Komma hier wirklich das "primäre" Trennzeichen ist und dass das Entfernen und dann abhängig vom Leerzeichen für die Feldaufteilung einfach falsch ist. Betrachten Sie noch einmal mein Gegenbeispiel : 'Los Angeles, United States, North America'.

Auch hier könnte die Dateinamenerweiterung die erweiterten Wörter beschädigen. Dies kann jedoch verhindert werden, indem das Globbing für die Zuweisung mit set -fund dann vorübergehend deaktiviert wird set +f.

Auch hier gehen alle leeren Felder verloren, was je nach Anwendung ein Problem sein kann oder 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 insofern ähnlich zu # 2 und # 3 , als es die Wortaufteilung verwendet, um die Arbeit zu erledigen, nur dass der Code jetzt explizit so festgelegt wird $IFS, dass er nur das in der Eingabezeichenfolge vorhandene Einzelzeichen-Feldtrennzeichen enthält. Es sollte wiederholt werden, dass dies für Feldtrennzeichen mit mehreren Zeichen wie das Komma-Raum-Trennzeichen des OP nicht funktionieren kann. Für ein Einzelzeichen-Trennzeichen wie das in diesem Beispiel verwendete LF ist es jedoch nahezu perfekt. Die Felder können nicht unbeabsichtigt in der Mitte aufgeteilt werden, wie wir bei früheren falschen Antworten gesehen haben, und es gibt je nach Bedarf nur eine Aufteilungsebene.

Ein Problem besteht darin, dass die Dateinamenerweiterung betroffene Wörter wie zuvor beschrieben beschädigt. Dies kann jedoch erneut gelöst werden, indem die kritische Anweisung in set -fund eingeschlossen wird set +f.

Ein weiteres potenzielles Problem besteht darin, dass, da LF wie zuvor definiert als "IFS-Leerzeichen" qualifiziert ist, alle leeren Felder verloren gehen, genau wie in # 2 und # 3 . Dies wäre natürlich kein Problem, wenn das Trennzeichen ein Nicht-IFS-Leerzeichen ist, und je nach Anwendung spielt es möglicherweise keine Rolle, beeinträchtigt jedoch die Allgemeingültigkeit der Lösung.

Zusammenfassend lässt sich sagen, dass Sie ein Ein-Zeichen-Trennzeichen haben und es sich entweder nicht um ein "IFS-Leerzeichen" handelt oder dass Sie sich nicht für leere Felder interessieren und die kritische Anweisung in set -fund einschließen. set +fDann funktioniert diese Lösung , aber sonst nicht.

(Zur Information kann das Zuweisen eines LF zu einer Variablen in bash auch einfacher mit der $'...'Syntax erfolgen, z 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 effektiv eine Kreuzung zwischen # 1 (indem sie $IFSauf Komma setzt ) und # 2-4 (indem sie die Wortaufteilung verwendet, um die Zeichenfolge in Felder aufzuteilen). Aus diesem Grund leidet es unter den meisten Problemen, die alle oben genannten falschen Antworten betreffen, ähnlich wie die schlimmste aller Welten.

Auch in Bezug auf die zweite Variante scheint der evalAufruf völlig unnötig zu sein, da sein Argument ein Zeichenfolgenliteral in einfachen Anführungszeichen ist und daher statisch bekannt ist. Die Verwendung evalauf diese Weise bietet jedoch einen nicht offensichtlichen Vorteil . Normalerweise, wenn Sie einen einfachen Befehl ausführen , die aus einer variablen Zuordnung besteht nur , ohne einen tatsächlichen Befehl Wort und bedeutet es folgende erfolgt die Zuordnung Wirkung in der Shell - Umgebung:

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

Dies gilt auch dann, wenn der einfache Befehl mehrere Variablenzuweisungen umfasst . Auch hier wirken sich alle Variablenzuweisungen auf die Shell-Umgebung aus, solange kein Befehlswort vorhanden ist:

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 eine "Präfixzuweisung"), wirkt sich dies nicht auf die Shell-Umgebung aus, sondern nur auf die Umgebung des ausgeführten Befehls, unabhängig davon, ob es sich um einen integrierten Befehl handelt oder extern:

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

Relevantes Zitat aus dem Bash-Handbuch :

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

Es ist möglich, diese Funktion der Variablenzuweisung zu nutzen, um Änderungen $IFSnur vorübergehend vorzunehmen, wodurch wir das gesamte Spiel zum Speichern und Wiederherstellen vermeiden können, wie es bei der $OIFSVariablen in der ersten Variante der Fall ist. Die Herausforderung, der wir uns hier gegenübersehen, besteht darin, dass der Befehl, den wir ausführen müssen, selbst eine bloße Variablenzuweisung ist und daher kein Befehlswort enthält, um die $IFSZuweisung vorübergehend zu machen . Sie könnten sich denken, warum fügen Sie der Anweisung nicht einfach ein No-Op-Befehlswort hinzu : builtin, um die $IFSZuweisung vorübergehend zu machen ? Dies funktioniert nicht, da die $arrayZuweisung dann auch vorübergehend wäre :

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

Wir befinden uns also effektiv in einer Sackgasse, ein bisschen wie ein Catch-22. Wenn evalder Code ausgeführt wird, wird er in der Shell-Umgebung ausgeführt, als wäre es normaler statischer Quellcode. Daher können wir die $arrayZuweisung innerhalb des evalArguments ausführen, damit sie in der Shell-Umgebung wirksam wird, während die $IFSPräfixzuweisung dies tut wird dem evalBefehl vorangestellt, überlebt den evalBefehl nicht. Dies ist genau der Trick, der in der zweiten Variante dieser Lösung verwendet wird:

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

Wie Sie sehen, handelt es sich also tatsächlich um einen ziemlich cleveren Trick, der genau das erreicht, was erforderlich ist (zumindest in Bezug auf die Zuweisungseffekte), und zwar auf eine nicht offensichtliche Weise. Ich bin eigentlich nicht gegen diesen Trick im Allgemeinen, trotz der Beteiligung von eval; Achten Sie nur darauf, die Argumentzeichenfolge in einfache Anführungszeichen zu setzen, um sich vor Sicherheitsbedrohungen zu schützen.

Aber auch hier ist dies aufgrund der "schlimmsten aller Welten" Agglomeration von Problemen immer noch eine falsche Antwort auf die Forderung des OP.


Falsche Antwort # 6

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

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

Ähm ... was? Das OP verfügt über eine Zeichenfolgenvariable, die in ein Array analysiert werden muss. Diese "Antwort" beginnt mit dem wörtlichen Inhalt der Eingabezeichenfolge, 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 $IFSVariable die gesamte Bash-Analyse in allen Kontexten beeinflusst, was nicht der Fall ist. Aus dem Bash-Handbuch:

IFS     Der interne Feldtrenner, der zum Teilen von Wörtern nach der Erweiterung und zum Teilen von Zeilen in Wörter mit dem Befehl read builtin verwendet wird. Der Standardwert ist <Leerzeichen> <Tab> <Neue Zeile> .

Die $IFSspezielle Variable wird also eigentlich nur in zwei Kontexten verwendet: (1) Wortaufteilung, die nach der Erweiterung durchgeführt wird (dh nicht beim Parsen des Bash-Quellcodes) und (2) zum Aufteilen von Eingabezeilen in Wörter durch das readeingebaute.

Lassen Sie mich versuchen, dies klarer zu machen. Ich denke, es könnte gut sein, zwischen Parsen und Ausführen zu unterscheiden . Bash muss zuerst den Quellcode analysieren , was offensichtlich ein Parsing- Ereignis ist, und später den Code ausführen , wenn die Erweiterung ins Bild kommt. Expansion ist wirklich ein Ausführungsereignis . Außerdem habe ich Probleme mit der Beschreibung der $IFSVariablen, die ich gerade zitiert habe. Anstatt zu sagen, dass die Wortaufteilung nach der Erweiterung durchgeführt wird , würde ich sagen, dass die Wortaufteilung während der Erweiterung durchgeführt wird, oder, vielleicht noch genauer, die Wortaufteilung ist ein Teil davonder Expansionsprozess. Der Ausdruck "Wortaufteilung" bezieht sich nur auf diesen Expansionsschritt; Es sollte niemals verwendet werden, um auf das Parsen von Bash-Quellcode zu verweisen, obwohl die Dokumente leider die Wörter "split" und "words" häufig herumwerfen. Hier ist ein relevanter Auszug aus der linux.die.net-Version des Bash-Handbuchs:

Die Erweiterung wird in der Befehlszeile ausgeführt, nachdem sie in Wörter aufgeteilt wurde. Es werden sieben Arten der Erweiterung durchgeführt: Klammererweiterung , Tilde-Erweiterung , Parameter- und Variablenerweiterung , Befehlssubstitution , arithmetische Erweiterung , Wortteilung und Pfadnamenerweiterung .

Die Reihenfolge der Erweiterungen lautet: Klammererweiterung; Tilde-Erweiterung, Parameter- und Variablenerweiterung, arithmetische Erweiterung und Befehlssubstitution (von links nach rechts); Wortaufteilung; und Pfadnamenerweiterung.

Sie könnten argumentieren, dass die GNU-Version des Handbuchs etwas besser abschneidet, da sie im ersten Satz des Erweiterungsabschnitts das Wort "Token" anstelle von "Wörtern" verwendet:

Die Erweiterung wird in der Befehlszeile ausgeführt, nachdem sie in Token aufgeteilt wurde.

Der wichtige Punkt ist, $IFSändert nichts an der Art und Weise, wie Bash den Quellcode analysiert. Das Parsen von Bash-Quellcode ist ein sehr komplexer Prozess, bei dem die verschiedenen Elemente der Shell-Grammatik erkannt werden, z. B. Befehlssequenzen, Befehlslisten, Pipelines, Parametererweiterungen, arithmetische Ersetzungen und Befehlsersetzungen. Zum größten Teil kann der Bash-Parsing-Prozess nicht durch Aktionen auf Benutzerebene wie Variablenzuweisungen geändert werden (tatsächlich gibt es einige geringfügige Ausnahmen von dieser Regel; siehe beispielsweise die verschiedenen compatxxShell-Einstellungen, die bestimmte Aspekte des Analyseverhaltens im laufenden Betrieb ändern können). Die vorgelagerten "Wörter" / "Token", die sich aus diesem komplexen Analyseprozess ergeben, werden dann gemäß dem allgemeinen Prozess der "Erweiterung" erweitert, wie in den obigen Dokumentationsausschnitten beschrieben, wobei die Wortaufteilung des erweiterten (expandierenden?) Textes in den nachgelagerten Text erfolgt Worte sind einfach ein Schritt dieses Prozesses. Das Teilen von Wörtern berührt nur Text, der aus einem vorhergehenden Erweiterungsschritt ausgespuckt wurde. Literaltext, der direkt aus dem Quell-Bytestream analysiert wurde, ist davon nicht betroffen.


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 verwenden read. Habe ich nicht früher gesagt, dass dies readunangemessen ist, weil es zwei Aufteilungsebenen durchführt, wenn wir nur eine brauchen? Der Trick dabei ist, dass Sie so aufrufen können read, dass effektiv nur eine Aufteilungsebene ausgeführt wird, insbesondere indem nur ein Feld pro Aufruf abgespalten wird, was die Kosten für den wiederholten Aufruf in einer Schleife erforderlich macht. Es ist ein bisschen ein Kinderspiel, aber es funktioniert.

Aber es gibt Probleme. Erstens: Wenn Sie mindestens ein NAME- Argument angebenread , werden führende und nachfolgende Leerzeichen in jedem Feld, das von der Eingabezeichenfolge getrennt ist, automatisch ignoriert. Dies tritt auf, unabhängig davon, ob $IFSder Standardwert festgelegt ist oder nicht, wie weiter oben in diesem Beitrag beschrieben. Nun, das OP kümmert sich möglicherweise nicht darum für seinen spezifischen Anwendungsfall, und tatsächlich kann es ein wünschenswertes Merkmal des Analyseverhaltens sein. Aber nicht jeder, der einen String in Felder analysieren möchte, wird dies wollen. Es gibt jedoch eine Lösung: Eine etwas nicht offensichtliche Verwendung von readbesteht darin, null NAME- Argumente zu übergeben. In diesem Fall readwird die gesamte Eingabezeile, die vom Eingabestream abgerufen wird, in einer Variablen mit dem Namen gespeichert $REPLY, was als Bonus nicht der Fall istEntfernen Sie führende und nachfolgende Leerzeichen vom Wert. Dies ist eine sehr robuste Verwendung, readdie ich in meiner Karriere als Shell-Programmierer häufig ausgenutzt 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 besteht darin, dass der Fall eines benutzerdefinierten Feldtrennzeichens, wie z. B. des Komma-Bereichs des OP, nicht behandelt wird. Nach wie vor werden Multicharakter-Separatoren nicht unterstützt, was eine unglückliche Einschränkung dieser Lösung darstellt. Wir könnten versuchen, zumindest durch Komma zu teilen, indem wir das Trennzeichen für die -dOption angeben, 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")

Vorhersehbarerweise wurde das nicht berücksichtigte umgebende Leerzeichen in die Feldwerte gezogen, und daher müsste dies anschließend durch Trimmvorgänge korrigiert werden (dies könnte auch direkt in der while-Schleife erfolgen). Aber es gibt noch einen weiteren offensichtlichen Fehler: Europa fehlt! Was ist damit passiert? Die Antwort lautet, dass readein fehlerhafter Rückkehrcode zurückgegeben wird, wenn er das Dateiende erreicht (in diesem Fall können wir ihn als Ende der Zeichenfolge bezeichnen), ohne dass ein endgültiger Feldabschluss im letzten Feld auftritt. Dies führt dazu, dass die while-Schleife vorzeitig unterbrochen wird und wir das letzte Feld verlieren.

Technisch gesehen betraf derselbe Fehler auch die vorherigen Beispiele. Der Unterschied besteht darin, dass das Feldtrennzeichen als LF angenommen wurde. Dies ist die Standardeinstellung, wenn Sie die -dOption nicht angeben , und der <<<Mechanismus ("hier-Zeichenfolge") hängt automatisch eine LF an die Zeichenfolge an, bevor sie als eingegeben wird Eingabe in den Befehl. Daher haben wir in diesen Fällen das Problem eines abgelegten Endfelds versehentlich gelöst, indem wir unabsichtlich einen zusätzlichen Dummy-Terminator an die Eingabe angehängt haben. 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 sie selbst mit der Eingabezeichenfolge verketten, wenn wir sie in der Here-Zeichenfolge instanziieren:

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

Dort ist das Problem gelöst. Eine andere Lösung besteht darin, die while-Schleife nur zu unterbrechen, wenn sowohl (1) einen readFehler zurückgegeben hat als auch (2) $REPLYleer ist, was bedeutet read, dass vor dem Erreichen des Dateiende keine Zeichen gelesen werden konnten. 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 den geheimen LF, der vom <<<Umleitungsoperator automatisch an die Here-Zeichenfolge angehängt wird . Es könnte natürlich durch einen expliziten Trimmvorgang, wie vor einem Moment beschrieben, separat entfernt werden, aber offensichtlich löst der manuelle Dummy-Terminator-Ansatz es direkt, also könnten wir einfach damit weitermachen. Die manuelle Dummy-Terminator-Lösung ist insofern recht praktisch, als sie diese beiden Probleme (das Problem mit dem abgelegten Endfeld und das Problem mit dem angehängten LF) auf einmal löst.

Insgesamt ist dies also eine ziemlich leistungsstarke Lösung. Die einzige verbleibende Schwäche ist die mangelnde Unterstützung für Multicharakter-Trennzeichen, auf die ich später noch eingehen werde.


Falsche Antwort # 8

string='first line
        second line
        third line'

readarray -t lines <<<"$string"

(Dies ist tatsächlich aus demselben Beitrag wie # 7 ; der Antwortende hat zwei Lösungen in demselben Beitrag bereitgestellt.)

Das readarrayeingebaute Synonym für mapfileist ideal. Es ist ein eingebauter Befehl, der einen Bytestream auf einmal in eine Array-Variable analysiert. Kein Durcheinander mit Schleifen, Bedingungen, Ersetzungen oder irgendetwas anderem. Und es entfernt nicht heimlich Leerzeichen von der Eingabezeichenfolge. Und (falls -Onicht angegeben) löscht es bequem das Zielarray, bevor es zugewiesen wird. Aber es ist immer noch nicht perfekt, daher meine Kritik daran als "falsche Antwort".

Um dies aus dem Weg zu räumen, beachten Sie zunächst, dass genau wie beim Verhalten readbeim Parsen von Feldern readarraydas nachfolgende Feld gelöscht wird, wenn es leer ist. Auch dies ist wahrscheinlich kein Problem für das OP, könnte aber für einige Anwendungsfälle sein. Ich werde gleich darauf zurückkommen.

Zweitens werden nach wie vor keine Multicharakter-Begrenzer unterstützt. Ich werde auch gleich eine Lösung dafür finden.

Drittens analysiert die geschriebene Lösung nicht die Eingabezeichenfolge des OP, und tatsächlich kann sie nicht so verwendet werden, wie sie ist, um sie zu analysieren. Ich werde auch kurz darauf eingehen.

Aus den oben genannten Gründen halte ich dies immer noch für eine "falsche Antwort" auf die Frage des OP. Im Folgenden werde ich das geben, was ich für die richtige Antwort halte.


Richtige Antwort

Hier ist ein naiver Versuch, # 8 zum Laufen zu bringen, indem Sie einfach die -dOption angeben:

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 mit dem Ergebnis identisch ist, das wir aus dem readin # 7 diskutierten doppelt bedingten Ansatz der Schleifenlösung erhalten haben . Wir können dies fast mit dem manuellen Dummy-Terminator-Trick lösen:

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

Das Problem hierbei ist, dass readarraydas nachfolgende Feld beibehalten wurde, da der <<<Umleitungsoperator den LF an die Eingabezeichenfolge angehängt hat und das nachfolgende Feld daher nicht leer war (andernfalls wäre es gelöscht worden). Wir können uns darum kümmern, indem wir das endgültige Array-Element explizit nachträglich deaktivieren:

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

Die einzigen zwei verbleibenden Probleme, die tatsächlich zusammenhängen, sind (1) das überflüssige Leerzeichen, das gekürzt werden muss, und (2) die mangelnde Unterstützung für Multicharakter-Begrenzer.

Das Leerzeichen kann natürlich später gekürzt werden (siehe z. B. Trimmen von Leerzeichen aus einer Bash-Variablen? ). Aber wenn wir ein Trennzeichen für mehrere Zeichen hacken können, würde dies beide Probleme auf einmal lösen.

Leider gibt es keinen direkten Weg, um ein Trennzeichen für mehrere Zeichen zum Laufen zu bringen. Die beste Lösung, an die ich gedacht habe, besteht darin, die Eingabezeichenfolge vorzuverarbeiten, um das Mehrzeichen-Trennzeichen durch ein Einzelzeichen-Trennzeichen zu ersetzen, das garantiert nicht mit dem Inhalt der Eingabezeichenfolge kollidiert. Das einzige Zeichen, das diese Garantie hat, ist das NUL-Byte . Dies liegt daran, dass Variablen in bash (übrigens nicht in zsh) das NUL-Byte nicht enthalten können. Dieser Vorverarbeitungsschritt kann inline in einer Prozesssubstitution durchgeführt werden. So geht's mit awk :

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

Endlich da! Diese Lösung teilt keine Felder fälschlicherweise in der Mitte auf, schneidet nicht vorzeitig aus, löscht keine leeren Felder, beschädigt sich nicht bei Dateinamenerweiterungen, entfernt nicht automatisch führende und nachfolgende Leerzeichen und hinterlässt am Ende keinen blinden LF. erfordert keine Schleifen und gibt sich nicht mit einem Einzelzeichen-Trennzeichen zufrieden.


Trimmlösung

Zuletzt wollte ich meine eigene ziemlich komplizierte Trimmlösung mit der obskuren -C callbackOption von demonstrieren readarray. Leider habe ich gegen Stack Overflows drakonisches Post-Limit von 30.000 Zeichen keinen Platz mehr, daher kann ich es nicht erklären. Ich werde das als Übung für den Leser belassen.

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")

8
Es kann auch hilfreich sein zu beachten (obwohl Sie verständlicherweise keinen Platz dafür hatten), dass die -dOption zum readarrayersten Mal in Bash 4.4 angezeigt wird.
Fbicknel

2
Tolle Antwort (+1). Wenn Sie Ihre awk ändern awk '{ gsub(/,[ ]+|$/,"\0"); print }'und diese Verkettung des Finales beseitigen, müssen ", " Sie nicht die Gymnastik durchlaufen, um den endgültigen Rekord zu eliminieren. Also: readarray -td '' a < <(awk '{ gsub(/,[ ]+/,"\0"); print; }' <<<"$string")auf Bash das unterstützt readarray. Hinweis Ihre Methode ist Bash 4.4+ Ich denke , wegen der -dinreadarray
Dawg

3
@datUser Das ist bedauerlich. Ihre Version von Bash muss zu alt sein für readarray. In diesem Fall können Sie die zweitbeste Lösung verwenden, auf der aufgebaut ist read. Ich beziehe mich darauf: a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,";(mit der awkSubstitution, wenn Sie Unterstützung für Trennzeichen mit mehreren Zeichen benötigen). Lassen Sie mich wissen, wenn Sie auf Probleme stoßen. Ich bin mir ziemlich sicher, dass diese Lösung auf ziemlich alten Versionen von Bash funktionieren sollte, zurück zu Version 2 - etwas, das wie vor zwei Jahrzehnten veröffentlicht wurde.
Bgoldst

1
Wow, was für eine brillante Antwort! Hee hee, meine Antwort: Ich habe das Bash-Skript fallen lassen und Python gestartet!
Artfulrobot

1
@datUser bash unter OSX steckt immer noch bei 3.2 (veröffentlicht ca. 2007); Ich habe die in Homebrew gefundene Bash verwendet, um 4.X Bash-Versionen unter OS X
JDS

222

Hier ist ein Weg 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 das Ersetzen von Zeichenfolgen:

${string//substring/replacement}

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

(element1 element2 ... elementN)

Hinweis: Diese Antwort verwendet den Operator split + glob . Um die Erweiterung einiger Zeichen (z. B. *) zu verhindern , empfiehlt es sich, das Globbing für dieses Skript anzuhalten.


1
Ich habe diesen Ansatz verwendet ... bis ich auf eine lange Schnur stieß, um sie zu teilen. 100% CPU für mehr als eine Minute (dann habe ich es getötet). Schade, denn diese Methode ermöglicht das Teilen durch eine Zeichenfolge, nicht durch ein Zeichen in IFS.
Werner Lehmann

100% CPU-Zeit für eine Minute klingt für mich so, als ob irgendwo etwas nicht stimmt. Wie lang war diese Zeichenfolge, hat sie eine Größe von MB oder GB? Ich denke, normalerweise, wenn Sie nur einen kleinen String-Split benötigen, möchten Sie in Bash bleiben, aber wenn es sich um eine riesige Datei handelt, würde ich etwas wie Perl ausführen, um dies zu tun.

12
WARNUNG: Bei diesem Ansatz ist gerade ein Problem aufgetreten. Wenn Sie ein Element mit dem Namen * haben, erhalten Sie auch alle Elemente Ihres cwd. Daher führt string = "1: 2: 3: 4: *" je nach Implementierung zu unerwarteten und möglicherweise gefährlichen Ergebnissen. Habe nicht den gleichen Fehler mit (IFS = ',' read -a array <<< "$ string") erhalten und dieser scheint sicher zu sein.
Dieter Gribnitz

4
Zitieren ${string//:/ }verhindert Shell-Erweiterung
Andrew White

1
Ich musste unter OSX Folgendes verwenden: array=(${string//:/ })
Mark Thomson

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

Druckt drei


8
Ich bevorzuge diesen Ansatz. Einfach.
Garnelenwagen

4
Ich habe dies kopiert und eingefügt und es hat nicht mit Echo funktioniert, aber es hat funktioniert, als ich es in einer for-Schleife verwendet habe.
Ben

2
Dies funktioniert nicht wie angegeben. @ Jmoney38 oder shrimpwagon Wenn Sie dies in ein Terminal einfügen und die gewünschte Ausgabe erhalten können, fügen Sie bitte das Ergebnis hier ein.
Abalter

2
@abalter Arbeitet für mich mit a=($(echo $t | tr ',' "\n")). Gleiches Ergebnis mit a=($(echo $t | tr ',' ' ')).
Blatt

@procrastinator Ich habe es gerade VERSION="16.04.2 LTS (Xenial Xerus)"in einer bashShell ausprobiert und der letzte echodruckt nur eine leere Zeile. Welche Linux-Version und welche Shell verwenden Sie? Terminalsitzung kann leider nicht in einem Kommentar angezeigt werden.
Abalter

29

Manchmal ist mir passiert, dass die in der akzeptierten Antwort beschriebene Methode nicht funktioniert hat, insbesondere wenn es sich bei dem Trennzeichen um einen Wagenrücklauf handelt.
In diesen Fällen habe ich folgendermaßen gelöst:

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"

for line in "${lines[@]}"
    do
        echo "--> $line"
done

2
+1 Das hat bei mir komplett funktioniert. Ich musste mehrere Zeichenfolgen, die durch einen Zeilenumbruch getrennt waren, in ein Array einfügen und read -a arr <<< "$strings"funktionierte nicht mit IFS=$'\n'.
Stefan van den Akker


Dies beantwortet die ursprüngliche Frage nicht ganz.
Mike

29

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 das viel einfachere Bash- Readarray :

readarray -t lines <<<"$string"

Das Drucken aller Zeilen ist sehr einfach und nutzt die Funktion printf:

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

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

2
Obwohl nicht jede Lösung für jede Situation funktioniert, hat Ihre Erwähnung von Readarray ... meine letzten zwei Stunden durch 5 Minuten ersetzt ... Sie haben meine Stimme erhalten
Angry 84


6

Der Schlüssel zum Aufteilen Ihrer Zeichenfolge in ein Array ist das Trennzeichen für mehrere Zeichen von ", ". Jede Lösung, die IFSfür Trennzeichen mit mehreren Zeichen verwendet wird, ist von Natur aus falsch, da IFS eine Menge dieser Zeichen und keine Zeichenfolge ist.

Wenn Sie zuweisen, IFS=", "wird die Zeichenfolge entweder bei ","ODER " "oder einer beliebigen Kombination davon unterbrochen , was keine genaue Darstellung des Zwei-Zeichen-Trennzeichens von ist ", ".

Sie können die Zeichenfolge mit awkoder seddurch Teilung des Prozesses verwenden oder teilen:

#!/bin/bash

str="Paris, France, Europe"
array=()
while read -r -d $'\0' each; do   # use a NUL terminated field separator 
    array+=("$each")
done < <(printf "%s" "$str" | awk '{ gsub(/,[ ]+|$/,"\0"); print }')
declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output

Es ist effizienter, einen regulären Ausdruck direkt in Bash zu verwenden:

#!/bin/bash

str="Paris, France, Europe"

array=()
while [[ $str =~ ([^,]+)(,[ ]+|$) ]]; do
    array+=("${BASH_REMATCH[1]}")   # capture the field
    i=${#BASH_REMATCH}              # length of field + delimiter
    str=${str:i}                    # advance the string by that length
done                                # the loop deletes $str, so make a copy if needed

declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output...

Bei der zweiten Form gibt es keine Unterschale und diese ist von Natur aus schneller.


Bearbeiten von bgoldst: Hier sind einige Benchmarks, die meine readarrayLösung mit der Regex-Lösung von dawg vergleichen , und ich habe auch die readLösung zum Teufel aufgenommen (Hinweis: Ich habe die Regex-Lösung leicht modifiziert, um eine bessere Harmonie mit meiner Lösung zu erzielen ) (siehe auch meine Kommentare unter Post):

## competitors
function c_readarray { readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); unset 'a[-1]'; };
function c_read { a=(); local REPLY=''; while read -r -d ''; do a+=("$REPLY"); done < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); };
function c_regex { a=(); local s="$1, "; while [[ $s =~ ([^,]+),\  ]]; do a+=("${BASH_REMATCH[1]}"); s=${s:${#BASH_REMATCH}}; done; };

## helper functions
function rep {
    local -i i=-1;
    for ((i = 0; i<$1; ++i)); do
        printf %s "$2";
    done;
}; ## end rep()

function testAll {
    local funcs=();
    local args=();
    local func='';
    local -i rc=-1;
    while [[ "$1" != ':' ]]; do
        func="$1";
        if [[ ! "$func" =~ ^[_a-zA-Z][_a-zA-Z0-9]*$ ]]; then
            echo "bad function name: $func" >&2;
            return 2;
        fi;
        funcs+=("$func");
        shift;
    done;
    shift;
    args=("$@");
    for func in "${funcs[@]}"; do
        echo -n "$func ";
        { time $func "${args[@]}" >/dev/null 2>&1; } 2>&1| tr '\n' '/';
        rc=${PIPESTATUS[0]}; if [[ $rc -ne 0 ]]; then echo "[$rc]"; else echo; fi;
    done| column -ts/;
}; ## end testAll()

function makeStringToSplit {
    local -i n=$1; ## number of fields
    if [[ $n -lt 0 ]]; then echo "bad field count: $n" >&2; return 2; fi;
    if [[ $n -eq 0 ]]; then
        echo;
    elif [[ $n -eq 1 ]]; then
        echo 'first field';
    elif [[ "$n" -eq 2 ]]; then
        echo 'first field, last field';
    else
        echo "first field, $(rep $[$1-2] 'mid field, ')last field";
    fi;
}; ## end makeStringToSplit()

function testAll_splitIntoArray {
    local -i n=$1; ## number of fields in input string
    local s='';
    echo "===== $n field$(if [[ $n -ne 1 ]]; then echo 's'; fi;) =====";
    s="$(makeStringToSplit "$n")";
    testAll c_readarray c_read c_regex : "$s";
}; ## end testAll_splitIntoArray()

## results
testAll_splitIntoArray 1;
## ===== 1 field =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.000s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 10;
## ===== 10 fields =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.001s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 100;
## ===== 100 fields =====
## c_readarray   real  0m0.069s   user 0m0.000s   sys  0m0.062s
## c_read        real  0m0.065s   user 0m0.000s   sys  0m0.046s
## c_regex       real  0m0.005s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 1000;
## ===== 1000 fields =====
## c_readarray   real  0m0.084s   user 0m0.031s   sys  0m0.077s
## c_read        real  0m0.092s   user 0m0.031s   sys  0m0.046s
## c_regex       real  0m0.125s   user 0m0.125s   sys  0m0.000s
##
testAll_splitIntoArray 10000;
## ===== 10000 fields =====
## c_readarray   real  0m0.209s   user 0m0.093s   sys  0m0.108s
## c_read        real  0m0.333s   user 0m0.234s   sys  0m0.109s
## c_regex       real  0m9.095s   user 0m9.078s   sys  0m0.000s
##
testAll_splitIntoArray 100000;
## ===== 100000 fields =====
## c_readarray   real  0m1.460s   user 0m0.326s   sys  0m1.124s
## c_read        real  0m2.780s   user 0m1.686s   sys  0m1.092s
## c_regex       real  17m38.208s   user 15m16.359s   sys  2m19.375s
##

Sehr coole Lösung! Ich habe nie daran gedacht, eine Schleife für ein Regex-Match zu verwenden $BASH_REMATCH. Es funktioniert und vermeidet in der Tat das Laichen von Unterschalen. +1 von mir. Aus Kritikgründen ist der reguläre Ausdruck selbst jedoch nicht ideal, da Sie anscheinend gezwungen waren, einen Teil des Trennzeichens (insbesondere das Komma) zu duplizieren, um die mangelnde Unterstützung für nicht gierige Multiplikatoren zu umgehen (auch Lookarounds) in ERE ("erweitertes" Regex-Aroma in Bash eingebaut). Dies macht es etwas weniger generisch und robust.
Bgoldst

Zweitens habe ich ein Benchmarking durchgeführt, und obwohl die Leistung besser ist als bei den anderen Lösungen für kleinere Saiten, verschlechtert sie sich exponentiell aufgrund des wiederholten Wiederaufbaus der Saiten, was für sehr große Saiten katastrophal wird. Siehe meine Bearbeitung Ihrer Antwort.
Bgoldst

@bgoldst: Was für ein cooler Benchmark! Zur Verteidigung des regulären Ausdrucks würde es für Zehntausende oder Hunderttausende von Feldern (was der reguläre Ausdruck aufteilt) wahrscheinlich irgendeine Form von Aufzeichnung (wie \nbegrenzte Textzeilen) geben, die diese Felder enthält, so dass die katastrophale Verlangsamung wahrscheinlich nicht auftreten würde. Wenn Sie eine Zeichenfolge mit 100.000 Feldern haben - vielleicht ist Bash nicht ideal ;-) Danke für den Benchmark. Ich habe ein oder zwei Dinge gelernt.
Morgengrauen

4

Reine Bash-Lösung mit mehreren Zeichen.

Wie andere in diesem Thread ausgeführt haben, gab die Frage des OP ein Beispiel für eine durch Kommas getrennte Zeichenfolge, die in ein Array analysiert werden soll, gab jedoch nicht an, ob er / sie nur an Komma-Trennzeichen, Einzelzeichen-Trennzeichen oder Mehrzeichen interessiert war Trennzeichen.

Da Google diese Antwort in der Regel an oder nahe der Spitze der Suchergebnisse platziert, wollte ich den Lesern eine eindeutige Antwort auf die Frage nach Trennzeichen für mehrere Zeichen geben, da dies auch in mindestens einer Antwort erwähnt wird.

Wenn Sie auf der Suche nach einer Lösung für ein Problem mit Trennzeichen für mehrere Zeichen sind, empfehle ich, den Beitrag von Mallikarjun M zu lesen , insbesondere die Antwort von gniourf_gniourf , der diese elegante reine BASH-Lösung mithilfe der Parametererweiterung bereitstellt:

#!/bin/bash
str="LearnABCtoABCSplitABCaABCString"
delimiter=ABC
s=$str$delimiter
array=();
while [[ $s ]]; do
    array+=( "${s%%"$delimiter"*}" );
    s=${s#*"$delimiter"};
done;
declare -p array

Link zum zitierten Kommentar / referenzierten Beitrag

Link zur zitierten Frage: Wie teilt man eine Zeichenfolge in einem mehrstelligen Trennzeichen in Bash?


1
Siehe meinen Kommentar für einen ähnlichen, aber verbesserten Ansatz.
Xebeche

3

Dies funktioniert für mich unter OSX:

string="1 2 3 4 5"
declare -a array=($string)

Wenn Ihre Zeichenfolge ein anderes Trennzeichen hat, ersetzen Sie diese zunächst durch Leerzeichen:

string="1,2,3,4,5"
delimiter=","
declare -a array=($(echo $string | tr "$delimiter" " "))

Einfach :-)


Funktioniert sowohl für Bash als auch für Zsh, was ein Plus ist!
Elijah W. Gagne

2

Eine andere Möglichkeit, dies zu tun, ohne IFS zu ändern:

read -r -a myarray <<< "${string//, /$IFS}"

Anstatt IFS so zu ändern, dass es mit unserem gewünschten Trennzeichen übereinstimmt, können wir alle Vorkommen unseres gewünschten Trennzeichens ", "durch Inhalte von $IFSvia ersetzen "${string//, /$IFS}".

Vielleicht ist dies für sehr große Saiten langsam?

Dies basiert auf Dennis Williamsons Antwort.


2

Ich bin auf diesen Beitrag gestoßen, als ich versucht habe, eine Eingabe wie die folgenden zu analysieren: word1, word2, ...

Keiner der oben genannten hat mir geholfen. löste es mit awk. Wenn es jemandem hilft:

STRING="value1,value2,value3"
array=`echo $STRING | awk -F ',' '{ s = $1; for (i = 2; i <= NF; i++) s = s "\n"$i; print s; }'`
for word in ${array}
do
        echo "This is the word $word"
done

1

Versuche dies

IFS=', '; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done

Es ist einfach. Wenn Sie möchten, können Sie auch eine Deklaration hinzufügen (und auch die Kommas entfernen):

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

Das IFS wird hinzugefügt, um das oben Gesagte rückgängig zu machen, funktioniert jedoch ohne es in einer neuen Bash-Instanz


1

Wir können den Befehl tr verwenden, um einen String in das Array-Objekt aufzuteilen. Es funktioniert sowohl unter MacOS als auch unter Linux

  #!/usr/bin/env bash
  currentVersion="1.0.0.140"
  arrayData=($(echo $currentVersion | tr "." "\n"))
  len=${#arrayData[@]}
  for (( i=0; i<=$((len-1)); i++ )); do 
       echo "index $i - value ${arrayData[$i]}"
  done

Eine andere Option ist der IFS-Befehl

IFS='.' read -ra arrayData <<< "$currentVersion"
#It is the same as tr
arrayData=($(echo $currentVersion | tr "." "\n"))

#Print the split string
for i in "${arrayData[@]}"
do
    echo $i
done

0

Benutze das:

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

#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe

3
Schlecht: vorbehaltlich Wortaufteilung und Pfadnamenerweiterung. Bitte beleben Sie alte Fragen nicht mit guten Antworten, um schlechte Antworten zu geben.
gniourf_gniourf

2
Dies mag eine schlechte Antwort sein, aber es ist immer noch eine gültige Antwort. Flagger / Rezensenten: Bei falschen Antworten wie dieser nicht abstimmen, nicht löschen!
Scott Weldon

2
@gniourf_gniourf Könnten Sie bitte erklären, warum es eine schlechte Antwort ist? Ich verstehe wirklich nicht, wann es fehlschlägt.
George Sovetov

3
@ GeorgeSovetov: Wie gesagt, es unterliegt der Wortteilung und der Erweiterung des Pfadnamens. Allgemeiner gesagtarray=( $string ) , das Aufteilen eines Strings in ein Array, wie es ein (leider sehr häufiges) Antimuster ist: Das Aufteilen von Wörtern erfolgt : string='Prague, Czech Republic, Europe'; Die Erweiterung des Pfadnamens tritt auf: schlägt string='foo[abcd],bar[efgh]'fehl, wenn Sie eine Datei mit dem Namen z. B. foododer barfin Ihrem Verzeichnis haben. Die einzig gültige Verwendung eines solchen Konstrukts ist, wenn stringes sich um einen Glob handelt.
gniourf_gniourf

0

UPDATE: Tun Sie dies nicht, da es Probleme mit der Bewertung gibt.

Mit etwas weniger Zeremonie:

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

z.B

string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar

4
eval ist böse! Tu das nicht.
Caesarsol

1
Pfft. Nein. Wenn Sie Skripte schreiben, die groß genug sind, um eine Rolle zu spielen, machen Sie es falsch. Im Anwendungscode ist eval böse. Bei Shell-Skripten ist dies häufig, notwendig und belanglos.
user1009908

2
Wenn Sie ein $in Ihre Variable eval
einfügen, werden

2
Sie haben Recht, dies kann nur verwendet werden, wenn bekannt ist, dass die Eingabe sauber ist. Keine robuste Lösung.
user1009908

Das einzige Mal, dass ich eval verwenden musste, war für eine Anwendung, die ihren eigenen Code / ihre eigenen Module selbst generierte ... UND dies hatte nie irgendeine Form von Benutzereingaben ...
Angry 84

0

Hier ist mein Hack!

Das Teilen von Strings durch Strings ist eine ziemlich langweilige Sache mit Bash. Was passiert ist, dass wir begrenzte Ansätze haben, die nur in wenigen Fällen funktionieren (geteilt durch ";", "/", "." Usw.) oder wir haben eine Vielzahl von Nebenwirkungen in den Ausgaben.

Der folgende Ansatz hat eine Reihe von Manövern erfordert, aber ich glaube, dass er für die meisten unserer Bedürfnisse funktionieren wird!

#!/bin/bash

# --------------------------------------
# SPLIT FUNCTION
# ----------------

F_SPLIT_R=()
f_split() {
    : 'It does a "split" into a given string and returns an array.

    Args:
        TARGET_P (str): Target string to "split".
        DELIMITER_P (Optional[str]): Delimiter used to "split". If not 
    informed the split will be done by spaces.

    Returns:
        F_SPLIT_R (array): Array with the provided string separated by the 
    informed delimiter.
    '

    F_SPLIT_R=()
    TARGET_P=$1
    DELIMITER_P=$2
    if [ -z "$DELIMITER_P" ] ; then
        DELIMITER_P=" "
    fi

    REMOVE_N=1
    if [ "$DELIMITER_P" == "\n" ] ; then
        REMOVE_N=0
    fi

    # NOTE: This was the only parameter that has been a problem so far! 
    # By Questor
    # [Ref.: https://unix.stackexchange.com/a/390732/61742]
    if [ "$DELIMITER_P" == "./" ] ; then
        DELIMITER_P="[.]/"
    fi

    if [ ${REMOVE_N} -eq 1 ] ; then

        # NOTE: Due to bash limitations we have some problems getting the 
        # output of a split by awk inside an array and so we need to use 
        # "line break" (\n) to succeed. Seen this, we remove the line breaks 
        # momentarily afterwards we reintegrate them. The problem is that if 
        # there is a line break in the "string" informed, this line break will 
        # be lost, that is, it is erroneously removed in the output! 
        # By Questor
        TARGET_P=$(awk 'BEGIN {RS="dn"} {gsub("\n", "3F2C417D448C46918289218B7337FCAF"); printf $0}' <<< "${TARGET_P}")

    fi

    # NOTE: The replace of "\n" by "3F2C417D448C46918289218B7337FCAF" results 
    # in more occurrences of "3F2C417D448C46918289218B7337FCAF" than the 
    # amount of "\n" that there was originally in the string (one more 
    # occurrence at the end of the string)! We can not explain the reason for 
    # this side effect. The line below corrects this problem! By Questor
    TARGET_P=${TARGET_P%????????????????????????????????}

    SPLIT_NOW=$(awk -F"$DELIMITER_P" '{for(i=1; i<=NF; i++){printf "%s\n", $i}}' <<< "${TARGET_P}")

    while IFS= read -r LINE_NOW ; do
        if [ ${REMOVE_N} -eq 1 ] ; then

            # NOTE: We use "'" to prevent blank lines with no other characters 
            # in the sequence being erroneously removed! We do not know the 
            # reason for this side effect! By Questor
            LN_NOW_WITH_N=$(awk 'BEGIN {RS="dn"} {gsub("3F2C417D448C46918289218B7337FCAF", "\n"); printf $0}' <<< "'${LINE_NOW}'")

            # NOTE: We use the commands below to revert the intervention made 
            # immediately above! By Questor
            LN_NOW_WITH_N=${LN_NOW_WITH_N%?}
            LN_NOW_WITH_N=${LN_NOW_WITH_N#?}

            F_SPLIT_R+=("$LN_NOW_WITH_N")
        else
            F_SPLIT_R+=("$LINE_NOW")
        fi
    done <<< "$SPLIT_NOW"
}

# --------------------------------------
# HOW TO USE
# ----------------

STRING_TO_SPLIT="
 * How do I list all databases and tables using psql?

\"
sudo -u postgres /usr/pgsql-9.4/bin/psql -c \"\l\"
sudo -u postgres /usr/pgsql-9.4/bin/psql <DB_NAME> -c \"\dt\"
\"

\"
\list or \l: list all databases
\dt: list all tables in the current database
\"

[Ref.: /dba/1285/how-do-i-list-all-databases-and-tables-using-psql]


"

f_split "$STRING_TO_SPLIT" "bin/psql -c"

# --------------------------------------
# OUTPUT AND TEST
# ----------------

ARR_LENGTH=${#F_SPLIT_R[*]}
for (( i=0; i<=$(( $ARR_LENGTH -1 )); i++ )) ; do
    echo " > -----------------------------------------"
    echo "${F_SPLIT_R[$i]}"
    echo " < -----------------------------------------"
done

if [ "$STRING_TO_SPLIT" == "${F_SPLIT_R[0]}bin/psql -c${F_SPLIT_R[1]}" ] ; then
    echo " > -----------------------------------------"
    echo "The strings are the same!"
    echo " < -----------------------------------------"
fi

0

Warum nicht so etwas für mehrzeilige Elemente?

$ array=($(echo -e $'a a\nb b' | tr ' ' '§')) && array=("${array[@]//§/ }") && echo "${array[@]/%/ INTERELEMENT}"

a a INTERELEMENT b b INTERELEMENT

-1

Ein anderer Weg wäre:

string="Paris, France, Europe"
IFS=', ' arr=(${string})

Jetzt werden Ihre Elemente im Array "arr" gespeichert. So durchlaufen Sie die Elemente:

for i in ${arr[@]}; do echo $i; done

1
Ich beschreibe diese Idee in meiner Antwort ; siehe Falsche Antwort Nr. 5 (Sie könnten besonders an meiner Diskussion über den evalTrick interessiert sein ). Ihre Lösung wird nachträglich $IFSauf den Komma-Leerzeichen-Wert gesetzt.
Bgoldst

-1

Da es so viele Möglichkeiten gibt, dies zu lösen, definieren wir zunächst, was wir in unserer Lösung sehen möchten.

  1. Bash bietet readarrayzu diesem Zweck eine integrierte Funktion. Lass es uns benutzen.
  2. Vermeiden Sie hässliche und unnötige Tricks wie das Ändern IFS, Schleifen, Verwenden evaloder Hinzufügen eines zusätzlichen Elements und das anschließende Entfernen.
  3. Finden Sie einen einfachen, lesbaren Ansatz, der leicht an ähnliche Probleme angepasst werden kann.

Der readarrayBefehl ist am einfachsten mit Zeilenumbrüchen als Trennzeichen zu verwenden. Bei anderen Trennzeichen kann dem Array ein zusätzliches Element hinzugefügt werden. Der sauberste Ansatz besteht darin, unsere Eingaben zunächst in ein Formular zu integrieren, mit readarraydem sie gut funktionieren, bevor sie weitergegeben werden.

Die Eingabe in diesem Beispiel hat kein Trennzeichen für mehrere Zeichen. Wenn wir ein wenig gesunden Menschenverstand anwenden, wird dies am besten als durch Kommas getrennte Eingabe verstanden, für die möglicherweise jedes Element zugeschnitten werden muss. Meine Lösung besteht darin, die Eingabe durch Komma in mehrere Zeilen aufzuteilen, jedes Element zu kürzen und alles an zu übergeben readarray.

string='  Paris,France  ,   All of Europe  '
readarray -t foo < <(tr ',' '\n' <<< "$string" |sed 's/^ *//' |sed 's/ *$//')
declare -p foo

# declare -a foo='([0]="Paris" [1]="France" [2]="All of Europe")'

-2

Ein anderer Ansatz kann sein:

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

Danach ist 'arr' ein Array mit vier Strings. Dies erfordert keinen Umgang mit IFS oder Lesen oder anderen speziellen Dingen, daher viel einfacher und direkter.


Gleiches (leider weit verbreitetes) Antimuster wie andere Antworten: vorbehaltlich Wortaufteilung und Dateinamenerweiterung.
gniourf_gniourf
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.