Wie führe ich eine for-Schleife für jedes Zeichen in einer Zeichenfolge in Bash durch?

82

Ich habe eine Variable wie diese:

words="这是一条狗。"

Ich möchte eine auf jedes der Zeichen für Schleife machen, einer nach dem anderen, zum Beispiel zuerst character="这", dann character="是", character="一"usw.

Die einzige Möglichkeit, die ich kenne, besteht darin, jedes Zeichen in einer separaten Zeile in einer Datei auszugeben und dann zu verwenden while read line. Dies scheint jedoch sehr ineffizient zu sein.

  • Wie kann ich jedes Zeichen in einer Zeichenfolge durch eine for-Schleife verarbeiten?
Dorf
quelle
3
Es könnte erwähnenswert sein, dass wir viele Fragen von Neulingen sehen, bei denen das OP der Meinung ist, dass dies das ist, was sie tun möchten. Sehr oft ist eine bessere Lösung möglich, bei der nicht jedes Zeichen einzeln verarbeitet werden muss. Dies wird als bekannt XY Problem und die richtige Lösung ist , zu erklären , was Sie wirklich wollen , erreichen in Ihrer Frage, nicht nur , wie die Schritte ausführen Sie denken , wird Ihnen helfen , sich dort.
Tripleee

Antworten:

45

Mit sedon dashshell of habe LANG=en_US.UTF-8ich Folgendes richtig gemacht:

$ echo "你好嗎 新年好。全型句號" | sed -e 's/\(.\)/\1\n/g'
你
好
嗎

新
年
好
。
全
型
句
號

und

$ echo "Hello world" | sed -e 's/\(.\)/\1\n/g'
H
e
l
l
o

w
o
r
l
d

Somit kann die Ausgabe mit wiederholt werden while read ... ; do ... ; done

bearbeitet für Beispieltext ins Englische übersetzen:

"你好嗎 新年好。全型句號" is zh_TW.UTF-8 encoding for:
"你好嗎"     = How are you[ doing]
" "         = a normal space character
"新年好"     = Happy new year
"。全型空格" = a double-byte-sized full-stop followed by text description
Rony
quelle
4
Gute Mühe mit UTF-8. Ich brauchte es nicht, aber du bekommst trotzdem meine Gegenstimme.
Jordanien
+1 Sie können die for-Schleife für die resultierende Zeichenfolge von sed verwenden.
Tyzoid
233

Sie können eine forSchleife im C-Stil verwenden :

