如何对Bash中的string中的每个字符执行for循环?

我有这样的variables:

words="这是一条狗。" 

我想对每个字符做一个for循环,例如,第一个character="这" ,然后是character="是"character="一"等。

我知道的唯一方法是输出每个字符来分隔文件中的行,然后while read line使用,但这似乎效率很低。

  • 我如何通过for循环来处理string中的每个字符?

LANG=en_US.UTF-8 dashshell上,我得到了如下工作:

 $ 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循环while read ... ; do ... ; done 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 

你可以使用C风格for循环:

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

${#foo}展开为foo的长度。 ${foo:$i:1}展开为从长度为1的位置$i开始的子string。

${#var}返回${#var}的长度

${var:pos:N}从后向返回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 

我很惊讶没有人提到明显的bash解决scheme只使用和read

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

注意使用echo -n来避免最后的无用换行符。 printf是另一个不错的select,可能更适合您的特定需求。 如果你想忽略空格,用"${words// /}"replace"$words" "${words// /}"

另一种select是fold 。 但是请注意,它不应该被送入for循环。 相反,使用while循环如下:

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

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

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

你需要用上面例子中使用的echo命令replace你想要对每个字符运行的命令。 请注意, xargs默认会丢弃空格。 您可以使用-d '\n'来禁用该行为。


国际化

我只testing了一些亚洲字符的fold ,并意识到它没有Unicode支持。 所以虽然对于ASCII需求来说没问题,但它不适用于所有人。 在这种情况下,有一些替代scheme。

我可能会用awk数组replacefold -w1

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

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

 grep -o . 

性能

仅供参考,我以上述三个选项为基准。 前两个是快速,几乎搭售,折叠循环比while循环稍快。 不出所料xargs是最慢的… 75倍慢。

这是(缩写)testing代码:

 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 

我只用asciistringtesting过,但是你可以这样做:

 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 

我相信仍然没有理想的解决scheme能够正确保留所有的空白字符,并且速度足够快,所以我会发布我的答案。 使用${foo:$i:1}可以工作,但速度很慢,对于大string尤为明显,如下所示。

我的想法是由Six提出的一种方法的扩展,其中涉及到read -n1 ,其中一些更改保留所有字符并正确地为任何string工作:

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

怎么运行的:

  • IFS='' – 将内部字段分隔符重新定义为空string可防止空白和制表符被剥离。 与read同一行意味着它不会影响其他shell命令。
  • -r – 意思是“原始的”,它防止read作为特殊行连接字符在行尾处理。
  • -d '' – 将空string作为分隔符传递,防止read换行符。 实际上意味着空字节被用作分隔符。 -d ''等于-d $'\0'
  • -n 1 – 表示一次读取一个字符。
  • printf %s "$string" – 使用printf而不是echo -n更安全,因为echo-n-e当作选项。 如果将“-e”作为string传递,则echo将不会打印任何内容。
  • < <(...) – 使用进程replace将string传递给循环。 如果你在这里使用string( done <<< "$string" ),结尾会追加一个额外的换行符。 另外,通过pipe道传递string( printf %s "$string" | while ... )会使循环运行在一个子shell中,这意味着所有的variables操作在循环中都是局部的。

现在,让我们用一个巨大的string来testing性能。 我使用以下文件作为来源:
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 

确切的数字可能在不同的系统上,但整体情况应该是相似的。

也可以使用fold将string拆分为一个字符数组,然后遍历这个数组:

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

另一种方法,如果你不关心被忽略的空白:

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

另一种方法是:

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