Bash'de bir dizedeki her karakter için bir for döngüsü nasıl gerçekleştirilir?


83

Bunun gibi bir değişkenim var:

words="这是一条狗。"

Ben karakterlerin her biri bir defada bir, mesela ilk on döngü için yapmak istiyorum character="这", o zaman character="是", character="一"vb

Bildiğim tek yol, her karakteri bir dosyada ayrı bir satıra çıkarmak ve sonra kullanmak while read line, ancak bu çok verimsiz görünüyor.

  • Bir dizedeki her karakteri bir for döngüsü aracılığıyla nasıl işleyebilirim?

3
OP'nin yapmak istediklerinin bu olduğunu düşündüğü pek çok yeni başlayan sorusu gördüğümüzden bahsetmekte fayda var . Çoğu zaman, her karakterin ayrı ayrı işlenmesini gerektirmeyen daha iyi bir çözüm mümkündür. Bu bir XY Problemi olarak bilinir ve doğru çözüm, sadece oraya ulaşmanıza yardımcı olacağını düşündüğünüz adımları nasıl uygulayacağınızı değil, sorunuzda gerçekten neyi başarmak istediğinizi açıklamaktır .
üçlü

Yanıtlar:


45

İle sedilgili dashkabuğu LANG=en_US.UTF-8, ben followings doğru çalışma var:

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

新
年
好
。
全
型
句
號

ve

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

w
o
r
l
d

Böylece çıktı ile döngü oluşturulabilir while read ... ; do ... ; done

İngilizce'ye çevirmek için örnek metin için düzenlenmiştir:

"你好嗎 新年好。全型句號" 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

4
UTF-8 için güzel çaba. Buna ihtiyacım yoktu, ama yine de benim oyumu aldın.
Ürdün

+1 sed'den elde edilen dizgede for döngüsünü kullanabilirsiniz.
Tyzoid

236

C tarzı bir fordöngü kullanabilirsiniz:

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

