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


82

我有一个像这样的变量:

words="这是一条狗。"

我想打一个for循环的每个字符,一次一个,例如,第一character="这",然后character="是"character="一"

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

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

3
值得一提的是,OP认为这是他们想要做的事情,因此我们遇到了许多新手问题。通常,不需要每个字符都单独处理的更好解决方案是可能的。这就是所谓的XY问题,正确的解决方案是解释您在问题中实际想要完成的工作,而不仅仅是解释如何执行您认为会帮助您实现目标的步骤。
Tripleee '18

Answers:


45

随着seddash的壳LANG=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

4
在UTF-8上做得很好。我不需要,但无论如何您都会得到我的支持。
约旦

+1您可以对sed生成的字符串使用for循环。
Tyzoid 2014年

233

您可以使用C样式的for循环:

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

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


为什么要在for语句周围需要两套方括号才能使其起作用?
tgun926

这是语法bash要求。
chepner

3
我知道这很旧,但是需要两个括号,因为它们允许进行算术运算。看到这里=> tldp.org/LDP/abs/html/dblparens.html
汉尼拔

8
@Hannibal我只是想指出,双括号的这种特殊用法实际上是bash构造:for (( _expr_ ; _expr_ ; _expr_ )) ; do _command_ ; done与$(((expr))或((expr))都不相同。在所有三个bash构造中,将expr视为相同,并且$((expr))也是POSIX。
nabin-info

1
@codeforester与数组无关;它只是bash在算术上下文中求值的众多表达式之一。
chepner

36

${#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

1
空格呢?
Leandro

什么关于空白?空格字符是一个字符,它会遍历所有字符。(尽管您应注意在包含显着空格的任何变量或字符串周围使用双引号。更普遍的是,除非您知道自己在做什么,否则总是用引号引起来。
Tripleee

23

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

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

使用foldcoreutils软件包的)外部命令的主要好处是简洁。您可以将其输出提供给另一个命令,例如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 .


性能

仅供参考,我以上述三个选项为基准。前两个速度很快,几乎是平手,而fold循环比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

character对于简单的while read解决方案,空格是空白的,如果必须区分不同类型的空格,则可能会出现问题。
pkfm

不错的解决方案。我发现需要更改read -n1read -N1才能正确处理空格字符。
nielsen

16

我相信仍然没有理想的解决方案来正确保留所有空白字符并且速度足够快,因此我将发布答案。使用${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

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


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

8

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

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

6

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

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

1
#!/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是字母


0

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

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



-3
TEXT="hello world"
for i in {1..${#TEXT}}; do
   echo ${TEXT[i]}
done

{1..N}包含范围在哪里

${#TEXT} 是字符串中的字母数

${TEXT[i]} -您可以像处理数组中的项一样从字符串中获取char


5
Shellcheck报告“ Bash在括号范围扩展中不支持变量”,因此这在Bash中不起作用
Bren

@Bren对我来说似乎是个虫子。
Sapphire_Brick
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.