96

私はこのような変数を持っています:

words="这是一条狗。"

character="这"各文字に対して、一度に1つずつ、たとえば最初に、次にcharacter="是"character="一"などのforループを作成したいと思います。

私が知っている唯一の方法は、各文字をファイル内の別々の行に出力してから使用することwhile read lineですが、これは非常に非効率的なようです。

  • 文字列内の各文字をforループで処理するにはどうすればよいですか?
4

15 に答える 15

266

Cスタイルのforループを使用できます。

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

${#foo}の長さに拡張されfooます。長さ1の${foo:$i:1}位置から始まる部分文字列に展開されます。$i

于 2012-05-11T13:19:42.123 に答える
54

sedon dashshell ofを使用するLANG=en_US.UTF-8と、次のことが正しく機能します。

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

新
年
好
。
全
型
句
號

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

w
o
r
l
d

したがって、出力はループすることができますwhile read ... ; do ... ; done

サンプルテキスト用に編集されたものは英語に翻訳されます:

"你好嗎 新年好。全型句號" 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
于 2012-05-13T15:19:33.523 に答える
40

${#var}の長さを返しますvar

${var:pos:N}N文字pos以降を返します

例:

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

したがって、反復するのは簡単です。

別の方法:

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

また

$ 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
于 2012-05-11T13:13:01.573 に答える
25

とだけをbash利用した明白な解決策について誰も言及していないことに驚いています。whileread

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

echo -n最後に余分な改行を避けるためにを使用していることに注意してください。printf別の良いオプションであり、特定のニーズにより適している場合があります。空白を無視したい場合は、に置き換え"$words""${words// /}"ください。

別のオプションはfoldです。ただし、forループにフィードしないように注意してください。むしろ、次のようにwhileループを使用します。

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

( coreutilsパッケージの)外部foldコマンドを使用する主な利点は簡潔さです。次のように、出力を( findutilsパッケージの一部)などの別のコマンドにフィードできます。xargs

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

echo上記の例で使用されているコマンドを、各文字に対して実行するコマンドに置き換える必要があります。xargsデフォルトでは空白が破棄されることに注意してください。を使用-d '\n'して、その動作を無効にすることができます。


国際化

いくつかのアジアの文字でテストfoldしたところ、Unicodeがサポートされていないことがわかりました。したがって、ASCIIのニーズには問題ありませんが、すべての人に役立つわけではありません。その場合、いくつかの選択肢があります。

私はおそらくfold -w1awk配列に置き換えます:

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

または、grep別の回答で言及されているコマンド:

grep -o .


パフォーマンス

参考までに、前述の3つのオプションのベンチマークを行いました。最初の2つは高速で、ほぼ同点であり、foldループはwhileループよりもわずかに高速でした。当然のことながらxargs、最も低速でした...75倍低速でした。

(省略された)テストコードは次のとおりです。

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

結果は次のとおりです。

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
于 2015-04-27T21:22:07.717 に答える
19

すべての空白文字を正しく保持し、十分に高速な理想的なソリューションはまだないと思います。そのため、回答を投稿します。使用${foo:$i:1}は機能しますが、非常に低速です。これは、以下に示すように、大きな文字列で特に顕著です。

私の考えは、 Sixによって提案された方法の拡張です。これにはread -n1、すべての文字を保持し、任意の文字列に対して正しく機能するようにいくつかの変更が加えられています。

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

使い方:

  • IFS=''-内部フィールドセパレータを空の文字列に再定義すると、スペースとタブが削除されなくなります。と同じ行でread実行すると、他のシェルコマンドに影響を与えないことを意味します。
  • -r-「生」を意味し、行の終わりで特別な行連結文字としてread扱われないようにします。\
  • -d ''-区切り文字として空の文字列を渡すと、read改行文字が削除されなくなります。実際には、ヌルバイトが区切り文字として使用されることを意味します。-d ''に等しい-d $'\0'
  • -n 1-一度に1文字ずつ読み取られることを意味します。
  • printf %s "$string"-をオプションとして扱うため、代わりに使用する方がprintf安全echo -nです。「-e」を文字列として渡すと、何も出力されません。echo-n-eecho
  • < <(...)-プロセス置換を使用して文字列をループに渡します。代わりにhere-strings(done <<< "$string")を使用すると、末尾に改行文字が追加されます。また、文字列をパイプ(printf %s "$string" | while ...)に渡すと、ループがサブシェルで実行されます。つまり、すべての変数操作がループ内でローカルになります。

それでは、巨大な文字列を使用してパフォーマンスをテストしてみましょう。次のファイルをソースとして使用しました:
https
://www.kernel.org/doc/Documentation/kbuild/makefiles.txt 次のスクリプトが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")

そして結果は次のとおりです。

$ time ./test.sh

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

ご覧のとおり、非常に高速です。
次に、ループをパラメーター展開を使用するループに置き換えました。

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

出力は、パフォーマンスの低下がどれほど悪いかを正確に示しています。

$ time ./test.sh

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

正確な数はシステムによって大きく異なる場合がありますが、全体像は類似している必要があります。

于 2016-11-27T20:18:24.677 に答える
13

私はこれをASCII文字列でテストしただけですが、次のようなことができます。

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
于 2012-05-11T13:13:49.340 に答える
9

を使用して文字列を文字配列に分割し、foldこの配列を反復処理することもできます。

for char in `echo "这是一条狗。" | fold -w1`; do
    echo $char
done
于 2015-01-11T17:01:27.917 に答える
9

@chepnerの答えのCスタイルのループはシェル関数update_terminal_cwdにあり、grep -o .解決策は賢いですが、を使用した解決策が見当たらないことに驚きましたseq。これが私のものです:

read word
for i in $(seq 1 ${#word}); do
  echo "${word:i-1:1}"
done
于 2018-11-30T06:43:35.460 に答える
4
#!/bin/bash

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

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

出力は次のとおりです。

Yは文字oは文字uは文字rは文字Mは文字eは文字sは文字sは文字aは文字gは文字eは文字

于 2020-10-22T16:31:44.757 に答える
1

POSIX準拠のシェルでASCII文字を繰り返すには、パラメーター拡張を使用して外部ツールを回避できます。

#!/bin/sh

str="Hello World!"

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

また

str="Hello World!"

while [ -n "$str" ]; do
    next=${str#?}
    echo "${str%$next}"
    str=$next
done
于 2020-12-18T00:32:36.690 に答える
1

sedはUnicodeで動作します

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

出力

hello: 你
hello: 好
hello: 嗎
于 2020-12-31T14:25:31.220 に答える
0

空白が無視されることを気にしない場合の別のアプローチ:

for char in $(sed -E s/'(.)'/'\1 '/g <<<"$your_string"); do
    # Handle $char here
done
于 2012-12-31T01:09:50.750 に答える
0

別の方法は次のとおりです。

Characters="TESTING"
index=1
while [ $index -le ${#Characters} ]
do
    echo ${Characters} | cut -c${index}-${index}
    index=$(expr $index + 1)
done
于 2017-03-22T23:31:03.420 に答える
-1

私は自分の解決策を共有します:

read word

for char in $(grep -o . <<<"$word") ; do
    echo $char
done
于 2018-02-26T21:59:58.807 に答える
-3
TEXT="hello world"
for i in {1..${#TEXT}}; do
   echo ${TEXT[i]}
done

{1..N}包括的範囲はどこですか

${#TEXT}文字列内の文字の数です

${TEXT[i]} -配列のアイテムのように文字列からcharを取得できます

于 2018-06-25T13:34:18.433 に答える