在 Bash 脚本中,我想将一行分成几部分并将它们存储在一个数组中。
例如,给定以下行:
Paris, France, Europe
我想让结果数组看起来像这样:
array[0] = Paris
array[1] = France
array[2] = Europe
一个简单的实现是可取的;速度无所谓。我该怎么做?
,
(逗号空格)而不是 单个字符(例如逗号)的定界。如果您只对后者感兴趣,这里的答案更容易理解:stackoverflow.com/questions/918886/…
cut
也是一个需要记住的有用的 bash 命令。分隔符是可定义的 en.wikibooks.org/wiki/Cut 您也可以从固定宽度的记录结构中提取数据。 en.wikipedia.org/wiki/Cut_(Unix) computerhope.com/unix/ucut.htm
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=', ' read -r -a array <<< "$string"
1:这是对 $IFS
的误用。 $IFS
变量的值不被视为单个变长字符串分隔符,而是被视为集合的< em>single-character 字符串分隔符,其中 read
从输入行拆分出来的每个字段都可以由集合中的 any 字符(逗号 或 空间,在本例中)。
实际上,对于真正的坚持者来说,$IFS
的完整含义稍微复杂一些。从 bash manual:
shell 将 IFS 的每个字符视为分隔符,并将其他扩展的结果拆分为使用这些字符作为字段终止符的单词。如果 IFS 未设置,或者它的值恰好是默认值
基本上,对于 $IFS
的非默认非空值,字段可以用 (1) 一个或多个字符的序列来分隔,这些字符都来自“IFS 空白字符”集(即,以 < strong><space>、<tab> 和 <newline>(“newline”表示 line feed (LF))出现在 {2 }),或 (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: 即使您要使用带有单字符分隔符的解决方案(例如逗号本身,即没有后面的空格或其他包袱),如果 {2 } 变量恰好包含任何 LF,然后 read
将在遇到第一个 LF 时停止处理。 read
内置每次调用只处理一行。即使您将输入 only 管道或重定向到 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
之前附加一个虚拟尾随分隔符来解决,稍后我将演示。
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'
)的反例。
此外,分词通常后跟 filename expansion(aka 路径名扩展 aka 通配符),如果这样做,可能会损坏包含字符 *
、{ 3} 或 [
后跟 ]
(并且,如果设置了 extglob
,则以 ?
、*
、+
、@
或 !
开头的带括号的片段)通过匹配它们针对文件系统对象并相应地扩展单词(“globs”)。这三个回答者中的第一个通过预先运行 set -f
来禁用通配,巧妙地解决了这个问题。从技术上讲,这是可行的(尽管您可能应该在之后添加 set +f
以重新启用可能依赖于它的后续代码的通配符),但是为了破解基本的字符串到数组的解析操作而不得不弄乱全局 shell 设置是不可取的在本地代码中。
此答案的另一个问题是所有空字段都将丢失。这可能是也可能不是问题,具体取决于应用程序。
注意:如果您要使用此解决方案,最好使用 parameter expansion 的 ${string//:/ }
“模式替换”形式,而不是麻烦地调用命令替换(它分叉 shell),启动管道,并运行外部可执行文件(tr
或 sed
),因为参数扩展纯粹是 shell 内部操作。 (此外,对于 tr
和 sed
解决方案,输入变量应在命令替换内用双引号引起来;否则分词将在 echo
命令中生效,并可能与字段值混淆。此外, $(...)
形式的命令替换比旧的 `...`
形式更可取,因为它简化了命令替换的嵌套并允许文本编辑器更好地突出显示语法。)
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
和 set +f
的分配来防止这种情况。
同样,所有空字段都将丢失,这可能会或可能不会成为问题,具体取决于应用程序。
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
和 {2 },则此解决方案有效,否则无效。
(另外,为了方便起见,使用 $'...'
语法(例如 IFS=$'\n';
)可以更轻松地将 LF 分配给 bash 中的变量。)
countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"
IFS=', ' eval 'array=($string)'
这个解决方案实际上是 #1(因为它将 $IFS
设置为逗号空格)和 #2-4(因为它使用分词来拆分字符串到字段中)。正因为如此,它遭受了困扰上述所有错误答案的大多数问题,有点像世界上最糟糕的问题。
此外,关于第二个变体,eval
调用似乎完全没有必要,因为它的参数是单引号字符串文字,因此是静态已知的。但实际上以这种方式使用 eval
有一个非常不明显的好处。通常,当您运行一个包含变量赋值only的简单命令时,这意味着它后面没有实际的命令字,赋值在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 manual 中的相关引用:
如果没有产生命令名,变量分配会影响当前的 shell 环境。否则,变量会被添加到执行命令的环境中,并且不会影响当前的 shell 环境。
可以利用变量赋值的这一特性来临时更改 $IFS
,这使我们能够避免整个保存和恢复策略,就像在第一个变体中使用 $OIFS
变量所做的那样。但是我们在这里面临的挑战是我们需要运行的命令本身只是一个变量赋值,因此它不会涉及一个命令字来使 $IFS
赋值临时。您可能会想,为什么不直接在语句中添加一个 no-op 命令字,如 : 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 要求的错误答案。
IFS=', '; array=(Paris, France, Europe)
IFS=' ';declare -a array=(Paris France Europe)
嗯什么? OP 有一个字符串变量,需要将其解析为数组。这个“答案”从粘贴到数组文字中的输入字符串的逐字内容开始。我想这是一种方法。
看起来回答者可能假设 $IFS
变量会影响所有上下文中的所有 bash 解析,这是不正确的。从 bash 手册:
IFS 内部字段分隔符,用于扩展后的分词,并使用 read 内置命令将行拆分为单词。默认值为
所以$IFS
这个特殊变量实际上只在两种情况下使用:(1)分词展开后(意思是not在解析bash源代码时)和( 2) 通过内置 read
将输入行拆分为单词。
让我试着更清楚地说明这一点。我认为区分解析和执行可能会很好。 Bash必须先解析源代码,这显然是一个解析事件,然后执行代码,也就是展开的时候图片。扩展实际上是一个执行事件。此外,我对上面刚刚引用的 $IFS
变量的描述提出了质疑;与其说分词是在展开之后,我会说分词是在在展开的过程中进行的,或者更准确地说,分词是的一部分的扩展过程。短语“分词”仅指扩展这一步;它永远不应该用来指对 bash 源代码的解析,尽管不幸的是,文档似乎确实大量使用了“split”和“words”这两个词。以下是 bash 手册 linux.die.net version 的相关摘录:
将其拆分为单词后在命令行上执行扩展。执行的扩展有七种:大括号扩展、波浪号扩展、参数和变量扩展、命令替换、算术扩展、分词和路径名扩展。展开顺序为:大括号展开;波浪号扩展、参数和变量扩展、算术扩展和命令替换(以从左到右的方式完成);分词;和路径名扩展。
您可能会争辩说手册的 GNU version 做得稍微好一些,因为它在扩展部分的第一句中选择了“令牌”而不是“单词”:
将其拆分为令牌后,在命令行上执行扩展。
重要的一点是,$IFS
不会改变 bash 解析源代码的方式。解析 bash 源代码实际上是一个非常复杂的过程,涉及到对 shell 语法的各种元素的识别,例如命令序列、命令列表、管道、参数扩展、算术替换和命令替换。在大多数情况下,bash 解析过程不能通过变量赋值之类的用户级操作来改变(实际上,这个规则有一些小例外;例如,参见各种 compatxx
shell settings,它可以改变解析行为的某些方面即时)。然后根据上述文档摘录中分解的“扩展”的一般过程,扩展从这个复杂的解析过程产生的上游“单词”/“令牌”,其中扩展(扩展?)文本的分词到下游文字只是该过程的一个步骤。分词只涉及从前一个扩展步骤中吐出的文本;它不会影响直接从源字节流解析的文字文本。
string='first line
second line
third line'
while read -r line; do lines+=("$line"); done <<<"$string"
这是最好的解决方案之一。请注意,我们又回到了使用 read
。我之前不是说 read
不合适,因为它执行两级拆分,而我们只需要一个吗?这里的诀窍是您可以调用 read
,使其有效地只进行一级拆分,特别是每次调用只拆分一个字段,这需要在循环中重复调用它的成本。这有点花招,但它确实有效。
但也有问题。第一:当您向 read
提供至少一个 NAME 参数时,它会自动忽略从输入字符串中分离出来的每个字段中的前导和尾随空格。如本文前面所述,无论 $IFS
是否设置为其默认值,都会发生这种情况。现在,OP 对于他的特定用例可能并不关心这一点,事实上,它可能是解析行为的一个理想特性。但并不是每个想要将字符串解析为字段的人都会想要这个。但是,有一个解决方案:read
的一个不太明显的用法是传递零个 NAME 参数。在这种情况下,read
将从输入流中获取的整个输入行存储在名为 $REPLY
的变量中,并且作为奖励,它不从价值。这是我在 shell 编程生涯中经常使用的 read
的一个非常强大的用法。这是行为差异的演示:
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
到达文件结尾(在这种情况下,我们可以称之为字符串结尾)而没有在最终字段上遇到最终字段终止符,则返回失败的返回码。这会导致 while 循环过早中断,并且我们丢失了 final 字段。
从技术上讲,同样的错误也影响了前面的例子。不同之处在于,字段分隔符被视为 LF,这是您未指定 -d
选项时的默认值,并且 <<<
(“here-string”)机制会自动将 LF 附加到字符串就在它作为命令的输入之前。因此,在这些情况下,我们通过不经意间在输入中附加一个额外的虚拟终止符,有点意外解决了最终字段丢失的问题。让我们将此解决方案称为“虚拟终结者”解决方案。我们可以通过在此处字符串中实例化它时自己将其与输入字符串连接起来,手动为任何自定义分隔符应用虚拟终止符解决方案:
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')
这种方法还揭示了由 <<<
重定向运算符自动附加到 here-string 的秘密 LF。它当然可以通过如前所述的显式修剪操作单独剥离,但显然手动虚拟终结器方法直接解决了它,所以我们可以这样做。手动 dummy-terminator 解决方案实际上非常方便,因为它一次性解决了这两个问题(drop-final-field 问题和 appended-LF 问题)。
所以,总的来说,这是一个非常强大的解决方案。唯一剩下的弱点是缺乏对多字符分隔符的支持,我将在稍后解决。
string='first line
second line
third line'
readarray -t lines <<<"$string"
(这实际上来自与 #7 相同的帖子;回答者在同一个帖子中提供了两个解决方案。)
readarray
是 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]=$' Europe\n')
我们看到结果与 #7 中讨论的循环 read
解决方案的双条件方法得到的结果相同。我们可以几乎使用手动虚拟终结器技巧解决这个问题:
readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'\n')
这里的问题是 readarray
保留了尾随字段,因为 <<<
重定向运算符将 LF 附加到输入字符串,因此尾随字段 not 为空(否则它会掉了)。我们可以通过在事后显式取消设置最终数组元素来解决这个问题:
readarray -td, a <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")
剩下的唯一两个实际相关的问题是 (1) 需要修剪的无关空白,以及 (2) 缺乏对多字符分隔符的支持。
之后当然可以修剪空白(例如,参见 How to trim whitespace from a Bash variable?)。但是,如果我们可以破解多字符分隔符,那么这将一次性解决这两个问题。
不幸的是,没有直接 方法可以让多字符分隔符起作用。我想到的最佳解决方案是对输入字符串进行预处理,将多字符定界符替换为单字符定界符,保证不会与输入字符串的内容发生冲突。唯一具有此保证的字符是 NUL byte。这是因为,在 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,不需要循环,并且不满足于单字符分隔符。
修整解决方案
最后,我想使用 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")
readarray
的 -d
选项首次出现在 Bash 4.4 中也可能会有所帮助。
awk '{ gsub(/,[ ]+|$/,"\0"); print }'
并消除最后一个 ", "
的串联,那么您不必通过体操来消除最终记录。所以:readarray -td '' a < <(awk '{ gsub(/,[ ]+/,"\0"); print; }' <<<"$string")
在支持 readarray
的 Bash 上。请注意,您的方法是 Bash 4.4+,我认为是因为 readarray
中的 -d
这是一种不设置 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 operator。因此,为了防止扩展某些字符(例如 *
),最好暂停此脚本的通配符。
${string//:/ }
防止 shell 扩展
array=(${string//:/ })
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"
打印三个
a=($(echo $t | tr ',' "\n"))
一起为我工作。与 a=($(echo $t | tr ',' ' '))
的结果相同。
VERSION="16.04.2 LTS (Xenial Xerus)"
的 bash
外壳中尝试过,最后一个 echo
只是打印一个空行。您使用的是什么版本的 Linux 和哪个 shell?不幸的是,无法在评论中显示终端会话。
有时我碰巧接受的答案中描述的方法不起作用,特别是如果分隔符是回车符。在这些情况下,我以这种方式解决了:
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
read -a arr <<< "$strings"
不适用于 IFS=$'\n'
。
接受的答案适用于一行中的值。如果变量有几行:
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]
while read
循环不会产生显示的输出,因为它会去除前导/尾随空格。它必须是 while IFS= read -r line; do lines+=("$line"); done <<<"$string"
才能产生显示的输出。
这适用于我在 OSX 上:
string="1 2 3 4 5"
declare -a array=($string)
如果您的字符串有不同的分隔符,只需首先用空格替换它们:
string="1,2,3,4,5"
delimiter=","
declare -a array=($(echo $string | tr "$delimiter" " "))
简单的 :-)
$string
,这通常无法正常工作。
如果你使用 macOS 并且不能使用 readarray,你可以简单地这样做——
MY_STRING="string1 string2 string3"
array=($MY_STRING)
迭代元素:
for element in "${array[@]}"
do
echo $element
done
MY_STRING
值包含通配符,它也可能会导致它们的扩展......
这类似于 approach by Jmoney38,但使用 sed:
string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
echo ${array[0]}
打印 1
tr
的答案,并使情况变得更糟。现在,更复杂的工具涉及更复杂的语法和正则表达式。此外,原始中的现代 $()
语法已被过时的反引号所取代。
将字符串拆分为数组的关键是 ", "
的多字符分隔符。将 IFS
用于多字符分隔符的任何解决方案本质上都是错误的,因为 IFS 是一组这些字符,而不是字符串。
如果您指定 IFS=", "
,则字符串将在 ","
或 " "
或它们的任何组合上中断,这不是 ", "
的两个字符分隔符的准确表示。
您可以使用 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...
使用第二种形式,没有子外壳,它本质上会更快。
由 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
。它有效,并且确实避免了产生子shell。 +1 来自我。但是,作为批评,正则表达式本身有点不理想,因为您似乎被迫复制分隔符标记的一部分(特别是逗号),以解决缺乏对非贪婪乘数的支持的问题ERE 中的(也是外观)(bash 中内置的“扩展”正则表达式风格)。这使它不那么通用和健壮。
\n
分隔的文本行)包含这些字段,因此灾难性的减速可能不会发生。如果您有一个包含 100,000 个字段的字符串——也许 Bash 并不理想;-) 感谢您的基准测试。我学到了一两件事。
c_readarray
不适用于 v4.4 之前的 Bash。 c_read
和 c_regex
工作得很好。你在哪里可以找到你问的这样的“旧”Bash 版本?在像 RHEL7.9 这样的发行版中,我告诉你。
纯 bash 多字符分隔符解决方案。
正如其他人在这个线程中指出的那样,OP的问题给出了一个逗号分隔字符串被解析为数组的例子,但没有表明他/她是否只对逗号分隔符、单字符分隔符或多字符感兴趣分隔符。
由于谷歌倾向于将此答案排在搜索结果的顶部或附近,因此我想为读者提供一个关于多字符分隔符问题的有力答案,因为至少在一个回复中也提到了这一点。
如果您正在寻找多字符分隔符问题的解决方案,我建议您查看 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
链接到 cited comment/referenced post
引用问题的链接:Howto split a string on a multi-character delimiter in bash?
尝试这个
IFS=', '; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done
这很简单。如果需要,您还可以添加声明(并删除逗号):
IFS=' ';declare -a array=(Paris France Europe)
添加 IFS 以撤消上述操作,但在新的 bash 实例中没有它也可以工作
#!/bin/bash
string="a | b c"
pattern=' | '
# replaces pattern with newlines
splitted="$(sed "s/$pattern/\n/g" <<< "$string")"
# Reads lines and put them in array
readarray -t array2 <<< "$splitted"
# Prints number of elements
echo ${#array2[@]}
# Prints all elements
for a in "${array2[@]}"; do
echo "> '$a'"
done
此解决方案适用于较大的分隔符(超过一个字符)。如果原始字符串中已有换行符,则不起作用
这适用于给定的数据:
$ aaa='Paris, France, Europe'
$ mapfile -td ',' aaaa < <(echo -n "${aaa//, /,}")
$ declare -p aaaa
结果:
declare -a aaaa=([0]="Paris" [1]="France" [2]="Europe")
它也适用于带有空格的扩展数据,例如“New York”:
$ aaa="New York, Paris, New Jersey, Hampshire"
$ mapfile -td ',' aaaa < <(echo -n "${aaa//, /,}")
$ declare -p aaaa
结果:
declare -a aaaa=([0]="New York" [1]="Paris" [2]="New Jersey" [3]="Hampshire")
在不修改 IFS 的情况下执行此操作的另一种方法:
read -r -a myarray <<< "${string//, /$IFS}"
我们可以通过 "${string//, /$IFS}"
将所有出现的所需分隔符 ", "
替换为 $IFS
的内容,而不是更改 IFS 以匹配我们想要的分隔符。
也许这对于非常大的字符串来说会很慢?
这是基于丹尼斯威廉姆森的回答。
我在解析如下输入时遇到了这篇文章: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
更新:不要这样做,因为 eval 有问题。
稍微少一点仪式:
IFS=', ' eval 'array=($string)'
例如
string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar
$
放入您的变量中,您会看到...我编写了许多脚本,但从未使用过单个 eval
不要更改 IFS!
这是一个简单的 bash 单行代码:
read -a my_array <<< $(echo ${INPUT_STRING} | tr -d ' ' | tr ',' ' ')
这是我的黑客!
使用 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.: 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
由于有很多方法可以解决这个问题,让我们从定义我们希望在我们的解决方案中看到的内容开始。
为此,Bash 提供了一个内置的 readarray。让我们使用它。避免丑陋和不必要的技巧,例如更改 IFS、循环、使用 eval 或添加额外元素然后删除它。找到一种简单易读的方法,可以很容易地适应类似的问题。
readarray
命令最容易使用换行符作为分隔符。使用其他分隔符,它可能会向数组添加一个额外的元素。最简洁的方法是首先将我们的输入调整为与 readarray
完美配合的表单,然后再将其传入。
此示例中的输入确实 not 具有多字符分隔符。如果我们应用一点常识,最好将其理解为逗号分隔的输入,每个元素可能需要对其进行修剪。我的解决方案是用逗号将输入拆分为多行,修剪每个元素,然后将其全部传递给 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")'
对于多线元素,为什么不类似
$ array=($(echo -e $'a a\nb b' | tr ' ' '§')) && array=("${array[@]//§/ }") && echo "${array[@]/%/ INTERELEMENT}"
a a INTERELEMENT b b INTERELEMENT
另一种方法是:
string="Paris, France, Europe"
IFS=', ' arr=(${string})
现在您的元素存储在“arr”数组中。遍历元素:
for i in ${arr[@]}; do echo $i; done
另一种方法可以是:
str="a, b, c, d" # assuming there is a space after ',' as in Q
arr=(${str//,/}) # delete all occurrences of ','
在这个 'arr' 之后是一个包含四个字符串的数组。这不需要处理 IFS 或读取或任何其他特殊的东西,因此更简单和直接。
IFS=', '
,您就不必单独删除空格。测试:IFS=', ' read -a array <<< "Paris, France, Europe"; echo "${array[@]}"
declare -p array
进行测试输出。France, Europe, "Congo, The Democratic Republic of the"
这将在 congo 之后拆分。str="Paris, France, Europe, Los Angeles"; IFS=', ' read -r -a array <<< "$str"
将拆分为array=([0]="Paris" [1]="France" [2]="Europe" [3]="Los" [4]="Angeles")
作为注释。所以这只适用于没有空格的字段,因为IFS=', '
是一组单独的字符——而不是字符串分隔符。