如何对Bash中的string中的每个字符执行for循环?
我有这样的variables:
words="这是一条狗。"
我想对每个字符做一个for循环,例如,第一个character="这"
,然后是character="是"
, character="一"
等。
我知道的唯一方法是输出每个字符来分隔文件中的行,然后while read line
使用,但这似乎效率很低。
- 我如何通过for循环来处理string中的每个字符?
在LANG=en_US.UTF-8
dash
shell上,我得到了如下工作:
$ 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包)的主要好处是简洁。 您可以将其输出提供给另一个命令,如xargs
( findutils包的一部分),如下所示:
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