这个问题的所有答案在某种程度上都是错误的。
错误的答案#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内部操作。(此外,对于和解决方案,输入变量应在命令替换中用双引号引起;否则,单词拆分将在命令中生效,并可能使字段值混乱。而且,命令替换的形式比旧版本更可取。tr
sed
tr
sed
echo
$(...)
`...`
格式,因为它简化了命令替换的嵌套,并允许文本编辑器更好地突出显示语法。)
错误的答案#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 -f
再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 -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环境中运行,就像正常的静态源代码一样,因此,我们可以$array
在eval
参数内部运行赋值以使其在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解析过程无法通过用户级操作(例如变量分配)来更改(实际上,此规则有一些小例外;例如,请参见各种compatxx
shell设置),这可以即时更改解析行为的某些方面。然后,根据上述文档摘录中分解的一般“扩展”过程,将由复杂的解析过程产生的上游“单词” /“令牌”进行扩展,其中将扩展(扩展?)文本的单词拆分为下游单词只是该过程的一个步骤。分词仅涉及上一个扩展步骤中吐出的文本;它不会影响立即从源字节流解析的文本文本。
错误的答案#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")
,
(逗号)而不是逗号等单个字符的定界问题。如果您仅对后者感兴趣,可以在这里轻松找到答案:stackoverflow.com/questions/918886/…–