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 $IFS
Variablen wird nicht als einzelnes Zeichenfolgentrennzeichen mit variabler Länge verwendet, sondern als Satz von Zeichenfolgen-Trennzeichen mit einem Zeichen , wobei jedes Feld, read
das 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 $IFS
etwas 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 $IFS
kö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 $IFS
zusammen 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 $string
Variable irgendwelche LFs enthalten passiert, dann read
werden Beenden Sie die Verarbeitung, sobald der erste LF gefunden wird. Das read
eingebaute verarbeitet nur eine Zeile pro Aufruf. Dies gilt auch dann, wenn Sie Eingaben nur an die read
Anweisung 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 read
eingebaute 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. read
Dies 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 read
Systems nicht angemessen, und wir sollten dies vermeiden.
3: Ein nicht offensichtliches potenzielles Problem bei dieser Lösung besteht darin, dass read
das 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 read
verwendet die allgemeine Wortaufteilung ebenso wie die allgemeine $IFS
Variable 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 $IFS
Zeichen 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 extglob
gesetzt, 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 -f
zuvor 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 ( tr
oder sed
), da die Parametererweiterung eine rein shellinterne Operation ist. (Außerdem sollte für die tr
und sed
-Lösungen die Eingabevariable innerhalb der Befehlssubstitution in doppelte Anführungszeichen gesetzt werden. Andernfalls würde die Wortaufteilung im echo
Befehl 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 $IFS
wird 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 -f
und 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 -f
und 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 -f
und einschließen. set +f
Dann 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 $IFS
auf 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 eval
Aufruf völlig unnötig zu sein, da sein Argument ein Zeichenfolgenliteral in einfachen Anführungszeichen ist und daher statisch bekannt ist. Die Verwendung eval
auf 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 $IFS
nur vorübergehend vorzunehmen, wodurch wir das gesamte Spiel zum Speichern und Wiederherstellen vermeiden können, wie es bei der $OIFS
Variablen 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 $IFS
Zuweisung 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 $IFS
Zuweisung vorübergehend zu machen ? Dies funktioniert nicht, da die $array
Zuweisung 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 eval
der Code ausgeführt wird, wird er in der Shell-Umgebung ausgeführt, als wäre es normaler statischer Quellcode. Daher können wir die $array
Zuweisung innerhalb des eval
Arguments ausführen, damit sie in der Shell-Umgebung wirksam wird, während die $IFS
Präfixzuweisung dies tut wird dem eval
Befehl vorangestellt, überlebt den eval
Befehl 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 $IFS
Variable 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 $IFS
spezielle 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 read
eingebaute.
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 $IFS
Variablen, 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 compatxx
Shell-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 read
unangemessen 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 $IFS
der 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 read
besteht darin, null NAME- Argumente zu übergeben. In diesem Fall read
wird 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, read
die 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 -d
Option 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 read
ein 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 -d
Option 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 read
Fehler zurückgegeben hat als auch (2) $REPLY
leer 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 readarray
eingebaute Synonym für mapfile
ist 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 -O
nicht 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 read
beim Parsen von Feldern readarray
das 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 -d
Option 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 read
in # 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 readarray
das 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 callback
Option 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")
,
(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/…