2924 Stimmen

Wie splitte ich eine Zeichenkette an einem Begrenzer in der Bash?

Ich habe diese Zeichenfolge in einer Variablen gespeichert:

IN="bla@some.com;john@home.com"

Nun möchte ich die Zeichenketten aufteilen nach ; Begrenzungszeichen, so dass ich habe:

ADDR1="bla@some.com"
ADDR2="john@home.com"

Ich brauche nicht unbedingt die ADDR1 y ADDR2 Variablen. Wenn sie Elemente eines Arrays sind, ist das sogar noch besser.


Nach den Vorschlägen aus den nachstehenden Antworten kam ich zu folgendem Ergebnis, das dem entspricht, was ich wollte:

#!/usr/bin/env bash

IN="bla@some.com;john@home.com"

mails=$(echo $IN | tr ";" "\n")

for addr in $mails
do
    echo "> [$addr]"
done

Ausgabe:

> [bla@some.com]
> [john@home.com]

Es gab eine Lösung, die die Einstellung Interner_Feld_Trenner (IFS) an ; . Ich bin mir nicht sicher, was mit dieser Antwort passiert ist, wie setzt man IFS zurück zum Standard?

RE: IFS Lösung, ich habe das ausprobiert und es funktioniert, ich behalte die alte IFS und dann wiederherstellen:

IN="bla@some.com;john@home.com"

OIFS=$IFS
IFS=';'
mails2=$IN
for x in $mails2
do
    echo "> [$x]"
done

IFS=$OIFS

Übrigens, als ich versuchte

mails2=($IN)

Ich habe nur die erste Zeichenkette erhalten, wenn ich sie in einer Schleife ohne Klammern ausgedruckt habe $IN es funktioniert.

30 Stimmen

In Bezug auf Ihr "Edit2": Sie können einfach "IFS aufheben" und der Standardzustand wird wiederhergestellt. Es besteht keine Notwendigkeit, ihn explizit zu speichern und wiederherzustellen, es sei denn, Sie haben Grund zu der Annahme, dass er bereits auf einen anderen Wert als den Standardwert gesetzt wurde. Wenn Sie dies innerhalb einer Funktion tun (und wenn Sie es nicht tun, warum nicht?), können Sie IFS als lokale Variable setzen, die nach dem Verlassen der Funktion auf ihren vorherigen Wert zurückgesetzt wird.

33 Stimmen

@BrooksMoses: (a) +1 für die Verwendung local IFS=... wenn möglich; (b) -1 für unset IFS wird IFS nicht genau auf seinen Standardwert zurückgesetzt, obwohl ich glaube, dass sich ein nicht gesetzter IFS genauso verhält wie der Standardwert von IFS ($' \t\n '), aber es scheint eine schlechte Praxis zu sein, blind davon auszugehen, dass Ihr Code niemals mit einem benutzerdefinierten IFS-Wert aufgerufen werden wird; (c) eine andere Idee ist, eine Subshell aufzurufen: (IFS=$custom; ...) Wenn die Subshell beendet wird, kehrt IFS zu dem ursprünglichen Zustand zurück.

0 Stimmen

Ich möchte nur einen kurzen Blick auf die Pfade werfen, um zu entscheiden, wohin ich eine ausführbare Datei werfen soll, also habe ich auf die Ausführung von ruby -e "puts ENV.fetch('PATH').split(':')" . Wenn Sie reine Bash bleiben wollen, hilft es nicht, wenn Sie eine beliebige Skriptsprache die eine eingebaute Aufteilung hat, ist einfacher.

1684voto

Johannes Schaub - litb Punkte 479831

Sie können die interner Feldtrenner (IFS)-Variable, und lassen Sie sie dann in ein Array parsen. Wenn dies in einem Befehl geschieht, dann wird die Zuweisung an IFS findet nur in der Umgebung dieses einzelnen Befehls statt (in read ). Anschließend wird die Eingabe entsprechend der IFS Variablenwert in ein Array, das wir dann durchlaufen können.

