Bash:将字符串拆分为字符数组

77
我有一个 Bash shell 脚本中的字符串,我想将它分成一个字符的数组,不基于分隔符而是每个数组索引只有一个字符。我该如何做?最好不使用任何外部程序。让我重新表达一下,我的目标是可移植性,因此像 sed 这样的可能在任何 POSIX 兼容系统上都存在的东西都是可以接受的。

2
如果您的平台是POSIX,那么“bash”不是默认提供的。 - tripleee
3
@tripleee 这两个东西都不是数组。 - lhunath
当然。我正在努力理解这个问题。也许OP的意思是在一个其他POSIX系统上针对Bash? - tripleee
1
最初的意图是创建一个可以在不了解用户平台的情况下在线共享的shell脚本。因此,我希望尽可能地具有跨OS X、Ubuntu等多个操作系统的兼容性。我不需要与Unix的奇异变体100%兼容。 - n s
在bash中,您可以使用${string:1:1}提取单个字符,但如果您需要支持POSIX sh,则不能假设您拥有它,也不能假设您根本具有数组。 - Charles Duffy
显示剩余2条评论
20个回答

151

尝试一下

echo "abcdefg" | fold -w1

编辑:添加了评论中提出的更优雅的解决方案。

echo "abcdefg" | grep -o .

2
尽管使用了外部命令,但为了简洁起见,+1。 - Dimitre Radoulov
10
这段代码 echo "abcdefg" | grep -o . 是在 Bash 环境下将字符串拆分为单个字符的一种优雅方式。 - tripleee
7
@xdazz这段代码在Unicode上不起作用。试试这个echo "عمر" | fold -w1,它会打印出空格和问号。然而,@tripleee的解决方法echo "عمر" | grep -o .可以很好地解决问题。有趣的是,一些小程序无法通过https://dev59.com/O3RA5IYBdhLWcg3w4R_o :). 无论如何,还是感谢您优雅的回答。 - Omar Al-Ithawi
2
@OmarIthawi 谢谢,已添加到答案中。 - xdazz
@OmarIthawi:这两种变体在我的Mac OS X和Linux CentOS 6.5上都可以工作,所以似乎问题并不像“折叠解决方案不能处理Unicode”那么简单。 - erik.weathers
显示剩余5条评论

41

你已经可以单独访问每个字母,无需进行数组转换:

$ foo="bar"
$ echo ${foo:0:1}
b
$ echo ${foo:1:1}
a
$ echo ${foo:2:1}
r

如果这还不够,你可以使用类似这样的方法:

