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

Vil*_*age 66 bash for-loop

我有一个像这样的变量:

words="??????"
Run Code Online (Sandbox Code Playgroud)

我想打一个for循环的每个字符,一次一个,例如第一的character="?",那么character="?",character="?"

我知道的唯一方法是将每个字符输出到文件中的单独行,然后使用while read line,但这似乎非常低效.

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

che*_*ner 204

您可以使用C风格的for循环:

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

${#foo}扩展到.的长度foo.${foo:$i:1}$i长度为1的位置开始扩展到子字符串.

  • @Hannibal我只想指出,双括号的这种特殊用法实际上是bash构造:`for(((_expr_; _expr_; _expr_)); 做_command_; 已完成,并且与$((_ expr_))或((_expr_))不同。在所有三个bash构造中,将_expr_视为相同,并且$((__ expr_))也是POSIX。 (5认同)
  • 我知道这是旧的,但是,两个括号是必需的,因为它们允许算术运算.见这里=> http://tldp.org/LDP/abs/html/dblparens.html (2认同)
  • @codeforester 与数组无关;它只是在算术上下文中计算的 `bash` 中的众多表达式之一。 (2认同)

Ron*_*ony 41

随着seddash的壳LANG=en_US.UTF-8,我得到了以下工作的权利:

$ echo "??? ????????" | sed -e 's/\(.\)/\1\n/g'
?
?
?

?
?
?
?
?
?
?
?
Run Code Online (Sandbox Code Playgroud)

$ echo "Hello world" | sed -e 's/\(.\)/\1\n/g'
H
e
l
l
o

w
o
r
l
d
Run Code Online (Sandbox Code Playgroud)

因此,输出可以循环 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
Run Code Online (Sandbox Code Playgroud)

  • UTF-8很好的努力.我不需要它,但无论如何你得到我的upvote. (3认同)

Tia*_*nyj 31

${#var} 返回的长度 var

${var:pos:N}从头pos开始返回N个字符

例子:

$ words="abc"
$ echo ${words:0:1}
a
$ echo ${words:1:1}
b
$ echo ${words:2:1}
c
Run Code Online (Sandbox Code Playgroud)

因此很容易迭代.

其他方式:

$ grep -o . <<< "abc"
a
b
c
Run Code Online (Sandbox Code Playgroud)

要么

$ 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
Run Code Online (Sandbox Code Playgroud)

  • 那么空白呢? (3认同)

Six*_*Six 19

我很惊讶没有人提到bash仅利用while和的明显解决方案read.

while read -n1 character; do
    echo "$character"
done < <(echo -n "$words")
Run Code Online (Sandbox Code Playgroud)

注意使用echo -n以避免最后的无关换行.printf是另一个不错的选择,可能更适合您的特殊需求.如果你想忽略空格,那么替换"$words""${words// /}".

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

while read char; do
    echo "$char"
done < <(fold -w1 <<<"$words")
Run Code Online (Sandbox Code Playgroud)

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

fold -w1 <<<"$words" | xargs -I% -- echo %
Run Code Online (Sandbox Code Playgroud)

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


国际化

我刚刚测试fold了一些亚洲字符并意识到它没有Unicode支持.因此,尽管ASCII需求很好,但它并不适用于所有人.在这种情况下,有一些替代方案.

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

awk 'BEGIN{FS=""} {for (i=1;i<=NF;i++) print $i}'
Run Code Online (Sandbox Code Playgroud)

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

grep -o .
Run Code Online (Sandbox Code Playgroud)


性能

仅供参考,我对上述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
Run Code Online (Sandbox Code Playgroud)

结果如下:

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
Run Code Online (Sandbox Code Playgroud)

  • 很好的解决方案。我发现需要将“read -n1”更改为“read -N1”才能正确处理空格字符。 (2认同)

Thu*_*eef 15

我相信仍然没有理想的解决方案可以正确保留所有空白字符并且足够快,所以我会发布我的答案.使用${foo:$i:1}作品,但非常慢,这对于大字符串尤其明显,我将在下面展示.

我的想法是对Six提出的方法进行扩展,其中包括read -n1一些更改以保留所有字符并正确处理任何字符串:

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

这个怎么运作:

  • 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")
Run Code Online (Sandbox Code Playgroud)

结果是:

$ time ./test.sh

real    0m1.161s
user    0m1.036s
sys     0m0.116s
Run Code Online (Sandbox Code Playgroud)

我们可以看到,它非常快.
接下来,我用一个使用参数扩展的循环替换了循环:

for (( i=0 ; i<${#string}; i++ )); do
    new_string+="${string:$i:1}"
done
Run Code Online (Sandbox Code Playgroud)

输出显示了性能损失的确切程度:

$ time ./test.sh

real    2m38.540s
user    2m34.916s
sys     0m3.576s
Run Code Online (Sandbox Code Playgroud)

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


Wil*_*ell 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
Run Code Online (Sandbox Code Playgroud)


De *_*ica 8

@chepner 的答案中的 C 风格循环在 shell 函数中update_terminal_cwdgrep -o .解决方案很聪明,但我很惊讶没有看到使用seq. 这是我的:

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


seb*_*bix 6

It is also possible to split the string into a character array using fold and then iterate over this array:

for char in `echo "??????" | fold -w1`; do
    echo $char
done
Run Code Online (Sandbox Code Playgroud)


小智 6

#!/bin/bash

word=$(echo 'Your Message' |fold -w 1)

for letter in ${word} ; do echo "${letter} is a letter"; done
Run Code Online (Sandbox Code Playgroud)

这是输出:

Y 是一个字母 o 是一个字母 u 是一个字母 r 是一个字母 M 是一个字母 e 是一个字母 s 是一个字母 s 是一个字母 a 是一个字母 g 是一个字母 e 是一个字母