在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
最好是一个简单的实现;速度并不重要。我该怎么做呢?
当前回答
将字符串分割为数组的关键是多字符分隔符“,”。任何使用IFS进行多字符分隔符的解决方案本质上都是错误的,因为IFS是这些字符的集合,而不是字符串。
如果指定IFS=", ","则字符串将在"," OR " "或它们的任何组合上中断,这不是","这两个字符分隔符的准确表示。
你可以使用awk或sed来拆分字符串,使用进程替换:
#!/bin/bash
str="Paris, France, Europe"
array=()
while read -r -d $'\0' each; do # use a NUL terminated field separator
array+=("$each")
done < <(printf "%s" "$str" | awk '{ gsub(/,[ ]+|$/,"\0"); print }')
declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output
在Bash中直接使用正则表达式更有效:
#!/bin/bash
str="Paris, France, Europe"
array=()
while [[ $str =~ ([^,]+)(,[ ]+|$) ]]; do
array+=("${BASH_REMATCH[1]}") # capture the field
i=${#BASH_REMATCH} # length of field + delimiter
str=${str:i} # advance the string by that length
done # the loop deletes $str, so make a copy if needed
declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output...
使用第二种形式,没有子外壳,它将固有地更快。
编辑by bgoldst:下面是一些比较我的readarray解决方案和dawg的regex解决方案的基准测试,我还包括了read解决方案(注:我稍微修改了regex解决方案,以使其与我的解决方案更加和谐)(也可以参阅我的帖子下面的评论):
## competitors
function c_readarray { readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); unset 'a[-1]'; };
function c_read { a=(); local REPLY=''; while read -r -d ''; do a+=("$REPLY"); done < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); };
function c_regex { a=(); local s="$1, "; while [[ $s =~ ([^,]+),\ ]]; do a+=("${BASH_REMATCH[1]}"); s=${s:${#BASH_REMATCH}}; done; };
## helper functions
function rep {
local -i i=-1;
for ((i = 0; i<$1; ++i)); do
printf %s "$2";
done;
}; ## end rep()
function testAll {
local funcs=();
local args=();
local func='';
local -i rc=-1;
while [[ "$1" != ':' ]]; do
func="$1";
if [[ ! "$func" =~ ^[_a-zA-Z][_a-zA-Z0-9]*$ ]]; then
echo "bad function name: $func" >&2;
return 2;
fi;
funcs+=("$func");
shift;
done;
shift;
args=("$@");
for func in "${funcs[@]}"; do
echo -n "$func ";
{ time $func "${args[@]}" >/dev/null 2>&1; } 2>&1| tr '\n' '/';
rc=${PIPESTATUS[0]}; if [[ $rc -ne 0 ]]; then echo "[$rc]"; else echo; fi;
done| column -ts/;
}; ## end testAll()
function makeStringToSplit {
local -i n=$1; ## number of fields
if [[ $n -lt 0 ]]; then echo "bad field count: $n" >&2; return 2; fi;
if [[ $n -eq 0 ]]; then
echo;
elif [[ $n -eq 1 ]]; then
echo 'first field';
elif [[ "$n" -eq 2 ]]; then
echo 'first field, last field';
else
echo "first field, $(rep $[$1-2] 'mid field, ')last field";
fi;
}; ## end makeStringToSplit()
function testAll_splitIntoArray {
local -i n=$1; ## number of fields in input string
local s='';
echo "===== $n field$(if [[ $n -ne 1 ]]; then echo 's'; fi;) =====";
s="$(makeStringToSplit "$n")";
testAll c_readarray c_read c_regex : "$s";
}; ## end testAll_splitIntoArray()
## results
testAll_splitIntoArray 1;
## ===== 1 field =====
## c_readarray real 0m0.067s user 0m0.000s sys 0m0.000s
## c_read real 0m0.064s user 0m0.000s sys 0m0.000s
## c_regex real 0m0.000s user 0m0.000s sys 0m0.000s
##
testAll_splitIntoArray 10;
## ===== 10 fields =====
## c_readarray real 0m0.067s user 0m0.000s sys 0m0.000s
## c_read real 0m0.064s user 0m0.000s sys 0m0.000s
## c_regex real 0m0.001s user 0m0.000s sys 0m0.000s
##
testAll_splitIntoArray 100;
## ===== 100 fields =====
## c_readarray real 0m0.069s user 0m0.000s sys 0m0.062s
## c_read real 0m0.065s user 0m0.000s sys 0m0.046s
## c_regex real 0m0.005s user 0m0.000s sys 0m0.000s
##
testAll_splitIntoArray 1000;
## ===== 1000 fields =====
## c_readarray real 0m0.084s user 0m0.031s sys 0m0.077s
## c_read real 0m0.092s user 0m0.031s sys 0m0.046s
## c_regex real 0m0.125s user 0m0.125s sys 0m0.000s
##
testAll_splitIntoArray 10000;
## ===== 10000 fields =====
## c_readarray real 0m0.209s user 0m0.093s sys 0m0.108s
## c_read real 0m0.333s user 0m0.234s sys 0m0.109s
## c_regex real 0m9.095s user 0m9.078s sys 0m0.000s
##
testAll_splitIntoArray 100000;
## ===== 100000 fields =====
## c_readarray real 0m1.460s user 0m0.326s sys 0m1.124s
## c_read real 0m2.780s user 0m1.686s sys 0m1.092s
## c_regex real 17m38.208s user 15m16.359s sys 2m19.375s
##
其他回答
有时,我发现在已接受的答案中描述的方法不起作用,特别是当分隔符是回车符时。 在这些情况下,我是这样解决的:
string='first line
second line
third line'
oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"
for line in "${lines[@]}"
do
echo "--> $line"
done
更新:不要这样做,由于eval的问题。
不那么讲究礼节的:
IFS=', ' eval 'array=($string)'
e.g.
string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar
我很好奇"正确答案"的相对表现 在@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进行的。
这适用于我在OSX:
string="1 2 3 4 5"
declare -a array=($string)
如果你的字符串有不同的分隔符,首先用空格替换它们:
string="1,2,3,4,5"
delimiter=","
declare -a array=($(echo $string | tr "$delimiter" " "))
简单:-)
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"
打印三