$ bar=($(echo $foo|sed  's/\(.\)/\1 /g'))
$ echo ${bar[1]}
a
如果你甚至不能使用sed或类似的工具,你可以使用上述第一种技术,结合使用while循环和原始字符串的长度(${#foo})来构建数组。
警告:如果字符串包含空格,则下面的代码不起作用。我认为Vaughn Cato的答案有更好的机会在处理特殊字符时生效。
thing=($(i=0; while [ $i -lt ${#foo} ] ; do echo ${foo:$i:1} ; i=$((i+1)) ; done))

3
别忘了加引号:echo "$foo" - glenn jackman
3
你建议的循环:for i in $(seq ${#foo}); do echo "${foo:$i-1:1}"; done - wjandrea
@wjandrea seq 不符合 POSIX 标准。 - Charles Duffy
@wjandrea seq 不符合 POSIX 标准。 - Charles Duffy

17
作为一种替代使用 for/while 循环来迭代 0 .. ${#string}-1 的方式,我能想到只使用 bash 有两种方法:使用 =~ 和使用 printf。(还有第三个可能性是使用 eval{..} 序列表达式,但这会使代码缺乏清晰性。)
如果在 bash 中启用了正确的环境和 NLS,这些方法将可以处理非 ASCII 字符,避免了旧系统工具(如 sed)可能导致的失败问题。这些方法从 bash-3.0 开始就可用(发布于 2005 年)。
使用 =~ 和正则表达式,将字符串转换为数组,只需一个表达式即可:
string="wonkabars"
[[ "$string" =~ ${string//?/(.)} ]]       # splits into array
printf "%s\n" "${BASH_REMATCH[@]:1}"      # loop free: reuse fmtstr
declare -a arr=( "${BASH_REMATCH[@]:1}" ) # copy array for later
这个方法的原理是对字符串进行扩展,将每个单个字符替换为(.),然后使用分组来捕获每个单独字符到BASH_REMATCH[]中生成的正则表达式匹配。由于特殊数组是只读的,因此索引0被设置为整个字符串,无法删除它,请注意在扩展数组时跳过索引0,如果需要可以使用:1。 针对长度较大(>64个字符)的非平凡字符串进行的一些快速测试表明,这种方法比使用bash字符串和数组操作的方法要明显地快得多。
上述方法适用于包含换行符的字符串,默认情况下=~支持POSIX ERE,在该模式中.除NUL外可匹配任何字符,即编译的正则表达式不包括REG_NEWLINE。(在此方面,POSIX文本处理实用程序的行为默认情况下允许有所不同,并且通常确实如此。)
第二种选项是使用printf
string="wonkabars"
ii=0
while printf "%s%n" "${string:ii++:1}" xx; do 
  ((xx)) && printf "\n" || break
done 

这个循环递增索引ii以逐个打印字符,并在没有剩余字符时退出。如果bash的printf像C语言一样返回所打印字符数而不是错误状态,那么这将更加简单,但实际上所打印字符的数量被用%n捕获到了变量xx中。(至少从bash-2.05b开始有效。)

使用bash-3.1和printf -v var,您有更多的灵活性,可以避免因执行其他操作(例如创建数组)而导致越界问题:

declare -a arr
ii=0
while printf -v cc "%s%n" "${string:(ii++):1}" xx; do 
    ((xx)) && arr+=("$cc") || break
done

我认为自己在Bash方面相当有知识,但是总有新的东西需要学习。这些都是非常酷的技巧,谢谢! - Shaun Mitchell

15
如果您的字符串存储在变量x中,以下代码将生成一个包含各个字符的数组y:
i=0
while [ $i -lt ${#x} ]; do y[$i]=${x:$i:1};  i=$((i+1));done

17
这个Bash命令:for (( i=0 ; i < ${#x} ; i++ )); do y[i]=${x:i:1}; done 看起来更符合惯用的写法。 - user2350426
这是老话题了,但我想说的是bash每次计算字符串长度时都会读取整个字符串,因此将其放入变量中会更好。或者,从长度开始向下计算也可以。 - phicr

6

我发现以下方法最佳:

array=( `echo string | grep -o . ` )

注意反引号:

如果你执行以下命令:echo ${array[@]}, 你会得到:s t r i n g

或者: echo ${array[2]}, 你会得到:r


好的解决方案。反引号在这种情况下以及其他用法中是如何工作的? - Itzik Chaimov
反引号表示执行两个反引号之间的命令,并用该命令的输出替换反引号和其中的内容。 括号创建一个数组,并将其显式地分配给变量“array”,正如提问者所请求的那样。 - AZAhmed

6
最简单、完整和优雅的解决方案:
$ read -a ARRAY <<< $(echo "abcdefg" | sed 's/./& /g')  

并测试

$ echo ${ARRAY[0]}
  a

$ echo ${ARRAY[1]}
  b
< p > 解释read -a 命令将标准输入读取为一个数组,并将其分配给变量 ARRAY,将空格视为每个数组项的分隔符。

通过将字符串回显到 sed 中进行评估,只需在每个字符之间添加所需的空格即可。

我们使用Here String (<<<) 来提供 read 命令的标准输入。


5

无需循环的纯Bash解决方案:

#!/usr/bin/env bash

str='The quick brown fox jumps over a lazy dog.'

# Need extglob for the replacement pattern
shopt -s extglob

# Split string characters into array (skip first record)
# Character 037 is the octal representation of ASCII Record Separator
# so it can capture all other characters in the string, including spaces.
IFS= mapfile -s1 -t -d $'\37' array <<<"${str//?()/$'\37'}"

# Strip out captured trailing newline of here-string in last record
array[-1]="${array[-1]%?}"

# Debug print array
declare -p array 

不错,你可以:- 创建一个函数并且 - 发布一些示例! - F. Hauri - Give Up GitHub

3
string=hello123

for i in $(seq 0 ${#string})
    do array[$i]=${string:$i:1}
done

echo "zero element of array is [${array[0]}]"
echo "entire array is [${array[@]}]"

数组的零元素是[h]。整个数组是[h e l l o 1 2 3 ]

1
这些子字符串提取操作优于涉及通过子进程传递字符串的等效解决方案。 - sdenham

2

又有一个问题:'将字符串拆分为字符数组',但没有详细说明接收数组的状态,也没有详细说明特殊字符(如空格和控制字符)。

我的假设是,如果我想将字符串拆分为字符数组,我希望接收数组仅包含该字符串,不留任何之前运行的内容,并保留任何特殊字符。

例如,提出的解决方案类似于

for (( i=0 ; i < ${#x} ; i++ )); do y[i]=${x:i:1}; done

目标数组中有剩余项。

$ y=(1 2 3 4 5 6 7 8)
$ x=abc
$ for (( i=0 ; i < ${#x} ; i++ )); do y[i]=${x:i:1}; done
$ printf '%s ' "${y[@]}"
a b c 4 5 6 7 8 

除了每次想要拆分问题时都写一长串代码,为什么不将所有这些隐藏到一个函数中,我们可以将其保存在包源文件中,并使用类似以下的API:

s2a "Long string" ArrayName

我找到了一个似乎能胜任的方案。
$ s2a()
> { [ "$2" ] && typeset -n __=$2 && unset $2;
>   [ "$1" ] && __+=("${1:0:1}") && s2a "${1:1}"
> }

$ a=(1 2 3 4 5 6 7 8 9 0) ; printf '%s ' "${a[@]}"
1 2 3 4 5 6 7 8 9 0 

$ s2a "Split It" a        ; printf '%s ' "${a[@]}"
S p l i t   I t 

1
这是一个旧的帖子/线程,但使用bash v5.2+的新特性,使用shell选项patsub_replacement=~操作符进行正则表达式。与@mr.spuratic的帖子/答案基本相同。
str='There can be only one, the Highlander.'
regexp="${str//?/(&)}"
[[ "$str" =~ $regexp ]] &&
printf '%s\n' "${BASH_REMATCH[@]:1}"

或者只需:(这包括索引0处的整个字符串)
declare -p BASH_REMATCH

如果不需要这样做,可以使用以下方法删除第一个索引(索引0)的值:
unset -v 'BASH_REMATCH[0]'

使用printfecho来打印数组BASH_REMATCH的值已经不再需要。
可以使用以下任意一种方式来检查/查看变量"$regexp"的值:
declare -p regexp

输出

declare -- regexp="(T)(h)(e)(r)(e)( )(c)(a)(n)( )(b)(e)( )(o)(n)(l)(y)( )(o)(n)(e)(,)( )(t)(h)(e)( )(H)(i)(g)(h)(l)(a)(n)(d)(e)(r)(.)"

或者

echo "$regexp"

在脚本中使用它时,可能想要测试是否启用了shopt,尽管手册说默认情况下它是开启的/启用的。
类似这样的内容。
if ! shopt -q patsub_replacement; then
  shopt -s patsub_replacement
fi

但是,确保也检查一下 bash 版本!如果您不确定正在使用哪个版本的 bash
if ! ((BASH_VERSINFO[0] >= 5 && BASH_VERSINFO[1] >= 2)); then
  printf 'No dice! bash version 5.2+ is required!\n' >&2
  exit 1
fi

空格可以从regexp变量中排除,将其更改为
regexp="${str//?/(&)}"

"到"
regexp="${str//[! ]/(&)}"

"输出结果为:"
declare -- regexp="(T)(h)(e)(r)(e) (c)(a)(n) (b)(e) (o)(n)(l)(y) (o)(n)(e) (t)(h)(e) (H)(i)(g)(h)(l)(a)(n)(d)(e)(r)(.)"

也许不像其他帖子/答案那么高效,但它仍然是一个解决方案/选项。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接