How to perform a for loop on each character in a string in Bash?
我有一个这样的变量:
1 | words="这是一条狗。" |
我想在每个字符上做一个for循环,一次一个,例如,首先是
我知道的唯一方法是将每个字符输出到一个文件中的单独行,然后使用
- 如何通过for循环处理字符串中的每个字符?
您可以使用C型
1 2 3 4 | foo=string for (( i=0; i<${#foo}; i++ )); do echo"${foo:$i:1}" done |
我把
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | $ echo"你好嗎 新年好。全型句號" | sed -e 's/\(.\)/\1 /g' 你 好 嗎 新 年 好 。 全 型 句 號 |
和
1 2 3 4 5 6 7 8 9 10 11 12 13 | $ echo"Hello world" | sed -e 's/\(.\)/\1 /g' H e l l o w o r l d |
因此,输出可以与
为示例文本编辑翻译为英语:
1 2 3 4 5 | "你好嗎 新年好。全型句號" 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 |
实例:
1 2 3 4 5 6 7 | $ words="abc" $ echo ${words:0:1} a $ echo ${words:1:1} b $ echo ${words:2:1} c |
所以很容易迭代。
另一种方式:
1 2 3 4 | $ grep -o . <<<"abc" a b c |
或
1 2 3 4 5 | $ 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 2 3 | while read -n1 character; do echo"$character" done < <(echo -n"$words") |
注意使用
另一个选择是
1 2 3 | while read char; do echo"$char" done < <(fold -w1 <<<"$words") |
使用外部
1 | fold -w1 <<<"$words" | xargs -I% -- echo % |
您需要将上面示例中使用的
'
国际化
我刚刚用一些亚洲字符测试了
我可能会用一个awk数组替换
1 | awk 'BEGIN{FS=""} {for (i=1;i<=NF;i++) print $i}' |
或者在另一个答案中提到的
1 | grep -o . |
性能
仅供参考,我将上述3个选项作为基准。前两个是快速的,几乎是平手,折叠环略快于while环。不出所料,
以下是(缩写)测试代码:
1 2 3 4 5 6 7 8 9 10 11 | 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 |
结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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 |
我相信仍然没有一个理想的解决方案能够正确地保留所有的空白字符,并且速度足够快,所以我会发布我的答案。使用
我的想法是对six提出的方法的扩展,该方法涉及
1 2 3 | 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 将不会打印任何内容。< <(...) —使用进程替换将字符串传递给循环。如果使用这里的字符串(done <<<"$string" ),则在末尾附加一个额外的换行符。此外,将字符串通过管道(printf %s"$string" | while ... 将使循环在子shell中运行,这意味着所有变量操作都是循环中的局部操作。
现在,让我们用一个巨大的字符串来测试性能。我使用以下文件作为源:https://www.kernel.org/doc/documentation/kbuild/makefiles.txt以下脚本是通过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #!/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") |
结果是:
1 2 3 4 5 | $ time ./test.sh real 0m1.161s user 0m1.036s sys 0m0.116s |
如我们所见,速度相当快。接下来,我将循环替换为使用参数扩展的循环:
1 2 3 | for (( i=0 ; i<${#string}; i++ )); do new_string+="${string:$i:1}" done |
输出准确显示了性能损失有多严重:
1 2 3 4 5 | $ time ./test.sh real 2m38.540s user 2m34.916s sys 0m3.576s |
确切的数字可能在不同的系统上非常多,但总体情况应该是相似的。
我只用ASCII字符串测试过这个,但是您可以做如下操作:
1 2 3 4 5 | 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 |
也可以使用
1 2 3 | for char in `echo"这是一条狗。" | fold -w1`; do echo $char done |
@chepner的答案中的C型循环在shell函数
1 2 3 4 | read word for i in $(seq 1 ${#word}); do echo"${word:i-1:1}" done |
另一种方法是:
1 2 3 4 5 6 7 | Characters="TESTING" index=1 while [ $index -le ${#Characters} ] do echo ${Characters} | cut -c${index}-${index} index=$(expr $index + 1) done |
另一种方法,如果您不关心空白被忽略:
1 2 3 | for char in $(sed -E s/'(.)'/'\1 '/g <<<"$your_string"); do # Handle $char here done |
我分享我的解决方案:
1 2 3 4 5 | read word for char in $(grep -o . <<<"$word") ; do echo $char done |
1 2 3 4 | TEXT="hello world" for i in {1..${#TEXT}}; do echo ${TEXT[i]} done |
其中,