In diesem Beispiel wird eine Zeile mit Elementen geparst, die durch ; und schiebt sie in ein Array:

IFS=';' read -ra ADDR <<< "$IN"
for i in "${ADDR[@]}"; do
  # process "$i"
done

Das andere Beispiel bezieht sich auf die Verarbeitung des gesamten Inhalts von $IN , jeweils eine Eingabezeile getrennt durch ; :

while IFS=';' read -ra ADDR; do
  for i in "${ADDR[@]}"; do
    # process "$i"
  done
done <<< "$IN"

35 Stimmen

Dies ist wahrscheinlich der beste Weg. Wie lange bleibt IFS in seinem aktuellen Wert bestehen, kann es meinen Code durcheinander bringen, indem es gesetzt wird, wenn es nicht sein sollte, und wie kann ich es zurücksetzen, wenn ich mit ihm fertig bin?

16 Stimmen

Jetzt nach der Korrektur nur noch während der Dauer des Lesebefehls :)

1 Stimmen

Ich wusste, dass es eine Möglichkeit gibt, mit Arrays zu arbeiten, ich konnte mich nur nicht mehr daran erinnern, was es war. Ich mag die Einstellung der IFS, aber ich bin nicht sicher, mit der Umleitung von $IN und gehen Sie durch lesen, nur um Array aufzufüllen. Ist nicht einfach IFS wiederherstellen einfacher? Auf jeden Fall +1 von IFS Vorschlag, danke.

1611voto

palindrom Punkte 15441

Entnommen aus Bash-Shell-Skript Split-Array :

