在Bash中将字符串拆分为数组

Split string into an array in Bash

在bash脚本中,我希望将一行拆分为多个部分,并将它们存储在一个数组中。

行:

1
Paris, France, Europe

我想把它们放在这样的数组中:

1
2
3
array[0] = Paris
array[1] = France
array[2] = Europe

我想用简单的代码,命令的速度不重要。我该怎么做?


1
IFS=', ' read -r -a array <<<"$string"

注意,$IFS中的字符单独被视为分隔符,因此在这种情况下,字段可以由逗号或空格分隔,而不是由两个字符的序列分隔。有趣的是,当输入中出现逗号空间时,不会创建空字段,因为该空间是经过特殊处理的。

要访问单个元素:

1
echo"${array[0]}"

迭代元素:

1
2
3
4
for element in"${array[@]}"
do
    echo"$element"
done

要同时获取索引和值:

1
2
3
4
for index in"${!array[@]}"
do
    echo"$index ${array[index]}"
done

最后一个例子很有用,因为bash数组是稀疏的。换句话说,您可以删除一个元素或添加一个元素,然后索引就不连续了。

1
2
unset"array[1]"
array[42]=Earth

要获取数组中的元素数:

1
echo"${#array[@]}"

如上所述,数组可以是稀疏的,因此不应该使用长度来获取最后一个元素。以下是您在bash 4.2及更高版本中的方法:

1
echo"${array[-1]}"

在任何版本的bash中(在2.05b之后的某个地方):

1
echo"${array[@]: -1:1}"

较大的负偏移选择距阵列末端更远的位置。请注意旧窗体中减号前的空格。这是必需的。


这个问题的所有答案在某种程度上都是错误的。好的。

回答错误1好的。

1
IFS=', ' read -r -a array <<<"$string"

1:这是对$IFS的滥用。$IFS变量的值不作为单变量长度字符串分隔符,而是作为一组单字符串分隔符,其中read从输入行拆分的每个字段都可以由集合中的任何字符(本例中为逗号或空格)终止。好的。

实际上,对于真正的黏性者来说,$IFS的完整含义稍微复杂一些。从bash手册:好的。

The shell treats each character of IFS as a delimiter, and splits the results of the other expansions into words using these characters as field terminators. If IFS is unset, or its value is exactly , the default, then sequences of , , and at the beginning and end of the results of the previous expansions are ignored, and any sequence of IFS characters not at the beginning or end serves to delimit words. If IFS has a value other than the default, then sequences of the whitespace characters , , and are ignored at the beginning and end of the word, as long as the whitespace character is in the value of IFS (an IFS whitespace character). Any character in IFS that is not IFS whitespace, along with any adjacent IFS whitespace characters, delimits a field. A sequence of IFS whitespace characters is also treated as a delimiter. If the value of IFS is null, no word splitting occurs.

Ok.

