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

时间:2012-05-11 13:07:44

标签: bash for-loop

我有一个这样的变量:

words="这是一条狗。"

我想在每个角色上制作一个for循环,一次一个,例如先是character="这",然后是character="是"character="一"等等。

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

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

14 个答案:

答案 0 :(得分:188)

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

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

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

答案 1 :(得分:38)

sed dash LANG=en_US.UTF-8 $ echo "你好嗎 新年好。全型句號" | sed -e 's/\(.\)/\1\n/g' 你 好 嗎 新 年 好 。 全 型 句 號 的{​​{1}},我的工作正确:

$ 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

循环

编辑样本文本翻译成英文:

{{1}}

答案 2 :(得分:27)

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

答案 3 :(得分:18)

我很惊讶没有人提到仅使用bashwhile的明显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")

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

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

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

<小时/> 的国际

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

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

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

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

grep -o .

<小时/> 的效果

仅供参考,我对上述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

结果如下:

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

答案 4 :(得分:13)

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

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

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

工作原理:

  • IFS='' - 将内部字段分隔符重新定义为空字符串可防止删除空格和制表符。在与read相同的行上执行此操作意味着它不会影响其他shell命令。
  • -r - 表示&#34; raw&#34;,它阻止read将行末尾的\视为特殊的行连接字符。
  • -d '' - 将空字符串作为分隔符传递,可防止read删除换行符。实际上意味着空字节用作分隔符。 -d ''等于-d $'\0'
  • -n 1 - 表示一次只读一个字符。
  • printf %s "$string" - 使用printf代替echo -n会更安全,因为echo会将-n-e视为选项。如果你通过&#34; -e&#34;作为字符串,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

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

答案 5 :(得分:12)

我只用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

答案 6 :(得分:5)

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

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

答案 7 :(得分:2)

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

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

答案 8 :(得分:1)

#!/bin/bash

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

for letter in ${word} ; do echo "${letter} is a letter"; done

以下是输出:

Y是字母 o是一封信 你是一封信 r是一个字母 M是字母 e是字母 s是字母 s是字母 一个是字母 g是一个字母 e是字母

答案 9 :(得分:1)

要在POSIX兼容的外壳程序上迭代ASCII字符,可以使用参数扩展来避免使用外部工具:

#!/bin/sh

str="Hello World!"

while [ ${#str} -gt 0 ]; do
    next=${str#?}
    echo "${str%$next}"
    str=$next
done

str="Hello World!"

while [ -n "$str" ]; do
    next=${str#?}
    echo "${str%$next}"
    str=$next
done

答案 10 :(得分:1)

sed 使用 unicode

IFS=$'\n'
for z in $(sed 's/./&\n/g' <(printf '你好嗎')); do
 echo hello: "$z"
done

输出

hello: 你
hello: 好
hello: 嗎

答案 11 :(得分:0)

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

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

答案 12 :(得分:0)

另一种方式是:

foreach(string port in ports)
{
    portMenuItem.DropDownItems.Add(port).Click += MenuItem_Click; // see definition above
}

答案 13 :(得分:-3)

ng build --prod

其中TEXT="hello world" for i in {1..${#TEXT}}; do echo ${TEXT[i]} done 是一个包含范围

{1..N}是字符串中的多个字母

${#TEXT}-您可以像从数组中的项一样从字符串中获取char