96

我有一个这样的变量:

words="这是一条狗。"

我想对每个字符进行一个 for 循环,一次一个,例如 first character="这", then character="是"character="一"等。

我知道的唯一方法是将每个字符输出到文件中的单独行,然后使用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${foo:$i:1}展开到从$i长度为 1 的位置开始的子字符串。

于 2012-05-11T13:19:42.123 回答
54

在外壳上sed,我得到了以下工作正常:dashLANG=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}pos从后面返回 N 个字符

例子:

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

使用外部fold命令(coreutils包的)的主要好处是简洁。您可以将其输出提供给另一个命令,例如xargsfindutils包的一部分),如下所示:

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

您需要将echo上面示例中使用的命令替换为您希望针对每个字符运行的命令。请注意,xargs默认情况下将丢弃空格。您可以使用-d '\n'来禁用该行为。


国际化

我刚刚测试fold了一些亚洲字符,发现它不支持 Unicode。因此,虽然它可以满足 ASCII 需求,但它并不适合所有人。在这种情况下,有一些替代方案。

我可能会fold -w1用 awk 数组替换:

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

或者grep另一个答案中提到的命令:

grep -o .


表现

仅供参考,我对上述 3 个选项进行了基准测试。前两个速度很快,几乎平手,折叠循环比 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意味着它不会影响其他 shell 命令。
  • -r- 表示“原始”,防止read\行尾视为特殊的行连接字符。
  • -d ''- 将空字符串作为分隔符传递可防止read剥离换行符。实际上意味着使用空字节作为分隔符。-d ''等于-d $'\0'
  • -n 1- 表示一次读取一个字符。
  • printf %s "$string"- 使用printf而不是echo -n更安全,因为echo-n-e视为选项。如果您将“-e”作为字符串传递,echo则不会打印任何内容。
  • < <(...)- 使用进程替换将字符串传递给循环。如果您使用 here-strings 代替 ( done <<< "$string"),则会在末尾附加一个额外的换行符。此外,通过管道 ( printf %s "$string" | while ...) 传递字符串将使循环在子 shell 中运行,这意味着所有变量操作都是循环内的本地操作。

现在,让我们用一个巨大的字符串来测试性能。我使用以下文件作为源:
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 风格循环在 shell functionupdate_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

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 回答
1

要在 POSIX 兼容的 shell 上迭代 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 回答
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]} - 您可以从字符串中获取字符,就像从数组中获取项目一样

于 2018-06-25T13:34:18.433 回答