Wie kann man eine Zeichenfolge in mehrere Zeichenfolgen aufteilen, die durch mindestens ein Leerzeichen in der Bash-Shell getrennt sind?

223

Ich habe eine Zeichenfolge mit vielen Wörtern mit mindestens einem Leerzeichen zwischen jeweils zwei. Wie kann ich die Zeichenfolge in einzelne Wörter aufteilen, um sie zu durchlaufen?

Die Zeichenfolge wird als Argument übergeben. Z.B${2} == "cat cat file" . Wie kann ich es durchlaufen?

Wie kann ich außerdem überprüfen, ob eine Zeichenfolge Leerzeichen enthält?

derrdji
quelle
1
Was für eine Muschel? Bash, cmd.exe, Powershell ...?
Alexey Sviridov
Müssen Sie nur eine Schleife ausführen (z. B. einen Befehl für jedes der Wörter ausführen)? Oder müssen Sie eine Liste mit Wörtern für die spätere Verwendung speichern?
DVK

Antworten:

280

Haben Sie versucht, die Zeichenfolgenvariable einfach an eine forSchleife zu übergeben? Zum einen wird Bash automatisch auf Leerzeichen aufgeteilt.

sentence="This is   a sentence."
for word in $sentence
do
    echo $word
done

 

This
is
a
sentence.
Mob
quelle
1
@MobRule - der einzige Nachteil davon ist, dass Sie die Ausgabe nicht einfach für die weitere Verarbeitung erfassen können (zumindest erinnere ich mich nicht an einen Weg). Siehe meine „tr“ Lösung unten für etwas , das Zeug zu STDOUT sendet
DVK
4
Sie können es einfach an eine Variable anhängen : A=${A}${word}).
Lucas Jones
1
setze $ text [dies setzt die Wörter in $ 1, $ 2, $ 3 ... usw.]
Rajesh
32
Tatsächlich ist dieser Trick nicht nur eine falsche Lösung, sondern auch extrem gefährlich, da die Schale global ist. touch NOPE; var='* a *'; for a in $var; do echo "[$a]"; doneAusgänge [NOPE] [a] [NOPE]anstelle der erwarteten [*] [a] [*](LFs werden aus Gründen der Lesbarkeit durch SPC ersetzt).
Tino
@mob was soll ich tun, wenn ich die Zeichenfolge basierend auf einer bestimmten Zeichenfolge teilen möchte? Beispiel ".xlsx" Trennzeichen.
295

Ich mag die Konvertierung in ein Array, um auf einzelne Elemente zugreifen zu können:

sentence="this is a story"
stringarray=($sentence)

Jetzt können Sie direkt auf einzelne Elemente zugreifen (es beginnt mit 0):

echo ${stringarray[0]}

oder konvertiere zurück in einen String, um eine Schleife zu erstellen:

for i in "${stringarray[@]}"
do
  :
  # do whatever on $i
done

Natürlich wurde das Durchlaufen der Zeichenfolge direkt zuvor beantwortet, aber diese Antwort hatte den Nachteil, dass die einzelnen Elemente für die spätere Verwendung nicht im Auge behalten wurden:

for i in $sentence
do
  :
  # do whatever on $i
done

Siehe auch Bash Array-Referenz .

Starker Wind
quelle
26
Leider nicht ganz perfekt, wegen Shell-Globbing: touch NOPE; var='* a *'; arr=($var); set | grep ^arr=Ausgänge arr=([0]="NOPE" [1]="a" [2]="NOPE")statt der erwartetenarr=([0]="*" [1]="a" [2]="*")
Tino
@Tino: Wenn Sie nicht möchten, dass Globbing stört, schalten Sie es einfach aus. Die Lösung funktioniert dann auch mit Platzhaltern. Es ist meiner Meinung nach der beste Ansatz.
Alexandros
3
@Alexandros Mein Ansatz ist es, nur Muster zu verwenden, die standardmäßig sicher sind und in jedem Kontext perfekt funktionieren. Die Anforderung, das Shell-Globbing zu ändern, um eine sichere Lösung zu erhalten, ist mehr als nur ein sehr gefährlicher Weg, es ist bereits die dunkle Seite. Mein Rat ist also, sich hier nie daran zu gewöhnen, ein solches Muster zu verwenden, da Sie früher oder später einige Details vergessen und dann jemand Ihren Fehler ausnutzt. Beweise für solche Exploits finden Sie in der Presse. Jeden. Single. Tag.
Tino
86

Verwenden Sie einfach die eingebauten Shells "Set". Beispielsweise,

setze $ text

Danach werden einzelne Wörter in $ text in $ 1, $ 2, $ 3 usw. angezeigt. Aus Gründen der Robustheit wird dies normalerweise der Fall sein

set - Junk $ Text
Verschiebung

um den Fall zu behandeln, in dem $ text leer ist, oder um mit einem Bindestrich zu beginnen. Beispielsweise:

text = "Dies ist ein Test"
set - Junk $ Text
Verschiebung
für Wort; machen
  Echo "[$ word]"
getan

Dies wird gedruckt

