Mam taką zmienną:
words="这是一条狗。"
Chcę zrobić dla pętli na każdym ze znaków, po jednym na raz, np najpierw character="这"
, potem character="是"
, character="一"
itp
Jedyny znany mi sposób to umieszczanie każdego znaku w osobnej linii w pliku, a następnie użycie go while read line
, ale wydaje się to bardzo nieefektywne.
- Jak mogę przetworzyć każdy znak w ciągu za pomocą pętli for?
Odpowiedzi:
Z
sed
nadash
skorupieLANG=en_US.UTF-8
, mam następne działa prawidłowo:$ echo "你好嗎 新年好。全型句號" | sed -e 's/\(.\)/\1\n/g' 你 好 嗎 新 年 好 。 全 型 句 號
i
$ echo "Hello world" | sed -e 's/\(.\)/\1\n/g' H e l l o w o r l d
W ten sposób wyjście można zapętlić za pomocą
while read ... ; do ... ; done
zredagowano przykładowy tekst przetłumacz na język angielski:
"你好嗎 新年好。全型句號" 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
źródło
Możesz użyć
for
pętli w stylu C :foo=string for (( i=0; i<${#foo}; i++ )); do echo "${foo:$i:1}" done
${#foo}
rozwija się do długościfoo
.${foo:$i:1}
rozwija się do podciągu zaczynając od pozycji$i
o długości 1.źródło
bash
wymaga składnia .for (( _expr_ ; _expr_ ; _expr_ )) ; do _command_ ; done
a nie to samo, co $ (( wyr )) ani (( wyr )). We wszystkich trzech konstrukcjach bash wyrażenie jest traktowane tak samo, a $ (( wyrażenie )) jest również zgodne z POSIX.bash
które jest oceniane w kontekście arytmetycznym.${#var}
zwraca długośćvar
${var:pos:N}
zwraca N znaków odpos
początkuPrzykłady:
$ words="abc" $ echo ${words:0:1} a $ echo ${words:1:1} b $ echo ${words:2:1} c
więc jest to łatwe do iteracji.
Inny sposób:
$ grep -o . <<< "abc" a b c
lub
$ 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
źródło
Dziwię się, że nikt nie wspomniał o oczywistym
bash
rozwiązaniu wykorzystującym tylkowhile
iread
.while read -n1 character; do echo "$character" done < <(echo -n "$words")
Zwróć uwagę na użycie,
echo -n
aby uniknąć dodatkowego znaku nowej linii na końcu.printf
to kolejna dobra opcja, która może być bardziej odpowiednia dla Twoich szczególnych potrzeb. Jeśli chcesz ignorować spacje następnie zastąpić"$words"
z"${words// /}"
.Inną opcją jest
fold
. Należy jednak pamiętać, że nigdy nie należy go wprowadzać do pętli for. Zamiast tego użyj pętli while w następujący sposób:while read char; do echo "$char" done < <(fold -w1 <<<"$words")
Główną korzyścią wynikającą z używania
fold
polecenia zewnętrznego (z pakietu coreutils ) byłaby zwięzłość. Możesz przekazać jego dane wyjściowe do innego polecenia, takiego jakxargs
(część pakietu findutils ) w następujący sposób:fold -w1 <<<"$words" | xargs -I% -- echo %
Będziesz chciał zastąpić
echo
polecenie użyte w powyższym przykładzie poleceniem, które chcesz wykonać przeciwko każdemu znakowi. Zauważ, żexargs
domyślnie odrzuca białe znaki. Możesz użyć,-d '\n'
aby wyłączyć to zachowanie.Umiędzynarodowienie
Właśnie przetestowałem
fold
niektóre znaki azjatyckie i zdałem sobie sprawę, że nie ma obsługi Unicode. Więc chociaż jest to dobre dla potrzeb ASCII, nie będzie działać dla wszystkich. W takim przypadku istnieje kilka alternatyw.Prawdopodobnie zamieniłbym
fold -w1
na tablicę awk:awk 'BEGIN{FS=""} {for (i=1;i<=NF;i++) print $i}'
Lub
grep
polecenie wymienione w innej odpowiedzi:Wydajność
Do Twojej wiadomości, porównałem 3 wyżej wymienione opcje. Pierwsze dwa były szybkie, prawie zawiązywane, a pętla zagięcia była nieco szybsza niż pętla while. Nic dziwnego, że
xargs
był najwolniejszy ... 75x wolniejszy.Oto (skrócony) kod testu:
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
Oto wyniki:
źródło
character
jest puste dla białych znaków w prostymwhile read
rozwiązaniu, co może być problematyczne, jeśli trzeba rozróżnić różne typy białych znaków.read -n1
naread -N1
była potrzebna do poprawnej obsługi znaków spacji.Uważam, że nadal nie ma idealnego rozwiązania, które poprawnie zachowałoby wszystkie białe znaki i jest wystarczająco szybkie, więc opublikuję swoją odpowiedź. Używanie
${foo:$i:1}
działa, ale jest bardzo powolne, co jest szczególnie zauważalne przy dużych strunach, co pokażę poniżej.Mój pomysł jest rozwinięciem metody zaproponowanej przez Six , która obejmuje
read -n1
pewne zmiany, aby zachować wszystkie znaki i działać poprawnie dla dowolnego ciągu:while IFS='' read -r -d '' -n 1 char; do # do something with $char done < <(printf %s "$string")
Jak to działa:
IFS=''
- Przedefiniowanie wewnętrznego separatora pól na pusty ciąg zapobiega usuwaniu spacji i tabulatorów. Zrobienie tego w tej samej liniiread
oznacza, że nie wpłynie to na inne polecenia powłoki.-r
- Oznacza „surowy”, który zapobiegaread
traktowaniu\
końca wiersza jako specjalnego znaku konkatenacji wiersza.-d ''
- Przekazanie pustego ciągu jako separatora zapobiegaread
usuwaniu znaków nowej linii. Faktycznie oznacza, że jako separator używany jest bajt zerowy.-d ''
jest równa-d $'\0'
.-n 1
- Oznacza, że będzie czytany jeden znak na raz.printf %s "$string"
- Używanieprintf
zamiastecho -n
jest bezpieczniejsze, ponieważecho
traktuje-n
i-e
jako opcje. Jeśli podasz "-e" jako łańcuch,echo
niczego nie wydrukuje.< <(...)
- Przekazywanie ciągu znaków do pętli za pomocą podstawiania procesów. Jeśli zamiast tego użyjesz here-strings (done <<< "$string"
), na końcu zostanie dodany dodatkowy znak nowej linii. Ponadto przekazanie ciągu znaków przez funkcję pipe (printf %s "$string" | while ...
) spowodowałoby, że pętla działałaby w podpowłoce, co oznacza, że wszystkie operacje na zmiennych są lokalne w pętli.Teraz przetestujmy wydajność z ogromnym sznurkiem. Jako źródło użyłem następującego pliku:
https://www.kernel.org/doc/Documentation/kbuild/makefiles.txt
Następujący skrypt został wywołany
time
poleceniem:#!/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")
A wynik jest taki:
Jak widać, jest to dość szybkie.
Następnie zastąpiłem pętlę taką, która wykorzystuje rozszerzenie parametrów:
for (( i=0 ; i<${#string}; i++ )); do new_string+="${string:$i:1}" done
Dane wyjściowe pokazują dokładnie, jak duża jest utrata wydajności:
Dokładne liczby mogą być bardzo różne w różnych systemach, ale ogólny obraz powinien być podobny.
źródło
Testowałem to tylko z ciągami ascii, ale możesz zrobić coś takiego:
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
źródło
Pętla w stylu C w odpowiedzi @ chepner znajduje się w funkcji powłoki
update_terminal_cwd
, agrep -o .
rozwiązanie jest sprytne, ale byłem zaskoczony, że nie widziałem rozwiązania używającegoseq
. To moje:read word for i in $(seq 1 ${#word}); do echo "${word:i-1:1}" done
źródło
Możliwe jest również podzielenie ciągu na tablicę znaków za pomocą,
fold
a następnie iteracja po tej tablicy:for char in `echo "这是一条狗。" | fold -w1`; do echo $char done
źródło
#!/bin/bash word=$(echo 'Your Message' |fold -w 1) for letter in ${word} ; do echo "${letter} is a letter"; done
Oto wynik:
Y to litera o to litera u to litera r to litera M to litera e to litera s to litera s to litera a to litera g to litera e to litera
źródło
Aby iterować znaki ASCII w powłoce zgodnej z POSIX, można uniknąć zewnętrznych narzędzi, używając Rozszerzeń parametrów:
#!/bin/sh str="Hello World!" while [ ${#str} -gt 0 ]; do next=${str#?} echo "${str%$next}" str=$next done
lub
str="Hello World!" while [ -n "$str" ]; do next=${str#?} echo "${str%$next}" str=$next done
źródło
sed działa z Unicode
IFS=$'\n' for z in $(sed 's/./&\n/g' <(printf '你好嗎')); do echo hello: "$z" done
wyjścia
źródło
Inne podejście, jeśli nie zależy ci na ignorowaniu białych znaków:
for char in $(sed -E s/'(.)'/'\1 '/g <<<"$your_string"); do # Handle $char here done
źródło
Innym sposobem jest:
Characters="TESTING" index=1 while [ $index -le ${#Characters} ] do echo ${Characters} | cut -c${index}-${index} index=$(expr $index + 1) done
źródło
Udostępniam moje rozwiązanie:
read word for char in $(grep -o . <<<"$word") ; do echo $char done
źródło
*
, otrzymasz pliki w bieżącym katalogu.TEXT="hello world" for i in {1..${#TEXT}}; do echo ${TEXT[i]} done
gdzie
{1..N}
jest zakres obejmujący${#TEXT}
to liczba liter w ciągu${TEXT[i]}
- możesz pobrać znak ze stringa jak element z tablicyźródło