foo=string
for (( i=0; i<${#foo}; i++ )); do
  echo "${foo:$i:1}"
done

${#foo}erweitert sich auf die Länge von foo. ${foo:$i:1}dehnt sich ab Position $ider Länge 1 zum Teilstring aus.

chepner
quelle
Warum benötigen Sie zwei Sätze von Klammern um die for-Anweisung, damit sie funktioniert?
tgun926
Das ist die Syntax, die bashbenötigt wird.
Chepner
3
Ich weiß, dass dies alt ist, aber die beiden Klammern sind erforderlich, weil sie arithmetische Operationen ermöglichen. Siehe hier => tldp.org/LDP/abs/html/dblparens.html
Hannibal
8
@Hannibal Ich wollte nur darauf hinweisen, dass diese spezielle Verwendung von doppelten Klammern tatsächlich das Bash-Konstrukt ist: for (( _expr_ ; _expr_ ; _expr_ )) ; do _command_ ; doneund nicht dasselbe wie $ (( Ausdruck )) oder (( Ausdruck )). In allen drei Bash-Konstrukten wird expr gleich behandelt und $ (( expr )) ist ebenfalls POSIX.
Nabin-Info
1
@codeforester Das hat nichts mit Arrays zu tun; Es ist nur einer von vielen Ausdrücken bash, die in einem arithmetischen Kontext ausgewertet werden.
Chepper
36

${#var} gibt die Länge von zurück var

${var:pos:N}Zeichen kehrt N von posvorwärts

Beispiele:

$ words="abc"
$ echo ${words:0:1}
a
$ echo ${words:1:1}
b
$ echo ${words:2:1}
c

so ist es einfach zu iterieren.

ein anderer Weg:

$ grep -o . <<< "abc"
a
b
c

oder

$ grep -o . <<< "abc" | while read letter;  do echo "my letter is $letter" ; done 

my letter is a
my letter is b
my letter is c
Tiago Peczenyj
quelle
1
Was ist mit Leerzeichen?
Leandro
Was ist mit Leerzeichen? Ein Leerzeichen ist ein Zeichen, das alle Zeichen durchläuft. (Obwohl Sie darauf achten sollten, doppelte Anführungszeichen um jede Variable oder Zeichenfolge zu verwenden, die signifikante Leerzeichen enthält. Im Allgemeinen zitieren Sie immer alles, es sei denn, Sie wissen, was Sie tun. )
Tripleee
23

Ich bin überrascht, dass niemand die offensichtliche bashLösung erwähnt hat, die nur whileund verwendet read.

while read -n1 character; do
    echo "$character"
done < <(echo -n "$words")

Beachten Sie die Verwendung von echo -n, um die überflüssige neue Zeile am Ende zu vermeiden. printfist eine weitere gute Option und möglicherweise besser für Ihre speziellen Bedürfnisse geeignet. Wenn Sie Leerzeichen ignorieren möchten, ersetzen Sie diese "$words"durch "${words// /}".

Eine andere Option ist fold. Bitte beachten Sie jedoch, dass es niemals in eine for-Schleife eingespeist werden sollte. Verwenden Sie stattdessen eine while-Schleife wie folgt:

while read char; do
    echo "$char"
done < <(fold -w1 <<<"$words")

Der Hauptvorteil der Verwendung des externen foldBefehls (des Coreutils- Pakets) wäre die Kürze. Sie können die Ausgabe wie folgt einem anderen Befehl wie xargs(Teil des findutils- Pakets) zuführen :

fold -w1 <<<"$words" | xargs -I% -- echo %

Sie möchten den echoim obigen Beispiel verwendeten Befehl durch den Befehl ersetzen, den Sie für jedes Zeichen ausführen möchten . Beachten Sie, dass xargsLeerzeichen standardmäßig verworfen werden. Sie können -d '\n'dieses Verhalten deaktivieren.


Internationalisierung

Ich habe gerade foldmit einigen asiatischen Charakteren getestet und festgestellt, dass es keine Unicode-Unterstützung gibt. Obwohl es für ASCII-Anforderungen in Ordnung ist, funktioniert es nicht für alle. In diesem Fall gibt es einige Alternativen.

Ich würde wahrscheinlich durch fold -w1ein awk-Array ersetzen :

awk 'BEGIN{FS=""} {for (i=1;i<=NF;i++) print $i}'

Oder der grepin einer anderen Antwort erwähnte Befehl:

grep -o .


Performance

Zu Ihrer Information, ich habe die 3 oben genannten Optionen verglichen. Die ersten beiden waren schnell und fast gebunden, wobei die Faltschleife etwas schneller war als die while-Schleife. Es überrascht nicht, dass xargses am langsamsten war ... 75x langsamer.

Hier ist der (abgekürzte) Testcode:

words=$(python -c 'from string import ascii_letters as l; print(l * 100)')

testrunner(){
    for test in test_while_loop test_fold_loop test_fold_xargs test_awk_loop test_grep_loop; do
        echo "$test"
        (time for (( i=1; i<$((${1:-100} + 1)); i++ )); do "$test"; done >/dev/null) 2>&1 | sed '/^$/d'
        echo
    done
}

testrunner 100

Hier sind die Ergebnisse:

test_while_loop
real    0m5.821s
user    0m5.322s
sys     0m0.526s

test_fold_loop
real    0m6.051s
user    0m5.260s
sys     0m0.822s

test_fold_xargs
real    7m13.444s
user    0m24.531s
sys     6m44.704s

test_awk_loop
real    0m6.507s
user    0m5.858s
sys     0m0.788s

test_grep_loop
real    0m6.179s
user    0m5.409s
sys     0m0.921s
Sechs
quelle
characterist für Leerzeichen mit der einfachen while readLösung leer , was problematisch sein kann, wenn verschiedene Arten von Leerzeichen voneinander unterschieden werden müssen.
pkfm
Schöne Lösung. Ich fand, dass ein Wechsel read -n1zu read -N1erforderlich war, um Leerzeichen korrekt zu behandeln.
Nielsen
16

Ich glaube, es gibt immer noch keine ideale Lösung, die alle Leerzeichen korrekt beibehält und schnell genug ist. Deshalb werde ich meine Antwort veröffentlichen. Die Verwendung ${foo:$i:1}funktioniert, ist aber sehr langsam, was sich besonders bei großen Saiten bemerkbar macht, wie ich weiter unten zeigen werde.

Meine Idee ist eine Erweiterung einer von Six vorgeschlagenen Methode , die read -n1einige Änderungen beinhaltet , um alle Zeichen beizubehalten und für jede Zeichenfolge korrekt zu funktionieren:

while IFS='' read -r -d '' -n 1 char; do
        # do something with $char
done < <(printf %s "$string")

Wie es funktioniert:

  • IFS=''- Durch die Neudefinition des internen Feldtrennzeichens in eine leere Zeichenfolge wird das Entfernen von Leerzeichen und Tabulatoren verhindert. Wenn Sie dies in derselben Zeile wie tun read, hat dies keine Auswirkungen auf andere Shell-Befehle.
  • -r- Mittel „raw“, das verhindert , dass readvon der Behandlung \am Ende der Zeile als ein spezieller Zeilenverkettungs Charakter.
  • -d ''- Wenn Sie eine leere Zeichenfolge als Trennzeichen übergeben, wird verhindert, dass readZeilenumbrüche entfernt werden. Bedeutet eigentlich, dass Nullbyte als Trennzeichen verwendet wird. -d ''ist gleich -d $'\0'.
  • -n 1 - Bedeutet, dass jeweils ein Zeichen gelesen wird.
  • printf %s "$string"- Verwenden printfstatt echo -nist sicherer, weil echobehandelt -nund -eals Optionen. Wenn Sie "-e" als Zeichenfolge übergeben, echowird nichts gedruckt.
  • < <(...)- Übergabe der Zeichenfolge an die Schleife mithilfe der Prozessersetzung. Wenn Sie stattdessen Here-Strings verwenden ( done <<< "$string"), wird am Ende ein zusätzliches Zeilenumbruchzeichen angehängt. printf %s "$string" | while ...Wenn Sie einen String durch eine pipe ( ) übergeben, wird die Schleife in einer Unterschale ausgeführt, was bedeutet, dass alle variablen Operationen innerhalb der Schleife lokal sind.

Lassen Sie uns nun die Leistung mit einer riesigen Zeichenfolge testen. Ich habe die folgende Datei als Quelle verwendet:
https://www.kernel.org/doc/Documentation/kbuild/makefiles.txt
Das folgende Skript wurde über den timeBefehl aufgerufen :

#!/bin/bash

# Saving contents of the file into a variable named `string'.
# This is for test purposes only. In real code, you should use
# `done < "filename"' construct if you wish to read from a file.
# Using `string="$(cat makefiles.txt)"' would strip trailing newlines.
IFS='' read -r -d '' string < makefiles.txt

while IFS='' read -r -d '' -n 1 char; do
        # remake the string by adding one character at a time
        new_string+="$char"
done < <(printf %s "$string")

# confirm that new string is identical to the original
diff -u makefiles.txt <(printf %s "$new_string")

Und das Ergebnis ist:

$ time ./test.sh

real    0m1.161s
user    0m1.036s
sys     0m0.116s

Wie wir sehen können, ist es ziemlich schnell.
Als nächstes habe ich die Schleife durch eine ersetzt, die die Parametererweiterung verwendet:

for (( i=0 ; i<${#string}; i++ )); do
    new_string+="${string:$i:1}"
done

Die Ausgabe zeigt genau, wie schlecht der Leistungsverlust ist:

$ time ./test.sh

real    2m38.540s
user    2m34.916s
sys     0m3.576s

Die genauen Zahlen können auf verschiedenen Systemen sehr unterschiedlich sein, aber das Gesamtbild sollte ähnlich sein.

Thunderbeef
quelle
13

Ich habe dies nur mit ASCII-Zeichenfolgen getestet, aber Sie können Folgendes tun:

while test -n "$words"; do
   c=${words:0:1}     # Get the first character
   echo character is "'$c'"
   words=${words:1}   # trim the first character
done
William Pursell
quelle
8

Die Schleife im C-Stil in @ chepners Antwort befindet sich in der Shell-Funktion update_terminal_cwd, und die grep -o .Lösung ist clever, aber ich war überrascht, dass keine Lösung verwendet wurde seq. Hier ist meins:

read word
for i in $(seq 1 ${#word}); do
  echo "${word:i-1:1}"
done
De Novo
quelle
6

Es ist auch möglich, die Zeichenfolge mithilfe folddieses Arrays in ein Zeichenarray aufzuteilen und anschließend zu durchlaufen:

for char in `echo "这是一条狗。" | fold -w1`; do
    echo $char
done
sebix
quelle
1
#!/bin/bash

word=$(echo 'Your Message' |fold -w 1)

for letter in ${word} ; do echo "${letter} is a letter"; done

Hier ist die Ausgabe:

Y ist ein Buchstabe o ist ein Buchstabe u ist ein Buchstabe r ist ein Buchstabe M ist ein Buchstabe e ist ein Buchstabe s ist ein Buchstabe s ist ein Buchstabe a ist ein Buchstabe g ist ein Buchstabe e ist ein Buchstabe

user13765771
quelle
0

Ein anderer Ansatz, wenn Sie nicht möchten, dass Leerzeichen ignoriert werden:

for char in $(sed -E s/'(.)'/'\1 '/g <<<"$your_string"); do
    # Handle $char here
done

quelle
0

Ein anderer Weg ist:

Characters="TESTING"
index=1
while [ $index -le ${#Characters} ]
do
    echo ${Characters} | cut -c${index}-${index}
    index=$(expr $index + 1)
done
Javier Salas
quelle
-1

Ich teile meine Lösung:

read word

for char in $(grep -o . <<<"$word") ; do
    echo $char
done
Dani Ballesteros
quelle
Dies ist sehr fehlerhaft - versuchen Sie es mit einer Zeichenfolge, die a enthält *. Sie erhalten Dateien im aktuellen Verzeichnis.
Charles Duffy
-3
TEXT="hello world"
for i in {1..${#TEXT}}; do
   echo ${TEXT[i]}
done

Wo {1..N}ist ein inklusive Bereich

${#TEXT} ist eine Anzahl von Buchstaben in einer Zeichenfolge

${TEXT[i]} - Sie können char aus einem String wie ein Element aus einem Array abrufen

Dmitri Emeliov
quelle
5
Shellcheck berichtet "Bash unterstützt keine Variablen in Klammerbereichserweiterungen" Also wird dies in Bash
Bren
@Bren Scheint mir ein Fehler zu sein.
Sapphire_Brick