[Dies]
[ist]
[ein]
[Prüfung]
Idelic
quelle
5
Dies ist eine hervorragende Möglichkeit, die Variable so aufzuteilen, dass auf einzelne Teile direkt zugegriffen werden kann. +1; löste mein Problem
Cheekysoft
Ich wollte vorschlagen, awkaber es setist viel einfacher. Ich bin jetzt ein setFan. Danke @Idelic!
Yzmir Ramirez
22
Bitte beachten Sie das Shell-Globbing, wenn Sie solche Dinge tun: touch NOPE; var='* a *'; set -- $var; for a; do echo "[$a]"; doneAusgaben [NOPE] [a] [NOPE]anstelle der erwarteten [*] [a] [*]. Verwenden Sie es nur, wenn Sie zu 101% sicher sind, dass die geteilte Zeichenfolge keine SHELL-Metazeichen enthält!
Tino
4
@Tino: Dieses Problem tritt überall auf, nicht nur hier, sondern in diesem Fall können Sie das Globbing kurz set -fdavor set -- $varund set +fdanach deaktivieren.
Idelic
3
@ Idelic: Guter Fang. Mit set -fIhrer Lösung ist auch sicher. Ist set +faber die Standardeinstellung jeder Shell, so ist es ein wesentliches Detail, das beachtet werden muss, da andere es wahrscheinlich nicht wissen (wie ich es auch war).
Tino
81

Der wahrscheinlich einfachste und sicherste Weg in BASH 3 und höher ist:

var="string    to  split"
read -ra arr <<<"$var"

(Wo arrist das Array, das die geteilten Teile des Strings aufnimmt?) oder wenn die Eingabe möglicherweise Zeilenumbrüche enthält und Sie mehr als nur die erste Zeile möchten:

var="string    to  split"
read -ra arr -d '' <<<"$var"

(Bitte beachten Sie das Leerzeichen in -d '', es kann nicht weggelassen werden), aber dies kann zu einem unerwarteten Zeilenumbruch führen <<<"$var"(da dies implizit einen LF am Ende hinzufügt).

Beispiel:

touch NOPE
var="* a  *"
read -ra arr <<<"$var"
for a in "${arr[@]}"; do echo "[$a]"; done

Gibt das erwartete aus

[*]
[a]
[*]

da diese Lösung (im Gegensatz zu allen vorherigen Lösungen hier) nicht zu unerwartetem und oft unkontrollierbarem Shell-Globbing neigt.

Auch dies gibt Ihnen die volle Leistung von IFS, wie Sie wahrscheinlich wollen:

Beispiel:

IFS=: read -ra arr < <(grep "^$USER:" /etc/passwd)
for a in "${arr[@]}"; do echo "[$a]"; done

Gibt so etwas aus wie:

[tino]
[x]
[1000]
[1000]
[Valentin Hilbig]
[/home/tino]
[/bin/bash]

Wie Sie sehen, können Räume auch auf diese Weise erhalten werden:

IFS=: read -ra arr <<<' split  :   this    '
for a in "${arr[@]}"; do echo "[$a]"; done

Ausgänge

[ split  ]
[   this    ]

Bitte beachten Sie, dass der Umgang mit IFSin BASH ein eigenständiges Thema ist. Machen Sie also Ihre Tests, einige interessante Themen dazu:

  • unset IFS: Ignoriert Läufe von SPC, TAB, NL und startet und endet online
  • IFS='': Keine Feldtrennung, liest einfach alles
  • IFS=' ': Läufe von SPC (und nur SPC)

Ein letztes Beispiel

var=$'\n\nthis is\n\n\na test\n\n'
IFS=$'\n' read -ra arr -d '' <<<"$var"
i=0; for a in "${arr[@]}"; do let i++; echo "$i [$a]"; done

Ausgänge

1 [this is]
2 [a test]

während

unset IFS
var=$'\n\nthis is\n\n\na test\n\n'
read -ra arr -d '' <<<"$var"
i=0; for a in "${arr[@]}"; do let i++; echo "$i [$a]"; done

Ausgänge

1 [this]
2 [is]
3 [a]
4 [test]

Übrigens:

  • Wenn Sie nicht $'ANSI-ESCAPED-STRING'daran gewöhnt sind, ist dies eine Zeitersparnis.

  • Wenn Sie nicht einschließen -r(wie in read -a arr <<<"$var"), wird beim Lesen ein Backslash ausgeführt. Dies bleibt als Übung für den Leser.


Zur zweiten Frage:

Um auf etwas in einer Zeichenfolge zu testen, halte ich mich normalerweise daran case, da dies mehrere Fälle gleichzeitig prüfen kann (Hinweis: case führt nur die erste Übereinstimmung aus, wenn Sie mehrere caseAnweisungen verwenden müssen), und dies ist häufig der Fall (Wortspiel) beabsichtigt):

case "$var" in
'')                empty_var;;                # variable is empty
*' '*)             have_space "$var";;        # have SPC
*[[:space:]]*)     have_whitespace "$var";;   # have whitespaces like TAB
*[^-+.,A-Za-z0-9]*) have_nonalnum "$var";;    # non-alphanum-chars found
*[-+.,]*)          have_punctuation "$var";;  # some punctuation chars found
*)                 default_case "$var";;      # if all above does not match
esac

