在Bash脚本中,我希望将一行分割成多个片段,并将它们存储在一个数组中。
例如,给定一行:
Paris, France, Europe
我想让结果数组看起来像这样:
array[0] = Paris
array[1] = France
array[2] = Europe
最好是一个简单的实现;速度并不重要。我该怎么做呢?
在Bash脚本中,我希望将一行分割成多个片段,并将它们存储在一个数组中。
例如,给定一行:
Paris, France, Europe
我想让结果数组看起来像这样:
array[0] = Paris
array[1] = France
array[2] = Europe
最好是一个简单的实现;速度并不重要。我该怎么做呢?
当前回答
当我想解析一个输入时,我看到了这篇文章: word1 word2,…
以上这些对我都没有帮助。使用awk解决了这个问题。如果它能帮助某人:
STRING="value1,value2,value3"
array=`echo $STRING | awk -F ',' '{ s = $1; for (i = 2; i <= NF; i++) s = s "\n"$i; print s; }'`
for word in ${array}
do
echo "This is the word $word"
done
其他回答
由于有很多方法来解决这个问题,让我们从定义我们希望在解决方案中看到的内容开始。
Bash为此提供了一个内置的readarray。让我们使用它。 避免丑陋和不必要的技巧,如更改IFS、循环、使用eval或添加一个额外的元素然后删除它。 找到一个简单易读的方法,可以很容易地适用于类似的问题。
readarray命令最容易使用换行符作为分隔符。使用其他分隔符,它可以向数组中添加一个额外的元素。最简洁的方法是,在传入输入之前,首先将输入调整为与readarray很好地工作的表单。
本例中的输入没有多字符分隔符。如果我们应用一点常识,最好将其理解为逗号分隔的输入,其中每个元素可能需要修剪。我的解决方案是用逗号将输入分割成多行,修饰每个元素,并将其全部传递给readarray。
string=' Paris,France , All of Europe '
readarray -t foo < <(tr ',' '\n' <<< "$string" |sed 's/^ *//' |sed 's/ *$//')
# Result:
declare -p foo
# declare -a foo='([0]="Paris" [1]="France" [2]="All of Europe")'
编辑:我的解决方案允许逗号分隔符周围的间距不一致,同时还允许元素包含空格。很少有其他解决方案可以处理这些特殊情况。
我也避免了那些看起来像黑客的方法,比如创建一个额外的数组元素,然后删除它。如果你不同意这是最好的答案,请留下评论来解释。
如果您想在Bash中使用更少的子shell尝试相同的方法,这是可能的。但是结果很难阅读,并且这种优化可能是不必要的。
string=' Paris,France , All of Europe '
foo="${string#"${string%%[![:space:]]*}"}"
foo="${foo%"${foo##*[![:space:]]}"}"
foo="${foo//+([[:space:]]),/,}"
foo="${foo//,+([[:space:]])/,}"
readarray -t foo < <(echo "$foo")
试试这个
IFS=', '; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done
这很简单。如果你想,你也可以添加一个声明(并删除逗号):
IFS=' ';declare -a array=(Paris France Europe)
添加IFS是为了撤销上述操作,但在新的bash实例中,它可以在没有IFS的情况下工作
这类似于Jmoney38的方法,但是使用sed:
string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
echo ${array[0]}
打印1
我很好奇"正确答案"的相对表现 在@bgoldst的流行回答中,显然是对循环的谴责, 所以我用三个纯bash实现做了一个简单的基准测试。
综上所述,我建议:
对于字符串长度< 4k左右的情况,纯bash比gawk更快 对于分隔符长度< 10和字符串长度< 256k,纯bash与gawk相当 对于分隔符长度>> 10和字符串长度< 64k左右,纯bash是“可接受的”; gawk的速度还不到5倍 对于字符串长度< 512k左右,gawk是“可接受的”
我任意地将“可接受”定义为“分割字符串所需时间< 0.5s”。
我认为问题是获取一个bash字符串并使用任意长度的分隔符字符串(不是regex)将其分割成一个bash数组。
# in: $1=delim, $2=string
# out: sets array a
我的纯bash实现是:
# naive approach - slow
split_byStr_bash_naive(){
a=()
local prev=""
local cdr="$2"
[[ -z "${cdr}" ]] && a+=("")
while [[ "$cdr" != "$prev" ]]; do
prev="$cdr"
a+=( "${cdr%%"$1"*}" )
cdr="${cdr#*"$1"}"
done
# echo $( declare -p a | md5sum; declare -p a )
}
# use lengths wherever possible - faster
split_byStr_bash_faster(){
a=()
local car=""
local cdr="$2"
while
car="${cdr%%"$1"*}"
a+=("$car")
cdr="${cdr:${#car}}"
(( ${#cdr} ))
do
cdr="${cdr:${#1}}"
done
# echo $( declare -p a | md5sum; declare -p a )
}
# use pattern substitution and readarray - fastest
split_byStr_bash_sub(){
a=()
local delim="$1" string="$2"
delim="${delim//=/=-}"
delim="${delim//$'\n'/=n}"
string="${string//=/=-}"
string="${string//$'\n'/=n}"
readarray -td $'\n' a <<<"${string//"$delim"/$'\n'}"
local len=${#a[@]} i s
for (( i=0; i<len; i++ )); do
s="${a[$i]//=n/$'\n'}"
a[$i]="${s//=-/=}"
done
# echo $( declare -p a | md5sum; declare -p a )
}
在naive版本中,初始的-z测试处理长度为零的情况 正在传递的字符串。如果没有测试,输出数组是空的; 使用它,数组只有一个长度为0的元素。
将readarray替换为while read会导致< 10%的减速。
这是我使用的gawk实现:
split_byRE_gawk(){
readarray -td '' a < <(awk '{gsub(/'"$1"'/,"\0")}1' <<<"$2$1")
unset 'a[-1]'
# echo $( declare -p a | md5sum; declare -p a )
}
显然,在一般情况下,delim参数需要被净化, 因为gawk需要一个正则表达式,而gawk-special字符可能会导致问题。 同样,按原样,该实现不会正确处理分隔符中的换行符。
由于gawk正在被使用,一个通用版本可以处理更多的任意 分隔符可以是:
split_byREorStr_gawk(){
local delim=$1
local string=$2
local useRegex=${3:+1} # if set, delimiter is regex
readarray -td '' a < <(
export delim
gawk -v re="$useRegex" '
BEGIN {
RS = FS = "\0"
ORS = ""
d = ENVIRON["delim"]
# cf. https://stackoverflow.com/a/37039138
if (!re) gsub(/[\\.^$(){}\[\]|*+?]/,"\\\\&",d)
}
gsub(d"|\n$","\0")
' <<<"$string"
)
# echo $( declare -p a | md5sum; declare -p a )
}
或者在Perl中使用相同的想法:
split_byREorStr_perl(){
local delim=$1
local string=$2
local regex=$3 # if set, delimiter is regex
readarray -td '' a < <(
export delim regex
perl -0777pe '
$d = $ENV{delim};
$d = "\Q$d\E" if ! $ENV{regex};
s/$d|\n$/\0/g;
' <<<"$string"
)
# echo $( declare -p a | md5sum; declare -p a )
}
这两个实现产生相同的输出,分别通过比较md5sum进行测试。
注意,如果输入有歧义(正如@bgoldst所说的“逻辑不正确”), 行为会略有不同。例如,使用分隔符——和字符串a-或——:
@goldst代码返回:宣布——=([0]=“a”)或宣布——=([0]=“a”[1]= " ") 我回:宣布——=([0]=“-”)或宣布——=([0]=“a”[1]=“-”)
参数由简单的Perl脚本派生,从:
delim="-=-="
base="ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"
下面是3种不同类型的计时结果表(以秒为单位) 字符串和分隔符参数的。
#s -字符串参数的长度 #d - delim参数的长度 = -性能盈亏平衡点 ! -“可接受的”性能限制(bash)在这里 !! -“可接受的”性能限制大概在这里 ——函数花了太长时间 <!> - gawk命令执行失败
1型
d=$(perl -e "print( '$delim' x (7*2**$n) )")
s=$(perl -e "print( '$delim' x (7*2**$n) . '$base' x (7*2**$n) )")
n | #s | #d | gawk | b_sub | b_faster | b_naive | |
---|---|---|---|---|---|---|---|
0 | 252 | 28 | 0.002 | 0.000 | 0.000 | 0.000 | |
1 | 504 | 56 | 0.005 | 0.000 | 0.000 | 0.001 | |
2 | 1008 | 112 | 0.005 | 0.001 | 0.000 | 0.003 | |
3 | 2016 | 224 | 0.006 | 0.001 | 0.000 | 0.009 | |
4 | 4032 | 448 | 0.007 | 0.002 | 0.001 | 0.048 | |
= | 5 | 8064 | 896 | 0.014 | 0.008 | 0.005 | 0.377 |
6 | 16128 | 1792 | 0.018 | 0.029 | 0.017 | (2.214) | |
7 | 32256 | 3584 | 0.033 | 0.057 | 0.039 | (15.16) | |
! | 8 | 64512 | 7168 | 0.063 | 0.214 | 0.128 | - |
9 | 129024 | 14336 | 0.111 | (0.826) | (0.602) | - | |
10 | 258048 | 28672 | 0.214 | (3.383) | (2.652) | - | |
!! | 11 | 516096 | 57344 | 0.430 | (13.46) | (11.00) | - |
12 | 1032192 | 114688 | (0.834) | (58.38) | - | - | |
13 | 2064384 | 229376 | <!> | (228.9) | - | - |
2型
d=$(perl -e "print( '$delim' x ($n) )")
s=$(perl -e "print( ('$delim' x ($n) . '$base' x $n ) x (2**($n-1)) )")
n | #s | #d | gawk | b_sub | b_faster | b_naive | |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0.003 | 0.000 | 0.000 | 0.000 | |
1 | 36 | 4 | 0.003 | 0.000 | 0.000 | 0.000 | |
2 | 144 | 8 | 0.005 | 0.000 | 0.000 | 0.000 | |
3 | 432 | 12 | 0.005 | 0.000 | 0.000 | 0.000 | |
4 | 1152 | 16 | 0.005 | 0.001 | 0.001 | 0.002 | |
5 | 2880 | 20 | 0.005 | 0.001 | 0.002 | 0.003 | |
6 | 6912 | 24 | 0.006 | 0.003 | 0.009 | 0.014 | |
= | 7 | 16128 | 28 | 0.012 | 0.012 | 0.037 | 0.044 |
8 | 36864 | 32 | 0.023 | 0.044 | 0.167 | 0.187 | |
! | 9 | 82944 | 36 | 0.049 | 0.192 | (0.753) | (0.840) |
10 | 184320 | 40 | 0.097 | (0.925) | (3.682) | (4.016) | |
11 | 405504 | 44 | 0.204 | (4.709) | (18.00) | (19.58) | |
!! | 12 | 884736 | 48 | 0.444 | (22.17) | - | - |
13 | 1916928 | 52 | (1.019) | (102.4) | - | - |
3型
d=$(perl -e "print( '$delim' x (2**($n-1)) )")
s=$(perl -e "print( ('$delim' x (2**($n-1)) . '$base' x (2**($n-1)) ) x ($n) )")
n | #s | #d | gawk | b_sub | b_faster | b_naive | |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0.000 | 0.000 | 0.000 | 0.000 | |
1 | 36 | 4 | 0.004 | 0.000 | 0.000 | 0.000 | |
2 | 144 | 8 | 0.003 | 0.000 | 0.000 | 0.000 | |
3 | 432 | 16 | 0.003 | 0.000 | 0.000 | 0.000 | |
4 | 1152 | 32 | 0.005 | 0.001 | 0.001 | 0.002 | |
5 | 2880 | 64 | 0.005 | 0.002 | 0.001 | 0.003 | |
6 | 6912 | 128 | 0.006 | 0.003 | 0.003 | 0.014 | |
= | 7 | 16128 | 256 | 0.012 | 0.011 | 0.010 | 0.077 |
8 | 36864 | 512 | 0.023 | 0.046 | 0.046 | (0.513) | |
! | 9 | 82944 | 1024 | 0.049 | 0.195 | 0.197 | (3.850) |
10 | 184320 | 2048 | 0.103 | (0.951) | (1.061) | (31.84) | |
11 | 405504 | 4096 | 0.222 | (4.796) | - | - | |
!! | 12 | 884736 | 8192 | 0.473 | (22.88) | - | - |
13 | 1916928 | 16384 | (1.126) | (105.4) | - | - |
长度为1..10的分隔符摘要
由于短分隔符可能比长分隔符更有可能, 下面总结了不同分隔符长度的结果 在1和10之间(结果为2..9个大多被省略为非常相似)。
s1=$(perl -e "print( '$d' . '$base' x (7*2**$n) )")
s2=$(perl -e "print( ('$d' . '$base' x $n ) x (2**($n-1)) )")
s3=$(perl -e "print( ('$d' . '$base' x (2**($n-1)) ) x ($n) )")
Bash_sub < gawk
string | n | #s | #d | gawk | b_sub | b_faster | b_naive |
---|---|---|---|---|---|---|---|
s1 | 10 | 229377 | 1 | 0.131 | 0.089 | 1.709 | - |
s1 | 10 | 229386 | 10 | 0.142 | 0.095 | 1.907 | - |
s2 | 8 | 32896 | 1 | 0.022 | 0.007 | 0.148 | 0.168 |
s2 | 8 | 34048 | 10 | 0.021 | 0.021 | 0.163 | 0.179 |
s3 | 12 | 786444 | 1 | 0.436 | 0.468 | - | - |
s3 | 12 | 786456 | 2 | 0.434 | 0.317 | - | - |
s3 | 12 | 786552 | 10 | 0.438 | 0.333 | - | - |
Bash_sub < 0.5s
string | n | #s | #d | gawk | b_sub | b_faster | b_naive |
---|---|---|---|---|---|---|---|
s1 | 11 | 458753 | 1 | 0.256 | 0.332 | (7.089) | - |
s1 | 11 | 458762 | 10 | 0.269 | 0.387 | (8.003) | - |
s2 | 11 | 361472 | 1 | 0.205 | 0.283 | (14.54) | - |
s2 | 11 | 363520 | 3 | 0.207 | 0.462 | (16.66) | - |
s3 | 12 | 786444 | 1 | 0.436 | 0.468 | - | - |
s3 | 12 | 786456 | 2 | 0.434 | 0.317 | - | - |
s3 | 12 | 786552 | 10 | 0.438 | 0.333 | - | - |
Gawk < 0.5s
string | n | #s | $d | gawk | b_sub | b_faster | b_naive |
---|---|---|---|---|---|---|---|
s1 | 11 | 458753 | 1 | 0.256 | 0.332 | (7.089) | - |
s1 | 11 | 458762 | 10 | 0.269 | 0.387 | (8.003) | - |
s2 | 12 | 788480 | 1 | 0.440 | (1.252) | - | - |
s2 | 12 | 806912 | 10 | 0.449 | (4.968) | - | - |
s3 | 12 | 786444 | 1 | 0.436 | 0.468 | - | - |
s3 | 12 | 786456 | 2 | 0.434 | 0.317 | - | - |
s3 | 12 | 786552 | 10 | 0.438 | 0.333 | - | - |
(我不完全确定为什么bash_sub与s>160k和d=1始终比d>1 s3慢。)
所有测试都是在Intel i7-7500U上运行xubuntu 20.04,使用bash 5.0.17进行的。
IFS=', ' read -r -a array <<< "$string"
请注意,$IFS中的字符被单独视为分隔符,因此在这种情况下,字段可以用逗号或空格分隔,而不是两个字符的序列。但有趣的是,当输入中出现逗号时,不会创建空字段,因为空格是经过特殊处理的。
要访问单个元素:
echo "${array[0]}"
要遍历元素:
for element in "${array[@]}"
do
echo "$element"
done
要同时获取索引和值:
for index in "${!array[@]}"
do
echo "$index ${array[index]}"
done
最后一个示例很有用,因为Bash数组很稀疏。换句话说,您可以删除一个元素或添加一个元素,然后索引不是连续的。
unset "array[1]"
array[42]=Earth
获取数组中元素的个数:
echo "${#array[@]}"
如上所述,数组可以是稀疏的,所以不应该使用长度来获取最后一个元素。以下是在Bash 4.2及以后版本中可以做到的:
echo "${array[-1]}"
在任何版本的Bash中(从2.05b之后的某个地方):
echo "${array[@]: -1:1}"
较大的负偏移量选择距离数组末端更远的位置。注意旧形式中负号前面的空格。这是必须的。