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


640

在Bash脚本中,我想将一行分割成几部分,并将它们存储在数组中。

该行:

Paris, France, Europe

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

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

我想使用简单的代码,命令的速度无关紧要。我该怎么做?


22
这是Google排名第一的搜索引擎,但答案中存在争议,因为不幸的是,该问题询问有关, (逗号)而不是逗号等单个字符的定界问题。如果您仅对后者感兴趣,可以在这里轻松找到答案:stackoverflow.com/questions/918886/…–
antak

如果您想删减一个字符串并且不关心将它作为数组,cut那么也要记住一个有用的bash命令。分隔符是可定义的en.wikibooks.org/wiki/Cut您也可以从固定宽度的记录结构中提取数据。en.wikipedia.org/wiki/Cut_(Unix) computerhope.com/unix/ucut.htm
JGFMK

Answers:


1088
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}"

较大的负偏移量选择距数组末端较远的位置。请注意在较早的形式中减号之前的空格。它是必需的。


15
只需使用IFS=', ',就不必分别删除空格。测试:IFS=', ' read -a array <<< "Paris, France, Europe"; echo "${array[@]}"
l0b0 2012年

4
@ l0b0:谢谢。我不知道我在想什么。declare -p array顺便说一下,我喜欢用于测试输出。
暂停,直到另行通知。

1
这似乎不尊重报价。例如,France, Europe, "Congo, The Democratic Republic of the"这将在刚果之后分裂。
Yisrael Dov 2014年

2
@YisraelDov:Bash无法单独处理CSV。它无法区分引号内的逗号和引号外的逗号之间的区别。您将需要使用可理解CSV的工具,例如高级语言的lib,例如Python中的csv模块。
暂停,直到另行通知。

5
str="Paris, France, Europe, Los Angeles"; IFS=', ' read -r -a array <<< "$str"将拆分array=([0]="Paris" [1]="France" [2]="Europe" [3]="Los" [4]="Angeles")为笔记。因此,这仅适用于没有空格的字段,因为它IFS=', '是一组单独的字符,而不是字符串定界符。
dawg

332

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


错误的答案#1

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

1:这是对的滥用$IFS。所述的值$IFS变量作为一个单可变长度字符串分隔符,而它被作为一组单字符串分离器,其中,每个字段read从输入线分裂出可通过终止任何字符集合中的(在此示例中为逗号空格)。

实际上,对于那些真正的粘手来说, $IFS要稍微复杂一些。从bash手册

外壳将IFS的每个字符视为定界符,并使用这些字符作为字段终止符将其他扩展的结果拆分为单词。如果未设置IFS,或者其值恰好是默认值<space> <tab> <newline>,则在先前扩展结果的开头和结尾处分别是<space><tab><newline>的序列会被忽略,并且任何不在开头或结尾的IFS字符序列都用于分隔单词。如果IFS的值不是默认值,则空格字符序列 <space><tab><只要空格字符在IFS值(IFS空格字符)中,该单词的开头和结尾都会被忽略。在任何字符IFS不是IFS的空白,与任何相邻的沿IFS空白字符,限定一个字段。IFS空格字符序列也被视为定界符。如果IFS的值为null,则不会发生单词拆分。

基本上,对于的非默认非null值$IFS,可以使用(1)一个或多个字符序列来分隔字段,这些字符序列均来自“ IFS空白字符”集(即<space>中的任何一个,<tab><newline>(“ newline”表示换行(LF))出现在的任何位置$IFS),或(2)出现在其中的任何非“ IFS空格字符” $IFS以及它周围的所有“ IFS空格字符”在输入行中。

对于OP,我在上一段中描述的第二种分隔模式很可能正是他为他的输入字符串所需要的,但是我们可以确信,我描述的第一种分隔模式根本不正确。例如,如果他的输入字符串是'Los Angeles, United States, North America'

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:即使您将此解决方案与单字符分隔符一起使用(例如,逗号本身,也就是没有跟随空格或其他包)),如果$string变量的值恰好包含任何LF,read则将一旦遇到第一个LF,就停止处理。该read内建只处理每次调用一行。即使你是管道或重定向输入这是真实的read说法,因为我们在这个例子中与正在做的下面的字符串机制,因此未处理的输入是保证丢失。read内置驱动程序的代码不了解其包含的命令结构中的数据流。