So können Sie den Rückgabewert so einstellen, dass nach SPC gesucht wird:

case "$var" in (*' '*) true;; (*) false;; esac

Warum case? Da es normalerweise etwas besser lesbar ist als Regex-Sequenzen und dank Shell-Metazeichen 99% aller Anforderungen sehr gut erfüllt.

Tino
quelle
2
Diese Antwort verdient aufgrund der hervorgehobenen globalen Probleme und ihrer Vollständigkeit mehr positive Stimmen
Brian Agnew
@brian Danke. Bitte beachten Sie, dass Sie Globbing verwenden set -foder set -o noglobumschalten können, sodass Shell-Metazeichen in diesem Zusammenhang keinen Schaden mehr anrichten. Aber ich bin nicht wirklich ein Freund davon, da dies viel Kraft der Shell hinterlässt / sehr fehleranfällig ist, diese Einstellung hin und her zu wechseln.
Tino
2
Wunderbare Antwort, verdient in der Tat mehr positive Stimmen. Randnotiz zum Fallfall des Falls - das können Sie ;&erreichen. Ich bin mir nicht ganz sicher, in welcher Version von Bash das aufgetaucht ist. Ich bin ein 4.3 Benutzer
Sergiy Kolodyazhnyy
2
@Serg danke fürs beachten, da ich das noch nicht wusste! Also habe ich es nachgeschlagen, es erschien in Bash4 . ;&ist das erzwungene Durchfallen ohne Musterprüfung wie in C. Und es gibt auch das, ;;&was gerade die weiteren Musterprüfungen fortsetzt. So ;;ist wie if ..; then ..; else if ..und ;;&ist wie if ..; then ..; fi; if .., wo ;&ist wie m=false; if ..; then ..; m=:; fi; if $m || ..; then ..- man hört nie auf zu lernen (von anderen);)
Tino
@Tino Das ist absolut richtig - Lernen ist ein kontinuierlicher Prozess. Tatsächlich wusste ich nicht, ;;&bevor Sie kommentierten: D Danke, und möge die Muschel bei Ihnen sein;)
Sergiy Kolodyazhnyy
43
$ echo "This is   a sentence." | tr -s " " "\012"
This
is
a
sentence.

Verwenden Sie grep, um nach Leerzeichen zu suchen:

$ echo "This is   a sentence." | grep " " > /dev/null
$ echo $?
0
$ echo "Thisisasentence." | grep " " > /dev/null     
$ echo $?
1
DVK
quelle
1
In BASH echo "X" |kann in der Regel <<<"X"wie folgt ersetzt werden : grep -s " " <<<"This contains SPC". Sie können den Unterschied erkennen, wenn Sie so etwas wie echo X | read varim Gegensatz zu tun read var <<< X. Nur letztere importiert Variablen varin die aktuelle Shell, während Sie in der ersten Variante wie folgt darauf gruppieren müssen:echo X | { read var; handle "$var"; }
Tino
17

(A) Um einen Satz in seine Wörter aufzuteilen (durch Leerzeichen getrennt), können Sie einfach das Standard-IFS verwenden, indem Sie verwenden

array=( $string )


Beispiel für das Ausführen des folgenden Snippets

#!/bin/bash

sentence="this is the \"sentence\"   'you' want to split"
words=( $sentence )

len="${#words[@]}"
echo "words counted: $len"

printf "%s\n" "${words[@]}" ## print array

wird ausgegeben

words counted: 8
this
is
the
"sentence"
'you'
want
to
split

Wie Sie sehen, können Sie problemlos auch einfache oder doppelte Anführungszeichen verwenden.

Hinweise:
- Dies ist im Grunde die gleiche Antwort wie bei Mob , aber auf diese Weise speichern Sie das Array für weitere Anforderungen. Wenn Sie nur eine einzelne Schleife benötigen, können Sie seine Antwort verwenden, die eine Zeile kürzer ist :)
- In dieser Frage finden Sie alternative Methoden zum Teilen einer Zeichenfolge basierend auf dem Trennzeichen.


(B) Um nach einem Zeichen in einer Zeichenfolge zu suchen, können Sie auch eine Übereinstimmung mit regulären Ausdrücken verwenden.
Beispiel zum Überprüfen des Vorhandenseins eines Leerzeichens, das Sie verwenden können:

regex='\s{1,}'
if [[ "$sentence" =~ $regex ]]
    then
        echo "Space here!";
fi
Luca Borrione
quelle
Für Regex-Hinweis (B) a +1, aber -1 für falsche Lösung (A), da dies fehleranfällig für Shell-Globbing ist. ;)
Tino
6

Zum Überprüfen von Leerzeichen nur mit Bash:

[[ "$str" = "${str% *}" ]] && echo "no spaces" || echo "has spaces"
Glenn Jackman
quelle
1
echo $WORDS | xargs -n1 echo

Dadurch wird jedes Wort ausgegeben. Sie können diese Liste nach Belieben verarbeiten.

Alex
quelle