Bashの文字列内の各文字に対してforループを実行するにはどうすればよいですか?


83

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

words="这是一条狗。"

私は文字のそれぞれ1つずつ、例えば最初にforループを作りたいcharacter="这"、そしてcharacter="是"character="一"など

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

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

3
OPこれをやりたいと思っている初心者の質問がたくさんあることは言及する価値があるかもしれません。多くの場合、各文字を個別に処理する必要のない、より優れたソリューションが可能です。これはXY問題として知られており、適切な解決策は、そこに到達するのに役立つと思われる手順を実行する方法だけでなく、質問で実際に達成したいことを説明することです。
トリプリー2018年

回答:


45

seddashのシェル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

4
UTF-8への素晴らしい努力。私はそれを必要としませんでした、しかしあなたはとにかく私の賛成票を得ます。
ヨルダン

+ 1sedの結果の文字列でforループを使用できます。
Tyzoid 2014年

236

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

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

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


forステートメントを機能させるためにforステートメントの前後に2セットの角かっこが必要なのはなぜですか?
tgun926 2015

それが構文にbash必要なことです。
chepner 2015

3
これは古いことは知っていますが、算術演算を可能にするため、2つの括弧が必要です。ここを参照=> tldp.org/LDP/abs/html/dblparens.html
ハンニバル

8
@Hannibal二重括弧のこの特定の使用は、実際にはbash構造for (( _expr_ ; _expr_ ; _expr_ )) ; do _command_ ; doneであり、$((expr))や((expr))と同じではないことを指摘したいと思います。3つのbash構造すべてで、exprは同じように扱われ、$((expr))もPOSIXです。
nabin-info 2017

1
@codeforesterそれは配列とは何の関係もありません。これbashは、算術コンテキストで評価される多くの式の1つにすぎません。
chepner 2017年

36

${#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

1
空白はどうですか?
Leandro

についての空白?空白文字は文字であり、これはすべての文字をループします。(ただし、重要な空白を含む変数または文字列は二重引用符で囲むように注意する必要があります。より一般的には、何をしているのかわからない限り、常にすべてを引用符で
囲んでください

23

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

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")

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

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

character単純なwhile readソリューションでは空白の場合は空です。これは、異なるタイプの空白を互いに区別する必要がある場合に問題になる可能性があります。
pkfm

素晴らしい解決策。スペース文字を正しく処理するread -n1read -N1は、に変更する必要があることがわかりました。
ニールセン

16

すべての空白文字を正しく保持し、十分に高速な理想的なソリューションはまだないと思います。そのため、回答を投稿します。使用は${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いるため、より安全でecho扱い-n-eオプションなど。「-e」を文字列として渡すと、echo何も出力されません。
  • < <(...)-プロセス置換を使用して文字列をループに渡します。代わりに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

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


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

8

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

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

6

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

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

出力は次のとおりです。

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


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

1

sedはUnicodeで動作します

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

出力

hello: 你
hello: 好
hello: 嗎

0

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

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

0

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

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

-1

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

read word

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

これは非常にバグがあります。を含む文字列を試してみてください*。現在のディレクトリにファイルがあります。
チャールズダフィー

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

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

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

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


5
Shellcheckは、「Bashは中括弧範囲拡張の変数をサポートしていません」と報告しているため、これはBashでは機能しません
Bren

@Brenは私にはバグのようです。
Sapphire_Brick
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.