${#foo}uzunluğuna genişler foo. uzunluk 1 ${foo:$i:1}konumundan başlayarak alt dizeye genişler $i.


Çalışması için neden for ifadesinin etrafında iki parantez kümesine ihtiyacınız var?
tgun926

Sözdizimi bashgerektirir.
chepner

3
Bunun eski olduğunu biliyorum, ancak aritmetik işlemlere izin verdiği için iki parantez gerekli. Buraya bakın => tldp.org/LDP/abs/html/dblparens.html
Hannibal

8
@Hannibal Bu özel çift parantez kullanımının aslında bash yapısı olduğunu for (( _expr_ ; _expr_ ; _expr_ )) ; do _command_ ; doneve $ (( ifade )) veya (( ifade )) ile aynı olmadığını belirtmek istedim . Her üç bash yapısında, ifade aynı şekilde ele alınır ve $ (( ifade )) ayrıca POSIX'dir.
nabin-info

1
@codeforester Bunun dizilerle ilgisi yoktur; basharitmetik bağlamda değerlendirilen birçok ifadeden yalnızca biridir .
chepner

36

${#var} uzunluğunu döndürür var

${var:pos:N}positibaren N karakter döndürür

Örnekler:

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

bu yüzden yinelemesi kolaydır.

diğer yol:

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

veya

$ 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

1
boşluk ne olacak?
Leandro

Peki ya beyaz boşluk? Boşluk karakteri bir karakterdir ve bu, tüm karakterler üzerinde döngü oluşturur. (Bununla birlikte, önemli boşluklar içeren herhangi bir değişken veya dize etrafında çift tırnak kullanmaya dikkat etmelisiniz. Daha genel olarak, ne yaptığınızı bilmiyorsanız her zaman her şeyi alıntılayın . )
18:10

23

Hiç kimsenin bashyalnızca whileve kullanan bariz çözümden bahsetmediğine şaşırdım read.

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

echo -nSonunda gereksiz yeni satırı önlemek için kullanımına dikkat edin. printfbaşka bir iyi seçenektir ve özel ihtiyaçlarınız için daha uygun olabilir. Boşluğu yok saymak istiyorsanız, "$words"ile değiştirin "${words// /}".

Başka bir seçenek de fold. Ancak bunun asla bir for döngüsüne beslenmemesi gerektiğini lütfen unutmayın. Bunun yerine aşağıdaki gibi bir while döngüsü kullanın:

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

Harici foldkomutu ( coreutils paketinin) kullanmanın birincil faydası kısalık olacaktır. Çıktısını aşağıdaki gibi xargs( findutils paketinin parçası) gibi başka bir komuta besleyebilirsiniz :

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

Sen değiştirmek isteyeceksiniz echokomutla her karakter karşı çalıştırmak istediğiniz Yukarıdaki örnekte kullanılan komutu. Not xargsvarsayılan olarak boşluk atar. Bu -d '\n'davranışı devre dışı bırakmak için kullanabilirsiniz .


Uluslararasılaştırma

foldBazı Asya karakterleriyle test ettim ve Unicode desteğine sahip olmadığını fark ettim. Yani ASCII ihtiyaçları için uygun olsa da, herkes için işe yaramayacak. Bu durumda bazı alternatifler var.

Muhtemelen fold -w1bir awk dizisiyle değiştirirdim :

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

Veya grepbaşka bir cevapta bahsedilen komut:

grep -o .


Verim

Bilginize, yukarıda belirtilen 3 seçeneği karşılaştırdım. İlk ikisi hızlıydı, neredeyse bağlıydı ve katlama döngüsü while döngüsünden biraz daha hızlıydı. Şaşırtıcı olmayan bir şekilde xargsen yavaştı ... 75 kat daha yavaş.

İşte (kısaltılmış) test kodu:

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

Sonuçlar burada:

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

characterbeyaz boşluklar için boştur ve basit while readçözümü vardır; bu, farklı türdeki boşlukların birbirinden ayırt edilmesi gerekiyorsa sorun yaratabilir.
pkfm

Güzel çözüm. Boşluk karakterlerini doğru şekilde kullanmak read -n1için read -N1olarak değiştirmenin gerekli olduğunu buldum .
nielsen

16

Hala tüm boşluk karakterlerini doğru bir şekilde koruyacak ve yeterince hızlı olan ideal bir çözüm olmadığına inanıyorum, bu yüzden cevabımı göndereceğim. ${foo:$i:1}Çalışmaları kullanmak , ancak çok yavaş, bu da aşağıda göstereceğim gibi özellikle büyük dizelerde dikkat çekiyor.

Benim fikrim, Six tarafından önerilen read -n1ve tüm karakterleri korumak ve herhangi bir dizge için doğru çalışmak için bazı değişiklikler içeren bir yöntemin genişletilmesi :

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

Nasıl çalışır:

  • IFS=''- Dahili alan ayırıcısının boş dizeye yeniden tanımlanması, boşlukların ve sekmelerin çıkarılmasını önler. Aynı satırda yapmak, readdiğer kabuk komutlarını etkilemeyeceği anlamına gelir.
  • -r- Satırın sonunda özel bir satır birleştirme karakteri olarak readişlem görmesini engelleyen "ham" anlamına gelir \.
  • -d ''- Boş dizenin sınırlayıcı readolarak iletilmesi, yeni satır karakterlerinin çıkarılmasını önler . Aslında boş baytın sınırlayıcı olarak kullanıldığı anlamına gelir. -d ''eşittir -d $'\0'.
  • -n 1 - Her seferinde bir karakterin okunacağı anlamına gelir.
  • printf %s "$string"- printfBunun yerine kullanmak echo -ndaha güvenlidir, çünkü echotedavi -nve -eseçenek olarak. Bir dizge olarak "-e" geçerseniz, echohiçbir şey yazdırmaz.
  • < <(...)- İşlem ikamesi kullanarak dizeyi döngüye iletme. Bunun yerine ( done <<< "$string") buradaki dizeleri kullanırsanız , sonuna fazladan bir satırsonu karakteri eklenir. Ayrıca, dizeyi bir boru ( printf %s "$string" | while ...) içinden geçirmek, döngünün bir alt kabukta çalışmasını sağlar, bu da tüm değişken işlemlerin döngü içinde yerel olduğu anlamına gelir.

Şimdi performansı devasa bir ip ile test edelim. Aşağıdaki dosyayı kaynak olarak kullandım:
https://www.kernel.org/doc/Documentation/kbuild/makefiles.txt
Aşağıdaki komut dosyası komut aracılığıyla çağrıldı time:

#!/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")

Ve sonuç:

$ time ./test.sh

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

Gördüğümüz gibi oldukça hızlı.
Daha sonra, döngüyü parametre genişletmeyi kullanan bir döngü ile değiştirdim:

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

Çıktı, performans kaybının tam olarak ne kadar kötü olduğunu gösterir:

$ time ./test.sh

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

Kesin sayılar farklı sistemlerde çok olabilir, ancak genel resim benzer olmalıdır.


13

Bunu yalnızca ascii dizeleri ile test ettim, ancak şöyle bir şey yapabilirsiniz:

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

8

@ Chepner'ın cevabındaki C stili döngü kabuk işlevindedir update_terminal_cwdve grep -o .çözüm akıllıdır, ancak kullanan bir çözüm görmemek beni şaşırttı seq. Benimki burada:

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

6

Dizeyi foldbu diziyi kullanarak bir karakter dizisine bölmek ve ardından bu diziyi yinelemek de mümkündür :

for char in `echo "这是一条狗。" | fold -w1`; do
    echo $char
done

1
#!/bin/bash

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

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

İşte çıktı:

Y bir harftir o bir harftir u bir harftir r bir harf M bir harf e bir harf bir harf bir s bir harf bir s bir harf bir bir bir bir bir g bir bir bir e bir harftir


1

POSIX uyumlu bir kabukta ASCII karakterlerini yinelemek için, Parametre Genişletmelerini kullanarak harici araçlardan kaçınabilirsiniz:

#!/bin/sh

str="Hello World!"

while [ ${#str} -gt 0 ]; do
    next=${str#?}
    echo "${str%$next}"
    str=$next
done

veya

str="Hello World!"

while [ -n "$str" ]; do
    next=${str#?}
    echo "${str%$next}"
    str=$next
done

1

sed, unicode ile çalışır

IFS=$'\n'
for z in $(sed 's/./&\n/g' <(printf '你好嗎')); do
 echo hello: "$z"
done

çıktılar

hello: 你
hello: 好
hello: 嗎

0

Başka bir yaklaşım, boşlukların göz ardı edilmesini umursamıyorsanız:

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

0

Başka bir yol şudur:

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

-1

Çözümümü paylaşıyorum:

read word

for char in $(grep -o . <<<"$word") ; do
    echo $char
done

Bu çok hatalı - a içeren bir dizge ile deneyin *, mevcut dizindeki dosyaları alırsınız.
Charles Duffy

-3
TEXT="hello world"
for i in {1..${#TEXT}}; do
   echo ${TEXT[i]}
done

{1..N}kapsayıcı aralık nerede

${#TEXT} bir dizedeki harf sayısıdır

${TEXT[i]} - bir dizideki bir öğe gibi dizeden karakter elde edebilirsiniz


5
Shellcheck "Bash küme ayracı aralığı genişletmelerinde değişkenleri desteklemiyor" raporunda bu, Bash
Bren

@Bren bana bir böcek gibi görünüyor.
Sapphire_Brick
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.