您可能会争辩说,这不太可能引起问题,但是,如果可能的话,应该避免这种隐患。这是由于以下事实造成的:read内置实际上执行了两个级别的输入拆分:首先拆分为行,然后拆分为字段。由于OP只需要一个拆分级别,因此对read内置函数的这种使用是不合适的,我们应该避免使用它。

3:此解决方案的一个显而易见的潜在问题是,read如果尾随字段为空,则始终删除尾随字段,尽管否则保留尾随字段。这是一个演示:

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对此并不在乎,但这仍然是一个值得了解的限制。它降低了解决方案的健壮性和通用性。

可以通过在输入字符串之前将伪尾随定界符附加到输入字符串来解决此问题read,如我稍后将演示的那样。


错误的答案#2

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

类似的想法:

t="one,two,three"
a=($(echo $t | tr ',' "\n"))

(注意:我在回答者似乎已省略的命令替换周围添加了缺少的括号。)

类似的想法:

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

这些解决方案利用数组分配中的单词拆分功能将字符串拆分为多个字段。有趣的是,就像read常规单词拆分一样,它也使用$IFS特殊变量,尽管在这种情况下,它暗示了将其设置为其默认值<space> <tab> <newline>,因此可以将一个或多个IFS的任何序列字符(现在都是空白字符)被视为字段定界符。

这解决了由提交的两个级别的拆分问题read,因为单词拆分本身仅构成一个拆分级别。但是,就像以前一样,这里的问题在于输入字符串中的各个字段已经可以包含$IFS字符,因此在单词拆分操作中会不正确地拆分它们。这些应答程序提供的任何示例输入字符串都不是这种情况(多么方便...),但是当然,这不会改变以下事实,即任何使用此惯用语的代码库都会冒以下风险:如果这个假设在某个时候被违反,就会爆炸。再次考虑我'Los Angeles, United States, North America'(或'Los Angeles:United States:North America')的反例。

此外,词的拆分通常接着文件名扩展又名路径扩展又名通配符),其中,如果进行,将包含字符可能会损坏的话*?[随后](如果extglob被设置,括号片段之前通过?*+@,或!),将它们与文件系统对象进行匹配,并相应地扩展单词(“ glob”)。这三个应答器中的第一个通过set -f预先运行以禁用通配符来巧妙地解决了此问题。从技术上讲这是可行的(尽管您可能应该添加set +f 之后再重新启用可能依赖于它的后续代码的glob),但是为了在本地代码中破解基本的字符串到数组的解析操作而不得不破坏全局shell设置是不可取的。

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

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


错误的答案#3

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

这个答案与#2几乎相同。不同之处在于,应答者已假设字段由两个字符分隔,其中一个以default表示$IFS,而另一个则不是。他通过使用模式替换扩展来删除非IFS表示的字符,然后使用单词拆分在剩余的IFS表示的分隔符上拆分字段,从而解决了这种相当特殊的情况。

这不是一个非常通用的解决方案。此外,可以说逗号实际上是此处的“主要”定界符,而将其剥离然后依赖于空格符进行字段拆分是完全错误的。再次考虑我的反例:'Los Angeles, United States, North America'

此外,再次,文件名扩展将破坏扩张的话,但是这可以通过暂时禁用通配符与工作分配防止set -fset +f

同样,所有空白字段都将丢失,根据应用程序的不同,这可能是问题,也可能不是问题。


错误的答案#4

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 -f和中来解决set +f

另一个潜在的问题是,由于LF符合前面定义的“ IFS空格字符”,所有空白字段都将丢失,就像#2#3一样。如果定界符碰巧是非“ IFS空格字符”,那么这当然不会成为问题,并且视应用而定,这可能无关紧要,但这确实削弱了解决方案的通用性。

因此,总而言之,假设您使用一个字符分隔符,并且它是非“ IFS空格字符”,或者您不关心空字段,并且将关键语句包装在set -f和中set +f,则此解决方案有效,但除此之外没有。

(此外,为了提供信息,使用bash等$'...'语法可以更轻松地将LF分配给bash中的变量IFS=$'\n';。)


错误的答案#5

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

类似的想法:

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

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

