将字符串拆分成Bash中的数组

在一个Bash脚本中,我想将一行分成几部分,并将它们放入一个数组中。

该行:

Paris, France, Europe

我想让他们像这样的数组:

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

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


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

较大的负偏移距离阵列的末端较远。 请注意旧格式中减号前的空格。 这是必需的。


以下是未设置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操作符。 因此,为了防止某些字符(如* )的扩展,暂停此脚本的globbing是一个好主意。


对这个问题的所有答案在某种方面都是错误的。


错误的答案#1

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

1:这是滥用$IFS 。 所述的值$IFS变量采取作为一个单一的可变长度的字符串分隔符,而它被作为一组单字符字符串分隔符,其中每一个该字段read从输入线分割关闭可以通过任何字符被终止在集合中(逗号空格,在这个例子中)。

实际上,对于那些真正的坚持者来说, $IFS的全部意义稍微有点牵扯。 从bash手册:

shell将IFS的每个字符视为分隔符,并将其他扩展的结果拆分为使用这些字符作为字段终止符的单词。 如果IFS未设置,或者其值正好是<space> <tab> <newline> ,则在先前扩展的结果开始和结束时的默认值,然后是<space><tab><newline>序列被忽略,并且任何不在开始或结束的IFS字符序列都用来分隔单词。 如果IFS的值不是默认值,那么只要空白字符的值为空白字符<space><tab><newline>的序列在该单词的开始和结尾被忽略IFSIFS空白字符)。 IFS中不是IFS空白字符的任何字符以及任何相邻的IFS空白字符都会分隔一个字段。 一系列IFS空格字符也被视为分隔符。 如果IFS的值为空,则不会发生分词。

