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

我在 Bash shell 脚本中有一个字符串,我想将它分割成一个字符数组,不是基于分隔符,而是基于每个数组索引一个字符。我怎么能这么做?让我换个说法。我的目标是可移植性,所以像 sed这样的东西可能在任何 POSIX 兼容系统上都是可行的。

106319 次浏览

您可以单独访问每个字母,而不需要进行数组转换:

$ 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或类似的东西,您可以使用上面的第一种技术,结合使用原始字符串长度(${#foo})的 while 循环来构建数组。

警告: 如果字符串包含空格,下面的代码将无法工作。我认为 Vaughn Cato 的回答在使用特殊字符时有更好的生存机会。

thing=($(i=0; while [ $i -lt ${#foo} ] ; do echo ${foo:$i:1} ; i=$((i+1)) ; done))

如果字符串存储在变量 x 中,这将生成一个包含单个字符的数组 y:

i=0
while [ $i -lt ${#x} ]; do y[$i]=${x:$i:1};  i=$((i+1));done

试试看

echo "abcdefg" | fold -w1

编辑: 在评论中添加了一个更优雅的解决方案。

echo "abcdefg" | grep -o .

如果你想把它存储在一个数组中,你可以这样做:

string=foo
unset chars
declare -a chars
while read -N 1
do
chars[${#chars[@]}]="$REPLY"
done <<<"$string"x
unset chars[$((${#chars[@]} - 1))]
unset chars[$((${#chars[@]} - 1))]


echo "Array: ${chars[@]}"
Array: f o o
echo "Array length: ${#chars[@]}"
Array length: 3

最后一个 x对于处理这样一个事实是必要的: 如果 $string不包含换行符,那么它就会被添加到 $string之后。

如果要使用 NUL 分隔的字符,可以尝试这样做:

echo -n "$string" | while read -N 1
do
printf %s "$REPLY"
printf '\0'
done

AWK 相当方便:

a='123'; echo $a | awk 'BEGIN{FS="";OFS=" "} {print $1,$2,$3}'

其中 FSOFS是读入和打印的分隔符

如果文本可以包含空格:

eval a=( $(echo "this is a test" | sed "s/\(.\)/'\1' /g") )
$ echo hello | awk NF=NF FS=
h e l l o

或者

$ echo hello | awk '$0=RT' RS=[[:alnum:]]
h
e
l
l
o

作为使用 for/while 循环迭代 0 .. ${#string}-1的一种替代方法,我可以想到使用 只有重击的另外两种方法: 使用 =~和使用 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

这种工作方式是执行 string的扩展,将每个字符替换为 (.),然后将生成的正则表达式与分组相匹配,将每个字符捕获到 BASH_REMATCH[]中。索引0被设置为整个字符串,因为这个特殊的数组是只读的,您不能删除它,如果需要的话,当数组展开到跳过索引0时请注意 :1。 对非平凡字符串(> 64个字符)的一些快速测试表明,这种方法比使用 bash 字符串和数组操作的方法快 实质上

上面的代码将处理包含换行符的字符串,=~默认支持 其中 .匹配除 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)而不是错误状态,而不是使用 %nxx中捕获打印的字符数,那么这将更加简单。(这至少可以追溯到 bash-2.05 b)

有了 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
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 ]

最简单、最完整、最优雅的解决方案:

$ read -a ARRAY <<< $(echo "abcdefg" | sed 's/./& /g')

测试

$ echo ${ARRAY[0]}
a


$ echo ${ARRAY[1]}
b

说明 : read -a将 stdin 读取为一个数组,并将其分配给变量 ARRAY,该变量将空格作为每个数组项的分隔符。

对回显字符串到 sed 的计算只需在每个字符之间添加所需的空格。

我们使用 < strong > < em > Here String (< < <)来提供 read 命令的 stdin。

对于那些降落在这里寻找如何在 做到这一点:

我们可以使用内置的 string命令(自2.3.0版本以来)进行字符串操作。

↪ string split '' abc
a
b
c

输出是一个列表,因此数组操作可以正常工作。

↪ for c in (string split '' abc)
echo char is $c
end
char is a
char is b
char is c

下面是一个更复杂的示例,它使用索引对字符串进行迭代。

↪ set --local chars (string split '' abc)
for i in (seq (count $chars))
echo $i: $chars[$i]
end
1: a
2: b
3: c

如果还需要支持带换行符的字符串,可以这样做:

str2arr(){ local string="$1"; mapfile -d $'\0' Chars < <(for i in $(seq 0 $((${#string}-1))); do printf '%s\u0000' "${string:$i:1}"; done); printf '%s' "(${Chars[*]@Q})" ;}
string=$(printf '%b' "apa\nbepa")
declare -a MyString=$(str2arr "$string")
declare -p MyString
# prints declare -a MyString=([0]="a" [1]="p" [2]="a" [3]=$'\n' [4]="b" [5]="e" [6]="p" [7]="a")

作为对亚历山德罗•德•奥利维拉(Alexandro de Oliveira)的回应,我认为以下内容更为优雅,或者至少更为直观:

while read -r -n1 c ; do arr+=("$c") ; done <<<"hejsan"

Zsh 解决方案: 要将标量 string变量放入 arr,它将是一个数组:

arr=(${(ps::)string})

我发现以下方法最有效:

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

(注意倒勾)

那么如果你这样做: echo ${array[@]}, 你得到: s t r i n g

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

还有一个问题:) ,这个问题只是简单地说“将字符串拆分成字符数组”,并且没有提到接收数组的状态,也没有提到特殊的字符,比如控制字符。

我的假设是,如果我想把一个字符串拆分成一个字符数组,我希望接收到的数组只包含那个字符串,不包含以前运行的剩余字符,同时保留任何特殊的字符。

例如,建议的解决方案族如

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
declare -r some_string='abcdefghijklmnopqrstuvwxyz'
declare -a some_array
declare -i idx


for ((idx = 0; idx < ${#some_string}; ++idx)); do
some_array+=("${some_string:idx:1}")
done


for idx in "${!some_array[@]}"; do
echo "$((idx)): ${some_array[idx]}"
done

没有循环的纯 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

我知道这是一个“ bash”问题,但是请让我向您展示 zsh 中的完美解决方案,这是一个最近非常流行的 shell:

string='this is a string'
string_array=(${(s::)string})  #Parameter expansion. And that's it!


print ${(t)string_array}  -> type array
print $#string_array -> 16 items

纯粹的狂欢,没有循环。

另一种解决方案,类似于/改编自 Lea Gris 的解决方案,但使用 read -a而不是 readarray/mapfile:

#!/usr/bin/env bash


str='azerty'


# Need extglob for the replacement pattern
shopt -s extglob


# Split string characters into array
# ${str//?()/$'\x1F'} replace each character "c" with "^_c".
# ^_ (Control-_, 0x1f) is Unit Separator (US), you can choose another
# character.
IFS=$'\x1F' read -ra array <<< "${str//?()/$'\x1F'}"


# now, array[0] contains an empty string and the rest of array (starting
# from index 1) contains the original string characters :
declare -p array


# Or, if you prefer to keep the array "clean", you can delete
# the first element and pack the array :
unset array[0]
array=("${array[@]}")
declare -p array

然而,我更喜欢较短的(对我来说也更容易理解) ,在分配数组之前删除初始的 0x1f:

#!/usr/bin/env bash


str='azerty'
shopt -s extglob


tmp="${str//?()/$'\x1F'}"              # same as code above
tmp=${tmp#$'\x1F'}                     # remove initial 0x1f
IFS=$'\x1F' read -ra array <<< "$tmp"  # assign array


declare -p array                       # verification