同样,关于第二个变体,eval由于它的参数是单引号的字符串文字,因此似乎完全不需要调用,因此它是静态已知的。但是,eval以这种方式使用实际上有一个非常明显的好处。通常,当您运行一个包含变量赋值的简单命令时,意味着没有紧随其后的实际命令字,该赋值将在shell环境中生效:

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

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

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

但是,如果变量赋值连接到命令名(我喜欢称之为“前缀分配”),那么它并不会影响shell环境,而是仅影响执行的命令的环境中,无论它是一个内置或外部:

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手册中的相关报价:

如果没有命令名称,则变量分配会影响当前的shell环境。否则,变量将添加到已执行命令的环境中,并且不会影响当前的shell环境。

可以利用变量分配的此功能$IFS仅进行临时更改,这使我们避免了像$OIFS第一个变量中的变量那样执行整个保存和恢复操作。但是我们在这里面临的挑战是,我们需要运行的命令本身仅仅是一个变量分配,因此它不会涉及使$IFS赋值临时化的命令字。您可能会想自己,为什么不只在语句之类的语句中添加一个无操作命令字: builtin以使$IFS分配成为临时任务呢?这是行不通的,因为这样也会使$array分配成为临时分配:

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

因此,我们实际上处于僵局,只有22个陷阱。但是,当eval运行其代码时,它将在shell环境中运行,就像正常的静态源代码一样,因此,我们可以$arrayeval参数内部运行赋值以使其在shell环境中生效,而$IFS前缀赋值则可以该eval命令的前缀不会使该命令失效eval。这正是此解决方案的第二个变体中使用的技巧:

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

因此,正如您所看到的,这实际上是一个巧妙的技巧,它以一种相当不明显的方式准确地完成了要求的工作(至少在赋值实现方面)。尽管有eval; 的参与,但实际上我总体上并不反对这种技巧。只需小心将引号字符串单引号以防止出现安全威胁。

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


错误的答案#6

IFS=', '; array=(Paris, France, Europe)

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

嗯什么?OP具有一个字符串变量,需要将其解析为数组。该“答案”以粘贴到数组文字中的输入字符串的逐字内容开头。我想那是做到这一点的一种方法。

看来应答者可能已经假定该$IFS变量会影响所有上下文中的所有bash解析,但事实并非如此。从bash手册中:

IFS     内部字段分隔符,用于在扩展后进行单词拆分,并使用read Builtin命令将行拆分为单词。默认值为<space> <tab> <newline>

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

让我试着更清楚一点。我认为最好在解析执行之间进行区分。Bash必须首先解析源代码,这显然是一个解析事件,然后再执行代码,这就是在图片扩展时。扩展实际上是一个执行事件。此外,我对$IFS上面刚刚引用的变量的描述持怀疑态度。与其说在扩展之后执行词拆分,不如说在扩展过程中执行词拆分,或者甚至更准确地说,词拆分是扩展的一部分扩展过程。短语“分词”仅指此扩展步骤;它不应该被用来引用bash源代码的解析,尽管不幸的是文档似乎确实把“ split”和“ words”这两个词混为一谈。这是bash手册的linux.die.net版本的相关摘录:

拆分成单词后,在命令行上执行扩展。执行了七种扩展:大括号扩展代字号扩展参数和变量扩展命令替换算术扩展单词拆分路径名扩展

扩展顺序为:大括号扩展;波浪线扩展,参数和变量扩展,算术扩展和命令替换(以从左到右的方式完成);分词 和路径名扩展。

您可能会认为GNU版本的手册做得更好,因为它在“扩展”部分的第一句中选择了“令牌”一词,而不是“单词”:

扩展已拆分为令牌后,在命令行上执行。

重要的是,$IFS不改变bash解析源代码的方式。bash源代码的解析实际上是一个非常复杂的过程,涉及识别外壳语法的各种元素,例如命令序列,命令列表,管道,参数扩展,算术替换和命令替换。在大多数情况下,bash解析过程无法通过用户级操作(例如变量分配)来更改(实际上,此规则有一些小例外;例如,请参见各种compatxxshell设置),这可以即时更改解析行为的某些方面。然后,根据上述文档摘录中分解的一般“扩展”过程,将由复杂的解析过程产生的上游“单词” /“令牌”进行扩展,其中将扩展(扩展?)文本的单词拆分为下游单词只是该过程的一个步骤。分词仅涉及上一个扩展步骤中吐出的文本;它不会影响立即从源字节流解析的文本文本。


