在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
最好是一个简单的实现;速度并不重要。我该怎么做呢?
当前回答
由于有很多方法来解决这个问题,让我们从定义我们希望在解决方案中看到的内容开始。
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的情况下工作
接受的答案适用于一行中的值。如果变量有几行:
string='first line
second line
third line'
我们需要一个非常不同的命令来获取所有的行:
While read -r line;做线+ =(“美元线”);完成了< < <字符串" $ "
或者更简单的bash readarray:
readarray -t lines <<<"$string"
利用printf特性很容易打印所有行:
printf ">[%s]\n" "${lines[@]}"
>[first line]
>[ second line]
>[ third line]
将字符串分割为数组的关键是多字符分隔符“,”。任何使用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
##
另一种方法是:
str="a, b, c, d" # assuming there is a space after ',' as in Q
arr=(${str//,/}) # delete all occurrences of ','
在这个'arr'后面是一个包含四个字符串的数组。 这不需要处理IFS或读取或任何其他特殊的东西,因此更简单和直接。
这类似于Jmoney38的方法,但是使用sed:
string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
echo ${array[0]}
打印1