基本上,对于$IFS非默认值非空值,字段可以用(1)一组一个或多个字符的序列来分隔,这些字符全部来自“IFS空白字符”集合(也就是说,无论哪个<space >,< 标签><换行符>(“换行”,意思是换行(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语句,这也是如此,就像我们在本示例中使用here-string机制所做的一样,因此未处理的输入将保证丢失。 为内置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' )的反例。

此外,文字分割后通常还有文件名扩展(又名路径名扩展(又名globbing)),如果完成,可能会损坏包含字符* ,?的单词? ,或[后跟] (如果extglob已设置, extglob ?*+@! extglob括号碎片)通过将它们与文件系统对象进行匹配并相应扩展单词(“globs”)来实现。 这三位回答者中的第一位通过事先运行set -f来禁用globbing,从而巧妙地削弱了这个问题。 从技术上讲,这是有效的(尽管之后你可能需要添加set +f来为可能依赖它的后续代码重新启用globbing),但为了破解基本的字符串到数组的解析操作,不得不弄乱全局shell设置在本地代码中。

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

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


错误的答案#3

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 -f临时禁用分配的globbing,然后set +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 -fset +f的关键语句包装来解决。

另一个潜在的问题是,由于LF被定义为前面定义的“IFS空白字符”,因此所有空字段都将丢失,就像在#2#3中一样 。 如果分隔符碰巧是非IFS空白字符,那么这当然不会成为问题,并且取决于应用程序,它可能无关紧要,但它确实破坏了解决方案的一般性。

因此,总而言之,假设您有一个单字符分隔符,并且它既可以是非IFS空格字符,也可以不关心空字段,并将关键语句包装在set -fset +f ,那么这个解决方案起作用,否则不行。

(另外,出于信息的原因,使用$'...'语法可以更轻松地将b指定给bash中的变量,例如IFS=$'n';


错误的答案#5

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

类似的想法:

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

这个解决方案实际上是#1 (它将$IFS设置$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环境中运行它,就好像它是普通的静态源代码一样,因此我们可以在eval参数内运行$array赋值,使其在shell环境中生效,而以eval命令为前缀的$IFS前缀分配不会超过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内部字段分隔符,用于扩展后的字词拆分,以及使用读取内置命令将字词拆分为单词。 默认值是<space> <tab> <newline>

因此, $IFS特殊变量实际上只用于两种情况:(1)扩展后执行的单词拆分(意味着不在解析bash源代码时执行)和(2)通过内置read将输入行拆分为单词。

让我试着更清楚一点。 我认为在解析和执行之间作出区分可能是件好事。 Bash必须首先解析源代码,这显然是一个解析事件,然后它会执行代码,这是扩展进入图片时的代码。 扩张实际上是一个执行事件。 此外,我对上面刚刚引用的$IFS变量的描述存在疑问。 而不是说在扩展之后执行分词,我会说分词是在扩展过程中执行的,或者更准确地说,分词是扩展过程的一部分。 “分词”一词仅指这一扩展步骤; 它不应该被用来指代bash源代码的解析,尽管不幸的是这些文档看起来似乎很多地使用了“拆分”和“单词”这些词。 以下是bash手册的linux.die.net版本的相关摘录:

将命令行分割为单词后,再进行扩展。 有七种类型的扩展:括号扩展,波形扩展,参数和变量扩展,命令替换,算术扩展,分词和路径名扩展。

扩展的顺序是:支撑扩展; 波浪扩展,参数和变量扩展,算术扩展和命令替换(按照从左到右的方式完成); 分词; 和路径名称扩展。

你可能会认为手册的GNU版本稍微好一点,因为它在扩展部分的第一句中选择了“令牌”而不是“单词”这个词:

在命令行被分割为令牌之后,扩展被执行。

重要的一点是, $IFS不会改变bash分析源代码的方式。 解析bash源代码实际上是一个非常复杂的过程,它涉及识别shell语法的各种元素,例如命令序列,命令列表,管道,参数扩展,算术替换和命令替换。 在大多数情况下,bash解析过程不能被用户级动作(如变量赋值)改变(实际上,这个规则有一些小例外;例如,请参阅各种compatxx shell设置,这些设置可以改变解析行为的某些方面在即时)。 然后根据在上述文档摘录中分解的“扩展”的一般过程来扩展由这个复杂解析过程导致的上游“词”/“标记”,其中将扩展(扩展的?)文本分词为下游话语只是这一过程的一个步骤。 分词只会触及已经从前面的扩展步骤中吐出的文本; 它不会影响从源字节流中解析出的文本文本。


错误的答案#7

string='first line
        second line
        third line'

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

这是最好的解决方案之一。 请注意,我们正在使用read 。 我不是早些时候说过, read是不恰当的,因为它执行两个级别的分裂,当我们只需要一个? 这里的诀窍是,你可以通过这种方式调用read ,使得它只能进行一级分割,特别是通过每次调用只分割一个字段,这需要花费不得不在循环中重复调用它。 这是一个有趣的手段,但它的作品。

但有问题。 首先:当你提供至少一个名称参数来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文件结束时read失败的返回码(在这种情况下,我们可以称之为字符串结束),而不会在最终字段中遇到最终字段终止符。 这会导致while循环提前中断,并且我们会丢失最后一个字段。

从技术上讲,同样的错误也影响了前面的例子。 不同之处在于字段分隔符被认为是LF,当您不指定-d选项时,这是缺省值,而<<< (“here-string”)机制会自动将LF追加到字符串中然后将其作为输入提供给命令。 因此,在这些情况下,我们通过无意中在输入中附加了一个虚拟终结符,意外地解决了终止字段丢失的问题。 我们称这个解决方案为“虚拟终结者”解决方案。 当在here-string中实例化时,我们可以通过手动将自定义分隔符与输入字符串连接,来手动应用虚拟终结符解决方案:

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]=$' Europen')

这种方法还揭示了隐秘的LF,它通过<<<重定向操作符自动附加到here-string。 当然,它可以通过前面介绍的显式修剪操作单独剥离,但显然手动伪终端方法直接解决了这个问题,所以我们可以继续这样做。 手动伪终端解决方案实际上非常方便,因为它一次性解决了这两个问题(丢失最终字段问题和附加LF问题)。

总的来说,这是一个非常强大的解决方案。 仅剩的弱点是缺乏对多字符分隔符的支持,我将在稍后讨论。


错误的答案#8

string='first line
        second line
        third line'

readarray -t lines <<<"$string"

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

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

首先,为了避免这种情况发生,请注意,就像在进行字段解析时的read行为一样, readarray会在尾部字段为空时删除尾部字段。 同样,这可能不是OP的问题,但它可能适用于某些使用情况。 我会在一会儿回来。

其次,和以前一样,它不支持多字符分隔符。 我也会在这一刻给出一个解决方案。

第三,所写的解决方案不解析OP的输入字符串,事实上,它不能用于解析它。 我也会暂时展开。

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


正确答案

这是一个天真的尝试,只需指定-d选项即可使#8工作:

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

我们看到结果与我们从#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(/, /,""); print; }' <<<"$string, "); unset 'a[-1]';
declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

那里,最后! 这个解决方案不会错误地在中间分割字段,不会过早地切出,不会删除空字段,在文件名扩展时不会自行破坏,不会自动删除前导和尾随空格,不会在最后留下偷走的LF,不需要循环,并且不适用于单字符分隔符。


修剪解决方案

最后,我想使用readarray -C callback选项来演示我自己相当复杂的修剪解决方案。 不幸的是,我已经没有足够的空间来对付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")
链接地址: http://www.djcxy.com/p/77075.html

上一篇: Split string into an array in Bash

下一篇: Split large string into smaller chunks in c#