错误的答案#7

string='first line
        second line
        third line'

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

这是最好的解决方案之一。请注意,我们回到使用read。我刚才不是说read不合适吗,因为当我们只需要一个级别时,它执行两个级别的拆分?这里的窍门是,您可以read这样一种方式进行调用,即它只能有效地执行一个级别的拆分,特别是通过每次调用仅拆分一个字段,这就需要必须在循环中重复调用它。有点麻烦,但是可以用。

但是有问题。第一:向提供至少一个NAME参数时read,它会自动忽略从输入字符串中分离出的每个字段中的前导和尾随空格。$IFS如本文前面所述,无论是否将其设置为默认值,都会发生这种情况。现在,OP可能不在乎其特定用例,实际上,它可能是解析行为的理想功能。但是,并非所有人都希望将字符串解析为字段。但是,有一个解决方案:的一种不太明显的用法read是传递零个NAME参数。在这种情况下,read会将从输入流中获得的整个输入行存储在名为的变量中$REPLY,作为奖励,它不会从值中去除前导和尾随空格。这是一种非常强大的用法,read在我的Shell编程生涯中经常使用它。这是行为差异的演示:

string=$'  a  b  \n  c  d  \n  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选项指定分隔符来尝试至少用逗号分割,但是看看会发生什么:

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如果命中文件末尾(在这种情况下,我们可以称其为字符串末尾)而未在final字段上遇到final字段终止符,则返回失败的返回码。这导致while循环过早中断,我们失去了最后一个字段。

从技术上讲,同样的错误也困扰着前面的例子。区别在于字段分隔符被视为LF,这是您未指定-d选项时的默认值,并且<<<(“ here-string”)机制会在将字符串作为参数输入之前自动将LF附加到字符串输入命令。因此,在那些情况下,我们通过不经意地将附加的虚拟终结器附加到输入中,无意中解决了最终字段丢失的问题。我们将此解决方案称为“虚拟终结者”解决方案。我们可以通过在here字符串中实例化伪终止符解决方案并将其自己与输入字符串连接起来,从而对任何自定义分隔符手动应用伪终止符解决方案:

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在命中文件结尾之前无法读取任何字符。演示:

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

这种方法还揭示了秘密LF,它由<<<重定向运算符自动附加到here-string 。当然,可以通过前面所述的显式修整操作将其单独剥离,但是显然,手动虚拟终止符方法可以直接解决该问题,因此我们可以继续进行下去。手动虚拟终结器解决方案实际上非常方便,因为它可以一次性解决这两个问题(掉落的最终场问题和附加的LF问题)。

因此,总的来说,这是一个功能强大的解决方案。唯一的弱点是缺乏对多字符定界符的支持,我将在后面解决。


错误的答案#8

string='first line
        second line
        third line'

readarray -t lines <<<"$string"

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

readarray内置的,这是一个代名词mapfile,是理想的。这是一个内置命令,可以一次将字节流解析为数组变量。不会弄乱循环,条件,替换或其他任何东西。并且它不会从输入字符串中秘密删除任何空格。并且(如果-O未给出)可以方便地在分配给目标数组之前清除目标数组。但是它仍然不完美,因此我批评它为“错误答案”。

首先,只是为了避免这种情况,请注意,就像read进行字段解析时的行为一样,readarray如果尾随字段为空,则将其删除。同样,这可能不是OP所关心的问题,但可能是某些用例所致。我待会儿再讲这个。

其次,和以前一样,它不支持多字符定界符。我也会对此进行修复。

第三,编写的解决方案不能解析OP的输入字符串,实际上,不能按原样使用它来解析它。我也会暂时对此进行扩展。

由于上述原因,我仍然认为这是对OP问题的“错误答案”。下面我将给出我认为是正确的答案。


正确答案

仅通过指定选项,就可以天真的尝试使#8起作用-d

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

我们看到结果与#7中read讨论的循环解决方案的双条件方法得到的结果相同。我们几乎可以使用手动虚拟终止符来解决此问题:

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

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

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的方法

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

终于到了!此解决方案不会在中间错误地分割字段,不会过早地删除字段,不会删除空字段,不会在文件名扩展中破坏自身,不会自动剥离开头和结尾的空格,不会在末端留下偷偷摸摸的LF,不需要循环,也不需要单字符定界符。