IN="bla@some.com;john@home.com"
arrIN=(${IN//;/ })
echo ${arrIN[1]}                  # Output: john@home.com

Erläuterung:

Diese Konstruktion ersetzt alle Vorkommen von ';' (die ursprüngliche // bedeutet global ersetzen) in der Zeichenfolge IN mit ' ' (ein einzelnes Leerzeichen) und interpretiert dann die durch Leerzeichen begrenzte Zeichenkette als Array (dafür sorgen die umgebenden Klammern).

Die Syntax, die innerhalb der geschweiften Klammern verwendet wird, um die einzelnen ';' Zeichen mit einer ' ' Zeichen wird genannt Parameter Erweiterung .

Es gibt einige häufige Probleme:

  1. Wenn die ursprüngliche Zeichenkette Leerzeichen enthält, müssen Sie IFS :
  • IFS=':'; arrIN=($IN); unset IFS;
  1. Wenn die ursprüngliche Zeichenkette Leerzeichen enthält y das Begrenzungszeichen eine neue Zeile ist, können Sie IFS mit:
  • IFS=$'\n'; arrIN=($IN); unset IFS;

121 Stimmen

Ich möchte noch hinzufügen, dass dies die einfachste aller Möglichkeiten ist: Sie können auf Array-Elemente mit ${arrIN[1]} zugreifen (natürlich beginnend mit Nullen)

36 Stimmen

Gefunden: Die Technik, eine Variable innerhalb eines ${} zu ändern, wird als "Parametererweiterung" bezeichnet.

10 Stimmen

Wenn Sie an einem Sonderzeichen wie z. B. der Tilde (~) teilen wollen, achten Sie darauf, es zu entwerten: arrIN=(${IN//\~/ })

552voto

DougW Punkte 26369

Ich habe ein paar Antworten gesehen, die sich auf die cut aber sie sind alle gelöscht worden. Es ist etwas seltsam, dass niemand darauf eingegangen ist, denn ich denke, es ist einer der nützlicheren Befehle für diese Art von Aufgaben, insbesondere für das Parsen von abgegrenzten Protokolldateien.

Im Falle der Aufteilung dieses speziellen Beispiels in ein Bash-Skript-Array, tr ist wahrscheinlich effizienter, aber cut kann verwendet werden und ist effektiver, wenn Sie bestimmte Felder aus der Mitte ziehen wollen.

Beispiel:

$ echo "bla@some.com;john@home.com" | cut -d ";" -f 1
bla@some.com
$ echo "bla@some.com;john@home.com" | cut -d ";" -f 2
john@home.com

Sie können das natürlich in eine Schleife einfügen und den Parameter -f iterieren, um jedes Feld unabhängig voneinander abzurufen.

Dies ist besonders nützlich, wenn Sie eine durch Trennzeichen getrennte Protokolldatei mit Zeilen wie dieser haben:

2015-04-27|12345|some action|an attribute|meta data

cut ist es sehr praktisch, die Möglichkeit zu haben cat diese Datei und wählen Sie ein bestimmtes Feld für die weitere Bearbeitung aus.

41 Stimmen

Hut ab vor der Verwendung cut Es ist das richtige Werkzeug für den Job! Viel sauberer als jeder dieser Shell-Hacks.

11 Stimmen

Dieser Ansatz funktioniert nur, wenn Sie die Anzahl der Elemente im Voraus kennen; Sie müssten eine weitere Logik dafür programmieren. Außerdem wird für jedes Element ein externes Tool ausgeführt.

1 Stimmen

Genau das, wonach ich gesucht habe, um leere Zeichenfolgen in einer CSV-Datei zu vermeiden. Jetzt kann ich auch den genauen 'Spalten'-Wert angeben. Arbeit mit IFS bereits in einer Schleife verwendet. Besser als erwartet für meine Situation.

385voto

Chris Lutz Punkte 69879

Wenn es Ihnen nichts ausmacht, sie sofort zu verarbeiten, mache ich das gerne:

for i in $(echo $IN | tr ";" "\n")
do
  # process
done

Sie könnten diese Art von Schleife verwenden, um ein Array zu initialisieren, aber es gibt wahrscheinlich einen einfacheren Weg, dies zu tun.

0 Stimmen

Sie hätten die IFS-Antwort behalten sollen. Sie hat mich etwas gelehrt, was ich nicht wusste, und sie hat definitiv eine Reihe gebildet, während dies nur ein billiger Ersatz ist.

0 Stimmen

Ich verstehe. Ja, ich finde, wenn ich diese dummen Experimente mache, lerne ich jedes Mal neue Dinge, wenn ich versuche, Dinge zu beantworten. Ich habe Sachen auf der Grundlage von #bash IRC-Feedback bearbeitet und wieder gelöscht :)

4 Stimmen

Sie könnten es ändern in echo "$IN" | tr ';' ' \n ' | while read -r ADDY; do # process "$ADDY"; done um ihn glücklich zu machen, denke ich :) Beachten Sie, dass dies zu einem Fork führt und Sie die äußeren Variablen innerhalb der Schleife nicht ändern können (deshalb habe ich die <<< "$IN"-Syntax verwendet).

366voto

F. Hauri Punkte 57640

Kompatible Antwort

Es gibt viele verschiedene Möglichkeiten, dies zu tun in bash .

Es ist jedoch wichtig, zunächst festzustellen, dass bash hat viele Spezial Merkmale (sogenannte Bashismen ), die in keinem anderen Programm funktionieren Shell .

Im Besonderen, Arrays , assoziative Arrays und Mustersubstitution die in den Lösungen in diesem Beitrag und in anderen Beiträgen verwendet werden, sind Bashismen und funktioniert möglicherweise nicht unter anderen Muscheln die viele Menschen nutzen.

Zum Beispiel: auf meinem Debian GNU/Linux gibt es eine Standard Shell genannt Bindestrich Ich kenne viele Leute, die gerne eine andere Shell benutzen, die ksh ; und es gibt auch ein spezielles Werkzeug namens busybox mit seinem eigenen Shell-Interpreter ( Asche ).

Gewünschte Zeichenfolge

Die aufzuteilende Zeichenfolge in der obigen Frage lautet:

IN="bla@some.com;john@home.com"

Ich werde eine geänderte Version dieser Zeichenkette verwenden, um sicherzustellen, dass meine Lösung robust gegenüber Zeichenketten mit Leerzeichen ist, was bei anderen Lösungen zu Problemen führen könnte:

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"

Zeichenkette anhand des Begrenzungszeichens in bash (Version >=4.2)

Unter rein bash können wir eine Array mit Elementen, die durch einen temporären Wert für IFS (die Eingabefeldtrennzeichen ). Der IFS sagt unter anderem bash welche(s) Zeichen bei der Definition eines Arrays als Trennzeichen zwischen den Elementen verwendet werden soll:

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"

# save original IFS value so we can restore it later
oIFS="$IFS"
IFS=";"
declare -a fields=($IN)
IFS="$oIFS"
unset oIFS

In neueren Versionen von bash Wenn einem Befehl eine IFS-Definition vorangestellt wird, ändert sich der IFS für diesen Befehl. nur und setzt ihn unmittelbar danach wieder auf den vorherigen Wert zurück. Das bedeutet, dass wir das oben Genannte in nur einer Zeile erledigen können:

IFS=\; read -a fields <<<"$IN"
# after this command, the IFS resets back to its previous value (here, the default):
set | grep ^IFS=
# IFS=$' \t\n'

Wir können sehen, dass die Zeichenfolge IN wurde in einem Array mit dem Namen fields , getrennt durch die Semikolons:

set | grep ^fields=\\\|^IN=
# fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name <fulnam@other.org>")
# IN='bla@some.com;john@home.com;Full Name <fulnam@other.org>'

(Wir können den Inhalt dieser Variablen auch anzeigen, indem wir declare -p :)

declare -p IN fields
# declare -- IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
# declare -a fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name <fulnam@other.org>")

Beachten Sie, dass read ist die schnellste Art und Weise, den Split durchzuführen, weil es keine Gabeln oder externe Ressourcen aufgerufen.

Sobald das Array definiert ist, können Sie eine einfache Schleife verwenden, um jedes Feld (oder besser gesagt, jedes Element in dem Array, das Sie jetzt definiert haben) zu verarbeiten:

# `"${fields[@]}"` expands to return every element of `fields` array as a separate argument
for x in "${fields[@]}" ;do
    echo "> [$x]"
    done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

Oder Sie könnten jedes Feld nach der Verarbeitung aus dem Array löschen, indem Sie eine wechseln. Ansatz, der mir gefällt:

while [ "$fields" ] ;do
    echo "> [$fields]"
    # slice the array 
    fields=("${fields[@]:1}")
    done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

Und wenn Sie nur einen einfachen Ausdruck des Arrays wünschen, müssen Sie nicht einmal eine Schleife darüber laufen lassen:

printf "> [%s]\n" "${fields[@]}"
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

Update: kürzlich bash >= 4.4

In neueren Versionen von bash können Sie auch mit dem Befehl mapfile :

mapfile -td \; fields < <(printf "%s\0" "$IN")

Bei dieser Syntax bleiben Sonderzeichen, Zeilenumbrüche und leere Felder erhalten!

Wenn Sie keine leeren Felder einfügen möchten, können Sie wie folgt vorgehen:

mapfile -td \; fields <<<"$IN"
fields=("${fields[@]%$'\n'}")   # drop '\n' added by '<<<'

Mit mapfile können Sie auch auf die Deklaration eines Arrays verzichten und implizit eine Schleife über die abgegrenzten Elemente ziehen, indem Sie für jedes Element eine Funktion aufrufen:

myPubliMail() {
    printf "Seq: %6d: Sending mail to '%s'..." $1 "$2"
    # mail -s "This is not a spam..." "$2" </path/to/body
    printf "\e[3D, done.\n"
}

mapfile < <(printf "%s\0" "$IN") -td \; -c 1 -C myPubliMail

(Anmerkung: Die \0 am Ende der Formatzeichenkette ist nutzlos, wenn Sie sich nicht um leere Felder am Ende der Zeichenkette kümmern oder diese nicht vorhanden sind).

mapfile < <(echo -n "$IN") -td \; -c 1 -C myPubliMail

# Seq:      0: Sending mail to 'bla@some.com', done.
# Seq:      1: Sending mail to 'john@home.com', done.
# Seq:      2: Sending mail to 'Full Name <fulnam@other.org>', done.

Oder könnten Sie verwenden <<< und fügen Sie in den Funktionskörper eine Verarbeitung ein, um den hinzugefügten Zeilenumbruch zu entfernen:

myPubliMail() {
    local seq=$1 dest="${2%$'\n'}"
    printf "Seq: %6d: Sending mail to '%s'..." $seq "$dest"
    # mail -s "This is not a spam..." "$dest" </path/to/body
    printf "\e[3D, done.\n"
}

mapfile <<<"$IN" -td \; -c 1 -C myPubliMail

# Renders the same output:
# Seq:      0: Sending mail to 'bla@some.com', done.
# Seq:      1: Sending mail to 'john@home.com', done.
# Seq:      2: Sending mail to 'Full Name <fulnam@other.org>', done.

Zeichenkette anhand des Begrenzungszeichens in Shell

Wenn Sie nicht verwenden können bash oder wenn Sie etwas schreiben wollen, das in vielen verschiedenen Shells verwendet werden kann, können Sie oft kann nicht verwenden. Bashismen -- und dazu gehören auch die Arrays, die wir in den obigen Lösungen verwendet haben.

Wir müssen jedoch keine Arrays verwenden, um eine Schleife über "Elemente" einer Zeichenkette zu ziehen. Es gibt eine Syntax, die in vielen Shells verwendet wird, um Teilstrings einer Zeichenkette aus der erste o zuletzt Auftreten eines Musters. Beachten Sie, dass * ist ein Platzhalter, der für null oder mehr Zeichen steht:

(Das Fehlen dieses Ansatzes in allen bisher veröffentlichten Lösungen ist der Hauptgrund, warum ich diese Antwort schreibe ;)

${var#*SubStr}  # drops substring from start of string up to first occurrence of `SubStr`
${var##*SubStr} # drops substring from start of string up to last occurrence of `SubStr`
${var%SubStr*}  # drops substring from last occurrence of `SubStr` to end of string
${var%%SubStr*} # drops substring from first occurrence of `SubStr` to end of string

Wie von Ergebnis_Unter :

# y % die kürzestmögliche übereinstimmende Teilzeichenkette aus der Datei Start y Ende der Zeichenkette, und

## y %% die längste übereinstimmende Teilzeichenkette löschen.

Mit der obigen Syntax können wir einen Ansatz erstellen, bei dem wir Teilstrings "Elemente" aus der Zeichenkette extrahieren, indem wir die Teilstrings bis zum oder nach dem Begrenzer löschen.

Der folgende Codeblock funktioniert gut in bash (einschließlich Mac OS's bash ), Bindestrich , ksh und busybox 's Asche :

(Dank an Adam Katz 's Kommentar Dadurch wird diese Schleife viel einfacher!)

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
while [ "$IN" != "$iter" ] ;do
    # extract the substring from start of string up to delimiter.
    iter=${IN%%;*}
    # delete this first "element" AND next separator, from $IN.
    IN="${IN#$iter;}"
    # Print (or doing anything with) the first "element".
    echo "> [$iter]"
done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

Viel Spaß!

32 Stimmen

El # , ## , % y %% Ersetzungen haben eine Erklärung, die IMO leichter zu merken ist (für die Menge, die sie löschen): # y % die kürzestmögliche übereinstimmende Zeichenfolge löschen und ## y %% so lange wie möglich löschen.

1 Stimmen

El IFS=\; read -a fields <<<"$var" schlägt bei Zeilenumbrüchen fehl und fügt einen abschließenden Zeilenumbruch hinzu. Bei der anderen Lösung wird ein leeres Feld am Ende entfernt.

0 Stimmen

Könnte die letzte Alternative mit einer Liste von Feldtrennern verwendet werden, die an anderer Stelle festgelegt wurde? Ich möchte dies zum Beispiel als Shell-Skript verwenden und eine Liste von Feldtrennzeichen als Positionsparameter übergeben.

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