基本上,对于$IFS的非默认非空值,字段可以用(1)一个或多个字符序列来分隔,这些字符都来自于"ifs whitespace characters"集合(也就是说,在$IFS中的任何位置都存在("newline"表示换行(lf)),或者(2)任何非"ifs whitese"在$IFS中出现的速度字符"以及在输入行中围绕它的任何"ifs空白字符"。好的。

对于OP,上一段中描述的第二个分离模式可能正是他想要的输入字符串,但我们可以非常确信我描述的第一个分离模式根本不正确。例如,如果他的输入字符串是'Los Angeles, United States, North America'呢?好的。

1
2
IFS=', ' read -ra a <<<'Los Angeles, United States, North America'; declare -p a;
## declare -a a=([0]="Los" [1]="Angeles" [2]="United" [3]="States" [4]="North" [5]="America")

2:如果你被连到使用这个解决方案与一个单一的字符separator(一个世纪的itself由逗号,这也与以下,没有空间或其他baggage),如果value"的$string可变happens到含有任何LFS,然后read将停止处理它的第一次遭遇LF。"readbuiltin唯一的过程中每一行的invocation。这是真实的,即使你是piping或redirecting只输入 readstatement,我们做的是,在这个例子与睾丸mechanism和输入字符串,因此unprocessed也保证要失去了。"code,权力的readbuiltin腹部没有知识的数据流在其包含的指挥结构。 <P / S>。

你可以argue这unlikely到原因的问题,但仍然, subtle危害,应该avoided如果可能。它也引起了由"事实上,readbuiltin实际上并levels两部:第一splitting输入到线,然后到的领域。由于"OP只想要一个水平的splitting,这usage之readbuiltin不适当的情况下,我们应该避免它。 <P / S>。

3:非太明显了潜在的问题与解决方案,这是read总是drops的trailing领域,如果它是空的,虽然它preserves另有空字段。这里的演示: <P / S>。

1
2
string=', , a, , b, c, , , '; IFS=', ' read -ra a <<<"$string"; declare -p a;
## declare -a a=([0]="" [1]="" [2]="a" [3]="" [4]="b" [5]="c" [6]="" [7]="")

也许"OP不会在乎这一点,但它仍然是在限制的价值认识的意义。它的reduces robustness和generality"的解决方案。 <P / S>。

这个问题可以通过solved appending虚拟trailing delimiter 输入字符串之前就到read饲养它,我将demonstrate以后。 <P / S>。

错误的答案# 2 <P / S>。

1
2
3
string="1:2:3:4:5"
set -f                     # avoid globbing (expansion of *).
array=(${string//:/ })

similar理念: <P / S>。

1
2
3
t="one,two,three"
a=($(echo $t | tr ','"
"
))

(注:我错过了parentheses周围添加的"替代"的命令,answerer似乎有omitted。) <P / S>。

similar理念: <P / S>。

1
2
3
string="1,2,3,4"
array=(`echo $string | sed 's/,/
/g'
`)

这些解决方案,利用Word的splitting在一个阵列分配到分离的字符串中的领域。funnily不够,就像read,一般用途的专用Word splitting也$IFS可变的,虽然在这一情况下,它也implied,它也设定到其违约value of <空格> <标签> <和> newline,therefore任何序列的一个或多个字符IFS(这是所有的字符数,现在也被视为对whitespace)在delimiter领域。 <P / S>。

这就解决了read提交的两级拆分问题,因为单词本身拆分仅构成一级拆分。但和以前一样,这里的问题是输入字符串中的各个字段可能已经包含了$IFS字符,因此在分词操作期间它们将被不正确地拆分。对于这些应答器提供的任何示例输入字符串(多么方便…)来说,情况并非如此,但这当然不会改变这样一个事实,即如果在某个时刻违反了这个假设,那么使用这个习语的任何代码基都会有崩溃的风险。再次,考虑一下我对'Los Angeles, United States, North America''Los Angeles:United States:North America'的反例。好的。

另外,分词后通常是文件名扩展(也称为路径名扩展,也称为globbing),如果这样做,可能会损坏包含字符*?[的单词,然后是](如果设置了extglob,则括号片段前面是?*+@!通过将它们与文件系统对象匹配并相应地扩展单词("globs")。这三个回答者中的第一个巧妙地通过预先运行set -f来禁用globbing来削弱这个问题。从技术上讲,这是可行的(尽管您可能应该在之后添加set +f以重新启用可能依赖它的后续代码的全局绑定),但不希望为了在本地代码中破解基本的字符串到数组的解析操作而混乱全局shell设置。好的。

此答案的另一个问题是,所有空字段都将丢失。这可能是问题,也可能不是问题,具体取决于应用程序。好的。

注意:如果您要使用这个解决方案,最好使用参数扩展的${string//:/ }模式替换形式,而不是麻烦调用命令替换(它分叉外壳)、启动管道和运行外部可执行文件(trsed,因为参数扩展是纯粹是一个shell内部操作。(另外,对于trsed解决方案,输入变量应在命令替换中进行双引号;否则,分词将在echo命令中生效,并可能与字段值混淆。此外,命令替换的$(...)形式比旧的`...`形式更可取,因为它简化了命令替换的嵌套,并允许文本编辑器更好地突出显示语法。)好的。

回答错误3好的。

1
2
str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

这个答案几乎与2相同。不同之处在于,回答者假定字段由两个字符分隔,其中一个字符在默认的$IFS中表示,另一个字符不表示。他通过使用模式替换扩展删除非IFS表示的字符,然后使用分词来拆分剩余的IFS表示的分隔符字符上的字段,解决了这个相当具体的问题。好的。

这不是一个非常通用的解决方案。此外,可以认为逗号实际上是这里的"主要"分隔符,剥离它然后根据空格字符进行字段拆分是完全错误的。再一次,考虑我的反例:'Los Angeles, United States, North America'。好的。

同样,文件名扩展可能会损坏扩展的单词,但是可以通过暂时禁用set -fset +f的分配的全局性来防止这种情况。好的。

同样,所有空字段都将丢失,这可能是问题,也可能不是问题,具体取决于应用程序。好的。

回答错误4好的。

1
2
3
4
5
6
7
8
9
10
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"

这类似于2和3,因为它使用分词来完成任务,只是现在代码显式地将$IFS设置为只包含输入字符串中存在的单个字符字段分隔符。应该重复的是,这对于多字符字段分隔符(如op的逗号空间分隔符)不起作用。但是对于像本例中使用的LF这样的单个字符分隔符,它实际上接近完美。正如我们在前面的错误答案中看到的那样,字段不能在中间无意中被拆分,并且根据需要,只有一个级别的拆分。好的。

一个问题是,文件名扩展会破坏前面描述的受影响的单词,尽管这可以通过在set -fset +f中包装critical语句来解决。好的。

另一个潜在的问题是,由于LF符合前面定义的"IFS空白字符",所有空字段都将丢失,就像在2和3中一样。如果分隔符恰好是非"ifs空白字符",这当然不是问题,而且根据应用程序的不同,它可能无论如何都不重要,但它确实会影响解决方案的通用性。好的。

因此,总而言之,假设您有一个单字符分隔符,并且它不是一个非"ifs空白字符",或者您不关心空字段,并且您将关键语句包装在set -fset +f中,那么这个解决方案是有效的,但不是有效的。好的。

(另外,为了便于参考,可以使用$'...'语法(例如IFS=$'
';
)更容易地为bash中的变量分配LF。)好的。

回答错误5好的。

1
2
3
4
countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

类似的想法:好的。

1
IFS=', ' eval 'array=($string)'

这个解决方案实际上是1(它将$IFS设置为逗号空间)和2-4(它使用分词将字符串拆分为字段)之间的交叉。正因为如此,它遭受了困扰上述所有错误答案的大多数问题,有点像世界上最糟糕的问题。好的。

另外,对于第二个变量,似乎完全不需要eval调用,因为它的参数是单引号字符串文字,因此是静态已知的。但这样使用eval实际上有一个非常不明显的好处。通常,当运行仅由变量赋值组成的简单命令时,意味着后面没有实际的命令字,赋值在shell环境中生效:好的。

1
IFS=', '; ## changes $IFS in the shell environment

即使简单命令涉及多个变量分配,也是如此;同样,只要没有命令字,所有变量分配都会影响shell环境:好的。

1
IFS=', ' array=($countries); ## changes both $IFS and $array in the shell environment

但是,如果变量赋值附加到命令名(我喜欢称之为"前缀赋值"),那么它不会影响shell环境,而是只影响已执行命令的环境,不管它是内置的还是外部的:好的。

1
2
IFS=', ' :; ## : is a builtin command, the $IFS assignment does not outlive it
IFS=', ' env; ## env is an external command, the $IFS assignment does not outlive it

bash手册中的相关引用:好的。

If no command name results, the variable assignments affect the current shell environment. Otherwise, the variables are added to the environment of the executed command and do not affect the current shell environment.

Ok.

利用变量分配的这一特性,我们可以暂时改变$IFS,这样我们就可以避免第一个变量中的$OIFS变量所带来的整个保存和恢复。但我们在这里面临的挑战是,我们需要运行的命令本身只是一个变量赋值,因此它不涉及命令字来临时执行$IFS赋值。你可能会想,为什么不在声明中添加一个no-op命令字,比如: builtin,以使$IFS赋值是临时的?这不起作用,因为这样一来,$array任务也将成为临时任务:好的。

1
IFS=', ' array=($countries) :; ## fails; new $array value never escapes the : command

所以,我们实际上陷入了僵局,有点像第22条军规。但是,当eval运行它的代码时,它在shell环境中运行,就像它是正常的静态源代码一样,因此我们可以在eval参数内部运行$array赋值,使它在shell环境中生效,而在eval命令前面的$IFS前缀赋值将不会比eval命令还长。这正是该解决方案的第二个变体所使用的技巧:好的。

1
IFS=', ' eval 'array=($string)'; ## $IFS does not outlive the eval command, but $array does

所以,正如你所看到的,这实际上是一个相当聪明的技巧,并且以一种相当不明显的方式完成所需的工作(至少在任务完成方面)。实际上,我并不反对这种伎俩,尽管eval参与其中;只是要小心地单引用参数字符串以防范安全威胁。好的。

但同样,由于"世界上最糟糕的"问题的聚集,这仍然是对OP要求的错误回答。好的。

回答错误6好的。

1
2
3
IFS=', '; array=(Paris, France, Europe)

IFS=' ';declare -a array=(Paris France Europe)

嗯。。。什么?OP有一个字符串变量需要解析为数组。这个"答案"以粘贴到数组文字中的输入字符串的逐字内容开始。我想这是一种方法。好的。

看起来,应答者可能假设$IFS变量影响所有上下文中的所有bash解析,但这不是真的。从bash手册:好的。

IFS    The Internal Field Separator that is used for word splitting after expansion and to split lines into words with the read builtin command. The default value is .

Ok.

因此,$IFS特殊变量实际上只在两个上下文中使用:(1)扩展后执行的分词(意思是解析bash源代码时不执行)和(2)由read内置的将输入行拆分为单词。好的。

让我试着让这更清楚些。我认为在解析和执行之间划出一个界限可能会比较好。bash必须首先解析源代码,这显然是一个解析事件,然后执行代码,这就是扩展进入图片的时候。扩展实际上是一个执行事件。此外,我对我刚才引用的$IFS变量的描述有异议;我认为分词是在扩展过程中执行的,而不是在扩展后执行的,或者更准确地说,分词是扩展过程的一部分。短语"分词"只指扩展的这一步;它不应该用于bash源代码的解析,尽管不幸的是,文档似乎经常围绕"分词"和"词"这两个词。以下是bash手册linux.die.net版本的相关摘录:好的。

将命令行拆分为单词后,在命令行上执行扩展。执行了七种扩展:大括号扩展、颚化符扩展、参数和变量扩展、命令替换、算术扩展、分词和路径名扩展。好的。

扩展的顺序是:大括号扩展;tilde扩展、参数和变量扩展、算术扩展和命令替换(从左到右进行);分词;路径名扩展。好的。

您可以争辩说,GNU版本的手册做得稍微好一些,因为它在扩展部分的第一句话中选择了单词"tokens"而不是"words":好的。

Expansion is performed on the command line after it has been split into tokens.

Ok.

重要的是,$IFS不会改变bash解析源代码的方式。bash源代码的解析实际上是一个非常复杂的过程,涉及到对shell语法的各种元素的识别,例如命令序列、命令列表、管道、参数扩展、算术替换和命令替换。在大多数情况下,bash解析过程不能被用户级操作(如变量分配)改变(实际上,这条规则有一些小的例外;例如,查看各种compatxxshell设置,这些设置可以动态地改变解析行为的某些方面)。然后,根据上述文档摘录中分解的"扩展"一般过程,扩展由此复杂解析过程产生的上游"单词"/"令牌",其中扩展(扩展?)的分词。将文本转换为下游单词只是这个过程的一个步骤。分词只涉及前面扩展步骤中吐出的文本;它不影响直接从源字节流分析的文本。好的。

回答错误7好的。

1
2
3
4
5
string='first line
        second line
        third line'


while read -r line; do lines+=("$line"); done <<<"$string"

这是最好的解决方案之一。注意,我们又开始使用read。我刚才不是说read不合适,因为它执行两个级别的拆分,而我们只需要一个级别?这里的诀窍是,您可以以这样的方式调用read,它实际上只执行一个级别的拆分,特别是每次调用只拆分一个字段,这就需要在一个循环中重复调用它的成本。这有点花招,但很管用。好的。

但也有问题。第一:当您向read提供至少一个name参数时,它会自动忽略从输入字符串中分离出来的每个字段中的前导和尾随空格。无论$IFS是否设置为默认值,都会发生这种情况,如本文前面所述。现在,对于特定的用例,操作人员可能不关心这个问题,事实上,它可能是解析行为的理想特性。但不是所有想将字符串解析为字段的人都想这样做。然而,有一个解决方案:read的一个不明显的用法是传递零名称参数。在这种情况下,read将把它从输入流中获得的整个输入行存储在名为$REPLY的变量中,并且作为一个额外的好处,它不会从值中去掉前导和尾随空格。这是我在shell编程生涯中经常使用的read的一个非常健壮的用法。下面是对行为差异的演示:好的。

1
2
3
4
5
6
7
8
9
string=$'  a  b  
  c  d  
  e  f  '
; ## input string

a=(); while read -r line; do a+=("$line"); done <<<"$string"; declare -p a;
## declare -a a=([0]="a  b" [1]="c  d" [2]="e  f") ## read trimmed surrounding whitespace

a=(); while read -r; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="  a  b " [1]="  c  d " [2]="  e  f ") ## no trimming

这个解决方案的第二个问题是,它实际上并没有处理自定义字段分隔符的情况,例如op的逗号空间。和以前一样,不支持多字符分隔符,这是该解决方案的一个不幸限制。我们可以通过指定-d选项的分隔符来尝试至少在逗号上拆分,但要看会发生什么:好的。

1
2
3
string='Paris, France, Europe';
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France")

可以预见,未计算的周围空白被拉入字段值中,因此必须随后通过剪裁操作更正(这也可以直接在while循环中完成)。但还有一个明显的错误:欧洲不见了!怎么了?答案是,如果read到达文件结尾(在本例中,我们可以称之为字符串结尾),而不在最后一个字段上遇到最后一个字段终止符,则返回失败的返回代码。这会导致while循环过早中断,从而丢失最终字段。好的。

从技术上讲,同样的错误也影响了前面的例子;不同之处在于,字段分隔符被认为是lf,这是不指定-d选项时的默认值,并且<<<机制自动将lf附加到字符串ju在它将它作为命令的输入提供之前。因此,在这些情况下,我们无意中在输入端附加了一个虚拟终止符,从而解决了最终字段丢失的问题。让我们把这个解决方案称为"虚拟终结器"解决方案。我们可以手动为任何自定义分隔符应用虚拟终止符解决方案,方法是在将其实例化为此处字符串时将其与输入字符串连接起来:好的。

1
2
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -p a;
declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

在那里,问题解决了。另一种解决方案是,只有当(1)read返回失败,(2)$REPLY为空时,才能中断while循环,这意味着read在到达文件结尾之前无法读取任何字符。演示:好的。

1
2
3
a=(); while read -rd,|| [[ -n"$REPLY" ]]; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe
')

这种方法还揭示了由<<<重定向操作符自动附加到here字符串的秘密lf。当然,可以通过前面描述的显式剪裁操作将其单独剥离,但显然,手动虚拟终结器方法直接解决了这一问题,所以我们可以继续。手动虚拟终止器解决方案实际上非常方便,因为它一次性解决了这两个问题(丢弃的最终字段问题和附加的LF问题)。好的。

所以,总的来说,这是一个非常强大的解决方案。唯一剩下的弱点是缺乏对多字符定界符的支持,稍后我将对此进行讨论。好的。

错误答案8好的。

1
2
3
4
5
string='first line
        second line
        third line'


readarray -t lines <<<"$string"

(事实上,这与7来自同一个帖子;回答者在同一帖子中提供了两个解决方案。)好的。

作为mapfile的同义词,readarray内置是理想的。它是一个内置命令,一次将字节流解析为数组变量;不与循环、条件、替换或其他任何操作混淆。它不会秘密地从输入字符串中删除任何空白。并且(如果没有给定-O)它在分配给目标数组之前方便地清除目标数组。但它仍然不完美,因此我批评它是一个"错误的答案"。好的。

首先,为了避免这种情况的发生,请注意,正如在进行字段解析时read的行为一样,readarray会在尾随字段为空时丢弃尾随字段。同样,这可能不是操作的问题,但也可能是一些用例的问题。我马上就回来。好的。

第二,和以前一样,它不支持多字符分隔符。我也会马上解决这个问题。好的。

第三,编写的解决方案不解析OP的输入字符串,事实上,它不能像解析那样被使用。我也会在这方面做些扩展。好的。

基于以上原因,我仍然认为这是对OP问题的"错误答案"。下面我将给出我认为正确的答案。好的。

正确答案好的。

这是NA?我试图通过指定-d选项来实现工作:好的。

1
2
3
4
string='Paris, France, Europe';
readarray -td, a <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe
')

我们看到结果与我们从循环read解决方案的双条件方法得到的结果相同,如7所述。我们几乎可以用手工的虚拟终结者技巧来解决这个问题:好的。

1
2
3
readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'
')

这里的问题是readarray保留了尾随字段,因为<<<重定向运算符将lf附加到输入字符串,因此尾随字段不是空的(否则它会被删除)。我们可以通过在以下事实之后显式地取消设置最终数组元素来解决这一问题:好的。

1
2
readarray -td, a <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

剩下的两个实际相关的问题是:(1)需要修剪的外来空白;(2)缺乏对多字符分隔符的支持。好的。

当然,空白可以在后面进行修剪(例如,参见如何从bash变量中修剪空白?)但是如果我们能破解一个多字符分隔符,那么这将一次性解决这两个问题。好的。

不幸的是,无法直接使用多字符分隔符。我想到的最佳解决方案是对输入字符串进行预处理,将多字符分隔符替换为单字符分隔符,这样可以保证不会与输入字符串的内容冲突。唯一具有此保证的字符是nul字节。这是因为,在bash中(顺便说一下,不是在zsh中),变量不能包含nul字节。这个预处理步骤可以在进程替换中以内联方式完成。下面介绍如何使用awk:好的。

1
2
3
readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; }' <<<"$string,"); unset 'a[-1]';
declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

终于到了!此解决方案不会在中间错误地拆分字段,不会过早地剪切,不会删除空字段,不会损坏文件名扩展名本身,不会自动删除前导和尾随的空格,不会在结尾处留下一个收起LF,不需要循环,也不会固定为单个字符分隔符。好的。

切边液好的。

最后,我想用readarray的模糊的-C callback选项演示我自己相当复杂的修剪解决方案。不幸的是,我已经没有足够的空间来应对堆栈溢出的30000个字符的严格限制,所以我无法解释它。我把它留给读者作为练习。好的。

1
2
3
4
5
6
function mfcb { local val="$4";"$1"; eval"$2[$3]=\$val;"; };
function val_ltrim { if [["$val" =~ ^[[:space:]]+ ]]; then val="${val:${#BASH_REMATCH[0]}}"; fi; };
function val_rtrim { if [["$val" =~ [[:space:]]+$ ]]; then val="${val:0:${#val}-${#BASH_REMATCH[0]}}"; fi; };
function val_trim { val_ltrim; val_rtrim; };
readarray -c1 -C 'mfcb val_trim a' -td, <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

号好啊。


以下是不设置IFS的方法:

1
2
3
4
5
6
7
string="1:2:3:4:5"
set -f                      # avoid globbing (expansion of *).
array=(${string//:/ })
for i in"${!array[@]}"
do
    echo"$i=>${array[i]}"
done

想法是使用字符串替换:

1
${string//substring/replacement}

要用空白替换$substring的所有匹配项,然后使用替换字符串初始化数组:

1
(element1 element2 ... elementN)

注意:这个答案使用了split+glob操作符。因此,为了防止某些字符(如*)的扩展,最好暂停此脚本的全局切换。


1
2
3
4
t="one,two,three"
a=($(echo"$t" | tr ',' '
'
))
echo"${a[2]}"

打印三张


有时我突然想到,在接受的答案中描述的方法不起作用,特别是如果分隔符是回车符。在这些情况下,我是这样解决的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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


接受的答案适用于一行中的值。如果变量有多行:

1
2
3
string='first line
        second line
        third line'

我们需要一个非常不同的命令来获取所有行:

埃多克斯1〔2〕

或者更简单的bash readarray:

1
readarray -t lines <<<"$string"

打印所有行非常容易利用打印功能:

1
2
3
4
5
6
printf">[%s]
"
"${lines[@]}"

>[first line]
>[        second line]
>[        third line]


将字符串拆分为数组的关键是","的多字符分隔符。对于多字符分隔符使用IFS的任何解决方案都是固有错误的,因为ifs是一组这些字符,而不是字符串。

如果指定IFS=",",则字符串将在","""或它们的任何组合上中断,这不是","的两个字符分隔符的准确表示。

您可以使用awksed分割字符串,过程替换为:

1
2
3
4
5
6
7
8
9
#!/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中使用regex更有效:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/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...

对于第二种形式,没有子shell,而且它本身就更快。

bgoldst编辑:这里有一些比较我的readarray解决方案与dawg的regex解决方案的基准,我还包括了read解决方案,以供检查(注:我稍微修改了regex解决方案,以使其与我的解决方案更协调)(另请参见我在帖子下面的评论):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
## 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 '
'
'/';
        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
##


这类似于JMoney38的方法,但使用的是SED:

1
2
3
4
string="1,2,3,4"
array=(`echo $string | sed 's/,/
/g'
`)
echo ${array[0]}

印刷品1


纯bash多字符分隔符解决方案。

正如其他人在这个线程中指出的那样,op的问题给出了一个逗号分隔字符串的例子,将其解析为数组,但没有指出他/她是否只对逗号分隔符、单字符分隔符或多字符分隔符感兴趣。

由于谷歌倾向于将这个答案排在搜索结果的顶部或附近,所以我想为读者提供一个关于多个字符分隔符问题的强有力的答案,因为至少有一个回答中也提到了这个问题。

如果你在寻找一个多字符定界符问题的解决方案,我建议你回顾一下Mallikarjun M的文章,特别是gniourf的回复。谁使用参数扩展提供了这个优雅的纯bash解决方案:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
str="LearnABCtoABCSplitABCaABCString"
delimiter=ABC
s=$str$delimiter
array=();
while [[ $s ]]; do
    array+=("${s%%"$delimiter"*}" );
    s=${s#*"$delimiter"};
done;
declare -p array

引用评论/参考文章链接

链接到引用的问题:如何在bash中拆分多字符分隔符上的字符串?


我在分析输入时遇到了这篇文章,比如:单词1,单词2,…

以上都没有帮助我。用锥子解决了这个问题。如果它能帮助某人:

1
2
3
4
5
6
7
STRING="value1,value2,value3"
array=`echo $STRING | awk -F ',' '{ s = $1; for (i = 2; i <= NF; i++) s = s"
"$i; print s; }'
`
for word in ${array}
do
        echo"This is the word $word"
done


试试这个

1
2
IFS=', '; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done

很简单。如果需要,还可以添加声明(也可以删除逗号):

1
IFS=' ';declare -a array=(Paris France Europe)

添加ifs是为了撤消上面的操作,但是在新的bash实例中没有它就可以工作。


更新:不要这样做,因为Eval有问题。

仪式稍微少一点:

1
IFS=', ' eval 'array=($string)'

例如

1
2
3
string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar


使用此:

1
2
3
4
5
6
7
8
countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe


这对我在OSX上有效:

1
2
string="1 2 3 4 5"
declare -a array=($string)

如果字符串具有不同的分隔符,只需首先用空格替换这些分隔符:

1
2
3
string="1,2,3,4,5"
delimiter=","
declare -a array=($(echo $string | tr"$delimiter"""))

简单:—)


这是我的黑客!

用字符串分割字符串是使用bash非常无聊的事情。会发生的是,我们的方法有限,只能在少数情况下工作(按";","/","等等分割),或者我们在输出中有各种副作用。

下面的方法需要一些操作,但我相信它可以满足我们的大多数需求!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#!/bin/bash

# --------------------------------------
# SPLIT FUNCTION
# ----------------

F_SPLIT_R=()
f_split() {
    : 'It does a"split" into a given string and returns an array.

    Args:
        TARGET_P (str): Target string to"split".
        DELIMITER_P (Optional[str]): Delimiter used to"split". If not
    informed the split will be done by spaces.

    Returns:
        F_SPLIT_R (array): Array with the provided string separated by the
    informed delimiter.
    '


    F_SPLIT_R=()
    TARGET_P=$1
    DELIMITER_P=$2
    if [ -z"$DELIMITER_P" ] ; then
        DELIMITER_P=""
    fi

    REMOVE_N=1
    if ["$DELIMITER_P" =="
"
] ; then
        REMOVE_N=0
    fi

    # NOTE: This was the only parameter that has been a problem so far!
    # By Questor
    # [Ref.: https://unix.stackexchange.com/a/390732/61742]
    if ["$DELIMITER_P" =="./" ] ; then
        DELIMITER_P="[.]/"
    fi

    if [ ${REMOVE_N} -eq 1 ] ; then

        # NOTE: Due to bash limitations we have some problems getting the
        # output of a split by awk inside an array and so we need to use
        #"line break" (
) to succeed. Seen this, we remove the line breaks
        # momentarily afterwards we reintegrate them. The problem is that if
        # there is a line break in the"string" informed, this line break will
        # be lost, that is, it is erroneously removed in the output!
        # By Questor
        TARGET_P=$(awk 'BEGIN {RS="dn"} {gsub("
","3F2C417D448C46918289218B7337FCAF"); printf $0}'
<<<"${TARGET_P}")

    fi

    # NOTE: The replace of"
" by"3F2C417D448C46918289218B7337FCAF" results
    # in more occurrences of"
3F2C417D448C46918289218B7337FCAF" than the
    # amount of"

" that there was originally in the string (one more
    # occurrence at the end of the string)! We can not explain the reason for
    # this side effect. The line below corrects this problem! By Questor
    TARGET_P=${TARGET_P%????????????????????????????????}

    SPLIT_NOW=$(awk -F"$DELIMITER_P" '{for(i=1; i<=NF; i++){printf"
%s
", $i}}' <<<"${TARGET_P}")

    while IFS= read -r LINE_NOW ; do
        if [ ${REMOVE_N} -eq 1 ] ; then

            # NOTE: We use"
'" to prevent blank lines with no other characters
            # in the sequence being erroneously removed! We do not know the
            # reason for this side effect! By Questor
            LN_NOW_WITH_N=$(awk '
BEGIN {RS="dn"} {gsub("3F2C417D448C46918289218B7337FCAF","
"
); printf $0}' <<<"'${LINE_NOW}'")

            # NOTE: We use the commands below to revert the intervention made
            # immediately above! By Questor
            LN_NOW_WITH_N=${LN_NOW_WITH_N%?}
            LN_NOW_WITH_N=${LN_NOW_WITH_N#?}

            F_SPLIT_R+=("$LN_NOW_WITH_N")
        else
            F_SPLIT_R+=("$LINE_NOW")
        fi
    done <<<"$SPLIT_NOW"
}

# --------------------------------------
# HOW TO USE
# ----------------

STRING_TO_SPLIT="
 * How do I list all databases and tables using psql?

"
sudo -u postgres /usr/pgsql-9.4/bin/psql -c "\l"
sudo -u postgres /usr/pgsql-9.4/bin/psql <DB_NAME> -c "\dt"
"

"
\list or \l: list all databases
\dt: list all tables in the current database
"

[Ref.: https://dba.stackexchange.com/questions/1285/how-do-i-list-all-databases-and-tables-using-psql]


"

f_split"$STRING_TO_SPLIT""bin/psql -c"

# --------------------------------------
# OUTPUT AND TEST
# ----------------

ARR_LENGTH=${#F_SPLIT_R[*]}
for (( i=0; i<=$(( $ARR_LENGTH -1 )); i++ )) ; do
    echo"> -----------------------------------------"
    echo"${F_SPLIT_R[$i]}"
    echo" < -----------------------------------------"
done

if ["$STRING_TO_SPLIT" =="${F_SPLIT_R[0]}bin/psql -c${F_SPLIT_R[1]}" ] ; then
    echo"> -----------------------------------------"
    echo"The strings are the same!"
    echo" < -----------------------------------------"
fi

另一种不修改IFS的方法是:

1
read -r -a myarray <<<"${string//, /$IFS}"

我们可以通过"${string//, /$IFS}"将所需分隔符","的所有内容替换为$IFS的内容,而不是更改ifs以匹配所需分隔符。

但对于非常大的弦来说,这可能会很慢?

这是基于丹尼斯·威廉姆森的回答。


另一种方法是:

1
2
string="Paris, France, Europe"
IFS=', ' arr=(${string})

现在元素存储在"arr"数组中。要遍历元素,请执行以下操作:

1
for i in ${arr[@]}; do echo $i; done


另一种方法是:

1
2
str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

在这个"arr"之后是一个包含四个字符串的数组。这不需要处理ifs或read或任何其他特殊的东西,因此更简单和直接。