修整解决方案

最后,我想使用的晦涩难懂的-C callback选项来演示我自己相当复杂的修剪解决方案readarray。不幸的是,我已经超出了Stack Overflow严格的30,000个字符的发布限制,因此我无法解释。我将其留给读者练习。

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")

8
这也可能是有帮助的说明(虽然可以理解,你有没有空间的话)的-d选项,readarray在Bash中4.4第一次出现。
fbicknel

2
好答案(+1)。如果您将awk更改为awk '{ gsub(/,[ ]+|$/,"\0"); print }'并消除了决赛的串联,", " 那么您就不必经过体操来消除决赛的记录了。因此:readarray -td '' a < <(awk '{ gsub(/,[ ]+/,"\0"); print; }' <<<"$string")在支持的Bash上readarray。注意:你的方法是猛砸4.4+我认为,因为的-dreadarray

3
@datUser不幸的是。您的bash版本必须太旧了readarray。在这种情况下,您可以使用构建于的第二好的解决方案read。我指的是:(如果需要多字符定界符支持a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,";,则awk用替换表示)。如果您遇到任何问题,请告诉我;我非常确定,该解决方案应该可以在相当早的bash版本(回到20年前发布的2版本)上使用。
bgoldst

1
哇,真是个绝妙的答案!嘻嘻,我的回应:抛弃bash脚本并启动python!
artfulrobot

1
OSX上的@datUser bash仍然停留在3.2(大约于2007年发布);我使用Homebrew中的bash在OS X上获得4.X bash版本
JDS

222

这是一种无需设置IFS的方法:

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

这个想法是使用字符串替换:

${string//substring/replacement}

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

(element1 element2 ... elementN)

注意:此答案使用split + glob运算符。因此,为防止某些字符(例如*)扩展,最好暂停此脚本的Globing。


1
使用这种方法...直到我遇到了一个很长的字符串要分裂。100%CPU超过一分钟(然后我杀死了它)。很遗憾,因为此方法允许按字符串而不是IFS中的某些字符进行拆分。
沃纳·莱曼

100%的CPU时间(超过一分钟)在我看来,好像某个地方有问题。该字符串长度是MB还是GB?我认为,通常情况下,如果只需要拆分一个小的字符串,则希望保留在Bash中,但是如果它是一个巨大的文件,我将执行类似Perl的操作。

12
警告:刚遇到这种方法的问题。如果您有一个名为*的元素,那么您还将获得cwd的所有元素。因此string =“ 1:2:3:4:*”将根据您的实现给出一些意想不到的甚至可能是危险的结果。(IFS =','read -a array <<<“ $ string”)没有得到相同的错误,并且似乎可以安全使用。
Dieter Gribnitz 2014年

4
引用${string//:/ }防止外壳扩展
安德鲁·怀特

1
我必须在OSX上使用以下功能: array=(${string//:/ })
Mark Thomson

95
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

打印三张


8
我实际上更喜欢这种方法。简单。
虾车2015年

4
我复制并粘贴了此内容,它不适用于echo,但是在for循环中使用它时却有效。
2015年

2
如上所述,这不起作用。@ Jmoney38或虾皮旅行车(如果您可以将其粘贴到终端中并获得所需的输出,请在此处粘贴结果)。
abalter

2
@abalter为我工作a=($(echo $t | tr ',' "\n"))。与的结果相同 a=($(echo $t | tr ',' ' '))

@procrastinator我只是VERSION="16.04.2 LTS (Xenial Xerus)"bashshell中尝试过,最后一个echo只是打印出空白行。您正在使用哪个版本的Linux和哪个Shell?不幸的是,无法在评论中显示终端会话。
abalter

29

有时候,我碰巧接受的答案中描述的方法不起作用,特别是如果分隔符是回车符的时候。
在那种情况下,我是这样解决的:

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

2
+1这完全为我工作。我需要将多个字符串(由换行符分隔)放入数组中,并且read -a arr <<< "$strings"无法使用IFS=$'\n'
Stefan van den Akker,2015年


这并不能完全回答原始问题。
迈克,

29

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

string='first line
        second line
        third line'

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

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

或更简单的bash readarray

readarray -t lines <<<"$string"

利用printf功能可以很容易地打印所有行:

printf ">[%s]\n" "${lines[@]}"

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

2
尽管并非每种解决方案都能在每种情况下都有效,但是您提到readarray……用5分钟取代了我的最后两个小时……您得到了我的投票
愤怒的84年3

7

这类似于Jmoney38方法,但是使用sed:

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

版画1


1
在我的情况下,它显示1 2 3 4
minigeek '19

6

将字符串拆分为数组的关键是的多字符定界符", "。使用IFS多字符定界符的任何解决方案本质上都是错误的,因为IFS是这些字符的集合,而不是字符串。

如果您指定,IFS=", "则字符串将在EITHER ","OR " "或它们的任意组合上中断,而这不能正确表示的两个字符定界符", "

您可以使用awksed分割字符串,并进行进程替换:

#!/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...

使用第二种形式时,没有子外壳,并且本质上会更快。


bgoldst编辑:以下是一些基准,将我的readarray解决方案与dawg的正则表达式解决方案进行了比较,并且还包括了read针对该问题的解决方案(注意:我对正则表达式解决方案进行了少许修改,以使其与我的解决方案更加协调)(另请参见下面的评论)发布):

## 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
##

很酷的解决方案!我从没想过在正则表达式匹配上使用循环,巧妙地使用$BASH_REMATCH。它有效,并且确实避免了产生子外壳。向我+1。但是,通过批评,正则表达式本身有点不理想,因为您似乎被迫复制了定界符标记的一部分(特别是逗号),从而避免了对非贪婪乘法器的支持(也是环顾四周)在ERE(bash中内置的“扩展”正则表达式样式)中。这使得它的通用性和健壮性有所降低。
bgoldst

其次,我进行了一些基准测试,尽管对于较小的字符串,其性能要优于其他解决方案,但由于反复进行字符串重建,它的性能呈指数级恶化,对非常大的字符串而言,这是灾难性的。查看我对您答案的修改。
bgoldst

@bgoldst:多么酷的基准!为了保护正则表达式,对于成千上万个字段(正则表达式正在拆分)的10或100的字段,可能会有某种形式的记录(例如带\n分隔符的文本行)包含这些字段,因此可能不会发生灾难性的减速。如果您有一个包含100,000个字段的字符串-也许Bash不理想;-)感谢您提供基准测试。我学到了一两件事。
dawg

4

纯bash多字符定界符解决方案。

正如其他人在该线程中指出的那样,OP的问题给出了一个以逗号分隔的字符串要解析为数组的示例,但并未指出他/她是否只对逗号分隔符,单字符分隔符或多字符感兴趣。定界符。

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

如果您正在寻找一个多字符定界符问题的解决方案,我建议您回顾一下Mallikarjun M的帖子,尤其是gniourf_gniourf的回应, 后者使用参数扩展提供了这种优雅的纯BASH解决方案:

#!/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
请参阅我的评论,以获取类似但经过改进的方法。
xebeche

3

这对我在OSX上有效:

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

如果您的字符串具有不同的定界符,则只需1st用空格替换它们:

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

简单:-)


适用于Bash和Zsh,这是一个加号!
伊莱贾·W·加涅

2

无需修改IFS的另一种方法:

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

无需更改IFS以匹配所需的定界符,我们可以用via的", "内容替换所有出现的所需定界符。$IFS"${string//, /$IFS}"

也许这对于非常大的字符串来说会很慢?

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


2

当我想解析一个输入时遇到了这个帖子: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

1

尝试这个

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

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

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

添加了IFS以撤消上述操作,但在新的bash实例中不使用IFS即可工作


1

我们可以使用tr命令将字符串拆分为数组对象。它同时适用于MacOS和Linux

  #!/usr/bin/env bash
  currentVersion="1.0.0.140"
  arrayData=($(echo $currentVersion | tr "." "\n"))
  len=${#arrayData[@]}
  for (( i=0; i<=$((len-1)); i++ )); do 
       echo "index $i - value ${arrayData[$i]}"
  done

另一种选择使用IFS命令

IFS='.' read -ra arrayData <<< "$currentVersion"
#It is the same as tr
arrayData=($(echo $currentVersion | tr "." "\n"))

#Print the split string
for i in "${arrayData[@]}"
do
    echo $i
done

0

用这个:

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

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

3
错误:可能会进行分词和路径名扩展。请不要用好的答案来复习旧问题,以给出不好的答案。
gniourf_gniourf

2
这可能是一个错误的答案,但仍然是有效的答案。举报者/审阅者: 对于错误答案,例如这一答案,请表决,请勿删除!
Scott Weldon

2
@gniourf_gniourf您能否解释为什么这是一个错误的答案?我真的不明白什么时候失败了。
乔治·索维托夫

3
@GeorgeSovetov:就像我说的那样,它需要分词和扩展路径名。更一般地,将字符串分割为一个数组作为array=( $string )是(可悲很常见)反模式:字分裂发生:string='Prague, Czech Republic, Europe'; 发生路径名扩展:string='foo[abcd],bar[efgh]'如果您有一个文件,例如foodbarf位于目录中。这种构造的唯一有效用法是when string是一个glob。
gniourf_gniourf

0

更新:由于评估问题,请勿执行此操作。

用更少的仪式:

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

例如

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

4
评估是邪恶的!不要这样
caesarsol 2015年

1
Pfft。不。如果您编写的脚本足够大,那么这是错误的。在应用程序代码中,eval是邪恶的。在shell脚本中,它是常见,必要和无关紧要的。
user1009908 2015年

2
将a $放入您的变量中,您将看到...我编写了许多脚本,而且我从未使用过单个脚本eval
caesarsol

2
没错,这仅在已知输入干净时才可用。不是一个可靠的解决方案。
user1009908 2015年

我唯一必须使用eval的时间是针对会自行生成自己的代码/模块的应用程序……而这从来没有任何形式的用户输入...
2015年

0

这是我的骇客!

使用bash逐个字符串拆分是一件很无聊的事情。发生的是,我们的方法有限,仅在少数情况下有效(由“;”,“ /”,“。”等分隔),否则输出中会产生多种副作用。

下面的方法需要一些技巧,但是我相信它将满足我们的大多数需求!

#!/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" == "\n" ] ; 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" (\n) 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("\n", "3F2C417D448C46918289218B7337FCAF"); printf $0}' <<< "${TARGET_P}")

    fi

    # NOTE: The replace of "\n" by "3F2C417D448C46918289218B7337FCAF" results 
    # in more occurrences of "3F2C417D448C46918289218B7337FCAF" than the 
    # amount of "\n" 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\n", $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", "\n"); 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.: /dba/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

0

对于多行元素,为什么不这样

$ array=($(echo -e $'a a\nb b' | tr ' ' '§')) && array=("${array[@]//§/ }") && echo "${array[@]/%/ INTERELEMENT}"

a a INTERELEMENT b b INTERELEMENT

-1

另一种方法是:

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

现在,您的元素存储在“ arr”数组中。要遍历元素:

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

1
我在回答中涵盖了这个想法; 请参阅错误答案5(您可能对我对这个eval技巧的讨论特别感兴趣)。$IFS事后,您的解决方案设置为逗号空间值。
bgoldst

-1

由于有很多方法可以解决此问题,因此让我们先定义要在解决方案中看到的内容。

  1. Bash提供了一个内置的 readarray为此。让我们使用它。
  2. 避免使用丑陋和不必要的技巧,例如更改IFS,循环播放,使用eval或添加额外的元素然后将其删除。
  3. 找到一种简单易读的方法,可以轻松地将其应用于类似问题。

readarray命令最容易与换行符一起用作分隔符。使用其他定界符,可能会在数组中添加额外的元素。最干净的方法是首先将我们的输入调整为与readarray然后再传递它。

在此示例中,输入没有多字符定界符。如果我们应用一些常识,则最好将其理解为逗号分隔的输入,可能需要针对每个输入修剪它们。我的解决方案是用逗号将输入分成多行,修剪每个元素,然后将其全部传递给readarray

string='  Paris,France  ,   All of Europe  '
readarray -t foo < <(tr ',' '\n' <<< "$string" |sed 's/^ *//' |sed 's/ *$//')
declare -p foo

# declare -a foo='([0]="Paris" [1]="France" [2]="All of Europe")'

-2

另一种方法可以是:

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

在此“ arr”之后是具有四个字符串的数组。这不需要处理IFS或读取或任何其他特殊内容,因此更加简单直接。


与其他答案相同(非常常见)的反模式:可能会出现单词拆分和文件名扩展的情况。
gniourf_gniourf
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.