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

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

例如,给定行:

Paris, France, Europe

我希望得到的数组看起来像这样:

array[0] = Parisarray[1] = Francearray[2] = Europe

一个简单的实现是可取的;速度并不重要。我该怎么做呢?

1277040 次浏览
IFS=', ' read -r -a array <<< "$string"

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

要访问单个元素:

echo "${array[0]}"

要遍历元素:

for element in "${array[@]}"doecho "$element"done

要同时获取索引和值:

for index in "${!array[@]}"doecho "$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}"

较大的负偏移选择离数组末尾更远的地方。注意旧形式中减号前的空格。这是必需的。

有时在我身上发生,在接受的答案中描述的方法不起作用,特别是如果分隔符是回车。
在这些情况下,我以这种方式解决:

string='first linesecond linethird line'
oldIFS="$IFS"IFS=''IFS=${IFS:0:1} # this is useful to format your code with tabslines=( $string )IFS="$oldIFS"
for line in "${lines[@]}"doecho "--> $line"done

这是一种不设置IFS的方法:

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

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

${string//substring/replacement}

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

(element1 element2 ... elementN)

注意:此答案使用了拆分+全局操作符。因此,为了防止某些字符(例如*)的扩展,暂停此脚本的全局化是一个好主意。

更新:请不要这样做,因为有了ava的问题。

稍微少一点的仪式:

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

e. g.

string="foo, bar,baz"IFS=', ' eval 'array=($string)'echo ${array[1]} # -> bar
t="one,two,three"a=($(echo "$t" | tr ',' '\n'))echo "${a[2]}"

三个指纹

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

string='first linesecond linethird line'

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

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

或者更简单的bash读取阵列

readarray -t lines <<<"$string"

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

printf ">[%s]\n" "${lines[@]}"
>[first line]>[        second line]>[        third line]

试试这个

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

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

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

添加IFS是为了撤消上述操作,但在新的bash实例中没有它也可以工作

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

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

打印1

另一种方法可以是:

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

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

这个问题的所有答案都以这样或那样的方式是错误的。


错误答案1

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

这是对 $IFS的滥用。$IFS变量的值被看作 单一可变长度单一可变长度字符串分隔符 没有,而不是 单个字符字符串分隔符的 准备好了,在这里,read从输入行分离出来的每个字段都可以被集合中的 任何字符终止(本例中使用的是逗号 或者空格)。

实际上,对于那些真正的顽固分子来说,$IFS的全部含义要略微复杂一些:

Shell 将 IFS的每个字符作为分隔符,并使用这些字符作为字段终止符将其他扩展的结果拆分为单词。如果 IFS未设置,或者它的值正好是 < space > < tab > < newline > ,默认值,那么在前面的展开结果的开始和结束处的 < 空间 > < 标签 > < 新行 > 序列将被忽略,任何不在开始或结束处的 IFS字符序列将用于分隔单词。如果 IFS的值不是默认值,那么在单词的开头和结尾忽略空格字符 < 空间 > < 标签 > < 新行 > 的序列,只要空白字符的值是 IFS(一个 IFS空白字符)。IFS中任何不是 IFS空格的字符,以及任何相邻的 IFS空格字符,都用于分隔字段。IFS空白字符序列也被视为分隔符。如果 IFS的值为 null,则不会发生分词。

基本上,对于 $IFS的非默认非空值,字段可以用(1)一个或多个字符的序列来分隔,这些字符都来自于“ IFS 空格字符集”(也就是说,< 空间 > < 标签 > < 新行 > (“ 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语句,正如我们在本例中使用 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]="")

也许观察员不会关心这个,但这仍然是一个值得了解的限制。它降低了解的鲁棒性和通用性。

这个问题可以通过在输入字符串之前添加一个虚拟的尾随分隔符来解决,我将在后面演示。


第二个错误答案

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')的反例。

此外,分词之后通常是 ?2(?3路径名扩展 ?3 globbing) ,如果这样做,将潜在地损坏包含字符 *?[的单词,然后是 ](如果设置了 extglob,则在 ?*+@!之前括号化片段) ,方法是将它们与文件系统对象匹配并相应地扩展单词(“ globs”)。这三个答案中的第一个聪明地削弱了这个问题,提前运行 ?0来禁用 globbing。从技术上来说,这是可行的(尽管你可能应该在之后添加 ?1来重新启用可能依赖于它的后续代码的 globbing) ,但是为了在本地代码中破解一个基本的字符串到数组解析操作而不得不改变全局 shell 设置是不可取的。

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

注意: 如果你要使用这个解决方案,最好使用 参数展开${string//:/ }“模式替换”形式,而不是去调用一个指令替代(分叉 shell) ,启动一个管道,并运行一个外部可执行程序(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 -fset +f赋值的 globbing 来防止这种情况。

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


错误答案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中,那么这个解决方案是可行的,但是否则就不行了。

(另外,为了提供信息,可以使用 $'...'语法(例如 IFS=$'\n';)更容易地将 LF 赋给 bash 中的变量。)


错误答案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赋值临时。您可能会想,那么为什么不在语句中添加一个 no-op 命令词,比如 : builtin,以使 $IFS的赋值变成临时的呢?这没有用,因为这会使 $array任务也成为临时任务:

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

所以,我们实际上陷入了僵局,有点像第二十二条军规。但是,当 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 手册中 解析0的一段相关摘录:

在将扩展拆分为单词后,将在命令行上执行扩展。有七种扩展被执行: 支撑扩张波动膨胀参数和变量展开式指令替代算术扩展分词路径名扩展

展开的顺序是: 大括号展开、波浪形展开、参数和变量展开、算术展开和指令替代(以从左到右的方式进行)、分词和路径名展开。

你可能会争辩说,手册中的 GNU 版本做得稍微好一点,因为它在展开部分的第一句话中选择了单词“ token”而不是“ words”:

在将扩展拆分为标记之后,将在命令行上执行扩展。

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


错误答案7

string='first line
second line
third line'


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

这是最好的解决办法之一。注意,我们又回到了使用 read。我之前不是说过 read是不合适的,因为它执行两个级别的分裂,而我们只需要一个级别的分裂?这里的技巧是,您可以以这样一种方式调用 read,它实际上只进行一级拆分,特别是通过每次调用只拆分一个字段,这就需要在循环中重复调用它。虽然有点花招,但还是奏效了。

但是有些问题。第一: 当您为 read提供至少一个 姓名参数时,它会自动忽略从输入字符串分离出来的每个字段中的前导空格和尾随空格。无论 $IFS是否设置为默认值,都会发生这种情况,如本文前面所述。现在,OP 可能并不关心他的特定用例,事实上,它可能是解析行为的一个理想特性。但并非所有希望将字符串解析为字段的人都希望这样做。但是,有一个解决方案: read的一个不太明显的用法是传递零个 姓名参数。在这种情况下,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-loop 中完成)。但还有一个明显的错误: 欧洲正在消失!它怎么了?答案是,如果 read到达文件末尾(在这种情况下,我们可以称之为字符串末尾)而没有在最后一个字段上遇到最后一个字段终止符,那么它返回一个失败的返回代码。这会导致 while 循环提前中断,并且丢失最后一个字段。

从技术上讲,这个错误也困扰着前面的例子; 区别在于字段分隔符被认为是 LF,当你没有指定 -d选项时这是默认值,而且 <<<(“ here-string”)机制在将其作为输入提供给命令之前自动将一个 LF 附加到字符串。因此,在这些情况下,我们通过在输入中不知不觉地附加一个附加的虚拟终止符来解决 不小心丢失 final 字段的问题。让我们把这个解决方案称为“虚拟终结者”解决方案。我们可以通过在 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]=$' Europe\n')

这种方法还揭示了由 <<<重定向操作符自动附加到 here-string 的秘密 LF。当然,可以像前面描述的那样,通过显式的修剪操作将其分离出来,但显然,手动虚拟终结器方法可以直接解决这个问题,因此我们可以直接使用这种方法。手动虚拟终结器解决方案实际上是相当方便的,因为它一次性解决了这两个问题(丢弃最终字段问题和附加 LF 问题)。

所以,总的来说,这是一个相当强大的解决方案。唯一的缺点是缺乏对多字符分隔符的支持,这个问题我将在后面讨论。


错误答案8

string='first line
second line
third line'


readarray -t lines <<<"$string"

(这实际上是从同一个职位作为 第七,答案提供了两个解决方案在同一个职位。)

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

我们看到的结果与我们从 第七中讨论的循环 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 字节。这个预处理步骤可以在进程替换中内联执行。下面是如何使用 进行操作:

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

另一种方式是:

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

现在您的元素存储在“ arr”数组中。 对元素进行迭代:

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

将字符串分割为数组的关键是 ", "的多字符分隔符。任何使用 IFS作为多字符分隔符的解决方案都是错误的,因为 IFS 是这些字符的集合,而不是字符串。

如果你分配 IFS=", ",那么字符串将在 ","或者 " "或者它们的任何组合上中断,这不是 ", "的两个字符分隔符的精确表示。

可以使用 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 中直接使用 regex 效率更高:

#!/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 分割字符串是一件非常无聊的事情。发生的情况是,我们的方法有限,只能在少数情况下工作(由“ ;”,“/”,“分割”等等)或者我们在输出中有各种各样的副作用。

下面的方法需要一些操作,但我相信它将工作的大多数我们的需要!

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

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

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

而不是更改 IFS 以匹配我们所需的分隔符 我们可以通过 ABC2将所需分隔符 ABC0的所有匹配项替换为 $IFS的内容。

但是,对于非常大的字符串,这可能会比较慢?

这是基于 Dennis Williamson 的回答。

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

正如其他人在这个帖子中指出的,OP 的问题给出了一个例子,一个以逗号分隔的字符串被解析成一个数组,但是没有说明他/她是否只对逗号分隔符、单个字符分隔符或多个字符分隔符感兴趣。

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

如果你正在寻找解决多字符分隔符问题的方法,我建议你回顾一下 Mallikarjun M的帖子,特别是来自 < a href = “ https://stackoverflow. com/users/1815797/gniourf-gniourf”> 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 中分割多字符分隔符上的字符串?


更新日期: 2022年8月3日

Xebeche 在下面的评论中提出了一个很好的观点。在回顾了他们建议的编辑之后,我已经修改了 (咒语)提供的脚本,并添加了备注,以便于理解脚本在做什么。我还将双括号[[]]更改为单括号,以实现更大的兼容性,因为许多 SHell 变体不支持双括号表示法。在这种情况下,对于 BaSH,逻辑在单括号或双括号内工作。

#!/bin/bash
  

str="LearnABCtoABCSplitABCABCaABCStringABC"
delimiter="ABC"
array=()


while [ "$str" ]; do


# parse next sub-string, left of next delimiter
substring="${str%%"$delimiter"*}"


# when substring = delimiter, truncate leading delimiter
# (i.e. pattern is "$delimiter$delimiter")
[ -z "$substring" ] && str="${str#"$delimiter"}" && continue


# create next array element with parsed substring
array+=( "$substring" )


# remaining string to the right of delimiter becomes next string to be evaluated
str="${str:${#substring}}"


# prevent infinite loop when last substring = delimiter
[ "$str" == "$delimiter" ] && break


done


declare -p array

没有评论:

#!/bin/bash
str="LearnABCtoABCSplitABCABCaABCStringABC"
delimiter="ABC"
array=()
while [ "$str" ]; do
substring="${str%%"$delimiter"*}"
[ -z "$substring" ] && str="${str#"$delimiter"}" && continue
array+=( "$substring" )
str="${str:${#substring}}"
[ "$str" == "$delimiter" ] && break
done
declare -p array

我在解析输入时看到了这篇文章,比如: 单词1,单词2,..。

以上都没有帮助我。用 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

这对我在 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" " "))

简单: -)

由于有很多方法可以解决这个问题,让我们从定义我们希望在解决方案中看到的内容开始。

  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/ *$//')


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

编辑: 我的解决方案允许逗号分隔符周围不一致的间距,同时也允许元素包含空格。几乎没有其他解决方案可以处理这些特殊情况。

我还避免使用看起来像是黑客技巧的方法,比如创建一个额外的数组元素,然后删除它。如果你不同意这是最好的答案,请留言解释。

如果您想纯粹在 Bash 中尝试相同的方法,并且使用较少的 subshell,那么这是可能的。但是结果很难阅读,这种优化可能是不必要的。

string='     Paris,France  ,   All of Europe    '
foo="${string#"${string%%[![:space:]]*}"}"
foo="${foo%"${foo##*[![:space:]]}"}"
foo="${foo//+([[:space:]]),/,}"
foo="${foo//,+([[:space:]])/,}"
readarray -t foo < <(echo "$foo")

对于多行元素,为什么不像

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


a a INTERELEMENT b b INTERELEMENT

如果您使用的是 macOS 而不能使用 readarray,那么您可以简单地这样做-

MY_STRING="string1 string2 string3"
array=($MY_STRING)

对元素进行迭代:

for element in "${array[@]}"
do
echo $element
done
#!/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")

此外,它还可以处理带有空间的扩展数据,比如“纽约”:

$ 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!

下面是一个简单的 bash 一行程序:

read -a my_array <<< $(echo ${INPUT_STRING} | tr -d ' ' | tr ',' ' ')

我很好奇“正确答案”的相对表现 在@bgoldst 的流行答案中,它明显地谴责了循环, 所以我对三个纯 bash 实现做了一个简单的基准测试。

总之,我建议:

  1. 对于字符串长度 < 4k 左右,纯 bash 比 gawk 快
  2. 对于分隔符长度 < 10和字符串长度 < 256k,纯 bash 与 gawk 相当
  3. 对于分隔符长度 > 10和字符串长度 < 64k 左右的情况,纯 bash 是“可接受的”; 而 Gawk 的速度不到5倍
  4. 对于字符串长度 < 512k 左右,gawk 是“可接受的”

我武断地将“可接受的”定义为“需要 < 0.5 s 来分割字符串”。


我认为问题在于使用任意长度的分隔符字符串(而不是 regex)将 bash 字符串拆分为 bash 数组。

# in: $1=delim, $2=string
# out: sets array a

我的纯 bash 实现是:

# naive approach - slow
split_byStr_bash_naive(){
a=()
local prev=""
local cdr="$2"
[[ -z "${cdr}" ]] && a+=("")
while [[ "$cdr" != "$prev" ]]; do
prev="$cdr"
a+=( "${cdr%%"$1"*}" )
cdr="${cdr#*"$1"}"
done
# echo $( declare -p a | md5sum; declare -p a )
}
# use lengths wherever possible - faster
split_byStr_bash_faster(){
a=()
local car=""
local cdr="$2"
while
car="${cdr%%"$1"*}"
a+=("$car")
cdr="${cdr:${#car}}"
(( ${#cdr} ))
do
cdr="${cdr:${#1}}"
done
# echo $( declare -p a | md5sum; declare -p a )
}
# use pattern substitution and readarray - fastest
split_byStr_bash_sub(){
a=()
local delim="$1" string="$2"


delim="${delim//=/=-}"
delim="${delim//$'\n'/=n}"


string="${string//=/=-}"
string="${string//$'\n'/=n}"


readarray -td $'\n' a <<<"${string//"$delim"/$'\n'}"


local len=${#a[@]} i s
for (( i=0; i<len; i++ )); do
s="${a[$i]//=n/$'\n'}"
a[$i]="${s//=-/=}"
done
# echo $( declare -p a | md5sum; declare -p a )
}

初始版本中的初始 -z测试处理长度为零的情况 如果没有测试,输出数组是空的; 使用它,数组只有一个零长度的元素。

while read代替 readarray,减速 < 10% 。


这是我使用的笨拙的实现:

split_byRE_gawk(){
readarray -td '' a < <(awk '{gsub(/'"$1"'/,"\0")}1' <<<"$2$1")
unset 'a[-1]'
# echo $( declare -p a | md5sum; declare -p a )
}

显然,在一般情况下,delim 参数需要清理, 因为 gawk 期望使用正则表达式,而 gawk 特殊字符可能会导致问题。 此外,实现不能正确处理分隔符中的换行符。

由于 gawk 被使用,一个通用的版本处理更多的任意 分界线可以是:

split_byREorStr_gawk(){
local delim=$1
local string=$2
local useRegex=${3:+1}  # if set, delimiter is regex


readarray -td '' a < <(
export delim
gawk -v re="$useRegex" '
BEGIN {
RS = FS = "\0"
ORS = ""
d = ENVIRON["delim"]


# cf. https://stackoverflow.com/a/37039138
if (!re) gsub(/[\\.^$(){}\[\]|*+?]/,"\\\\&",d)
}
gsub(d"|\n$","\0")
' <<<"$string"
)
# echo $( declare -p a | md5sum; declare -p a )
}

或者在佩尔也有同样的想法:

split_byREorStr_perl(){
local delim=$1
local string=$2
local regex=$3  # if set, delimiter is regex


readarray -td '' a < <(
export delim regex
perl -0777pe '
$d = $ENV{delim};
$d = "\Q$d\E" if ! $ENV{regex};
s/$d|\n$/\0/g;
' <<<"$string"
)
# echo $( declare -p a | md5sum; declare -p a )
}

实现产生相同的输出,通过分别比较 md5sum 进行测试。

注意,如果输入不明确(如@bgoldst 所说,“逻辑上不正确”) , 例如,使用分隔符 --和字符串 a-a---:

  • @ goldst’s code return: declare -a a=([0]="a") or declare -a a=([0]="a" [1]="")
  • 我的回报: declare -a a=([0]="a-")declare -a a=([0]="a" [1]="-")

参数是通过简单的 Perl 脚本从以下地方派生出来的:

delim="-=-="
base="ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"

下面是3种不同类型的计时结果(以秒为单位)表 字符串和分隔符参数。

  • 字符串参数的长度
  • 外延参数的长度
  • 性能收支平衡点
  • !-“可接受的”性能限制(bash)大约在这里
  • !!-“可接受的”性能限制(gawk)在这里的某个地方
  • -功能花了太长时间
  • 运行 <!>-gawk 命令失败

类别1

d=$(perl -e "print( '$delim' x (7*2**$n) )")
s=$(perl -e "print( '$delim' x (7*2**$n) . '$base' x (7*2**$n) )")
N # s # D 发呆 B _ sub 再快点 太天真了
0 252 28 0.002 0.000 0.000 0.000
1 504 56 0.005 0.000 0.000 0.001
2 1008 112 0.005 0.001 0.000 0.003
3 二零一六年 224 0.006 0.001 0.000 0.009
4 4032 448 0.007 0.002 0.001 0.048
= 5 8064 896 0.014 0.008 0.005 0.377
6 16128 1792年 0.018 0.029 0.017 (2.214)
7 32256 3584 0.033 0.057 0.039 (15.16)
8 64512 7168 0.063 0.214 0.128 -
9 129024 14336 0.111 (0.826) (0.602) -
10 258048 28672 0.214 (3.383) (2.652) -
!! 11 516096 57344 0.430 (13.46) (11.00) -
12 1032192 114688 (0.834) (58.38) - -
13 2064384 229376 < ! > (228.9) - -

第二类

d=$(perl -e "print( '$delim' x ($n) )")
s=$(perl -e "print( ('$delim' x ($n) . '$base' x $n ) x (2**($n-1)) )")
N # s # D 发呆 B _ sub 再快点 太天真了
0 0 0 0.003 0.000 0.000 0.000
1 36 4 0.003 0.000 0.000 0.000
2 144 8 0.005 0.000 0.000 0.000
3 432 12 0.005 0.000 0.000 0.000
4 1152 16 0.005 0.001 0.001 0.002
5 2880 20 0.005 0.001 0.002 0.003
6 6912 24 0.006 0.003 0.009 0.014
= 7 16128 28 0.012 0.012 0.037 0.044
8 36864 32 0.023 0.044 0.167 0.187
9 82944 36 0.049 0.192 (0.753) (0.840)
10 184320 40 0.097 (0.925) (3.682) (4.016)
11 405504 44 0.204 (4.709) (18.00) (19.58)
!! 12 884736 48 0.444 (22.17) - -
13 1916928 52 (1.019) (102.4) - -

第三类

d=$(perl -e "print( '$delim' x (2**($n-1)) )")
s=$(perl -e "print( ('$delim' x (2**($n-1)) . '$base' x (2**($n-1)) ) x ($n) )")
N # s # D 发呆 B _ sub 再快点 太天真了
0 0 0 0.000 0.000 0.000 0.000
1 36 4 0.004 0.000 0.000 0.000
2 144 8 0.003 0.000 0.000 0.000
3 432 16 0.003 0.000 0.000 0.000
4 1152 32 0.005 0.001 0.001 0.002
5 2880 64 0.005 0.002 0.001 0.003
6 6912 128 0.006 0.003 0.003 0.014
= 7 16128 256 0.012 0.011 0.010 0.077
8 36864 512 0.023 0.046 0.046 (0.513)
9 82944 1024 0.049 0.195 0.197 (3.850)
10 184320 2048年 0.103 (0.951) (1.061) (31.84)
11 405504 4096 0.222 (4.796) - -
!! 12 884736 8192 0.473 (22.88) - -
13 1916928 16384 (1.126) (105.4) - -

分隔符长度摘要1.10

由于短期分隔符可能比长期分隔符更可能出现, 下面总结的是不同分隔符长度的结果 在1到10之间(2.9的结果大多被省略为非常相似)。

s1=$(perl -e "print( '$d' . '$base' x (7*2**$n) )")
s2=$(perl -e "print( ('$d' . '$base' x $n ) x (2**($n-1)) )")
s3=$(perl -e "print( ('$d' . '$base' x (2**($n-1)) ) x ($n) )")

Bash _ sub < gawk

绳子 N # s # D 发呆 B _ sub 再快点 太天真了
中一 10 229377 1 0.131 0.089 1.709 -
中一 10 229386 10 0.142 0.095 1.907 -
第2条 8 32896 1 0.022 0.007 0.148 0.168
第2条 8 34048 10 0.021 0.021 0.163 0.179
中三 12 786444 1 0.436 0.468 - -
中三 12 786456 2 0.434 0.317 - -
中三 12 786552 10 0.438 0.333 - -

Bash _ sub < 0.5 s

绳子 N # s # D 发呆 B _ sub 再快点 太天真了
中一 11 458753 1 0.256 0.332 (7.089) -
中一 11 458762 10 0.269 0.387 (8.003) -
第2条 11 361472 1 0.205 0.283 (14.54) -
第2条 11 363520 3 0.207 0.462 (16.66) -
中三 12 786444 1 0.436 0.468 - -
中三 12 786456 2 0.434 0.317 - -
中三 12 786552 10 0.438 0.333 - -

Gawk < 0.5 s

绳子 N # s $D 发呆 B _ sub 再快点 太天真了
中一 11 458753 1 0.256 0.332 (7.089) -
中一 11 458762 10 0.269 0.387 (8.003) -
第2条 12 788480 1 0.440 (1.252) - -
第2条 12 806912 10 0.449 (4.968) - -
中三 12 786444 1 0.436 0.468 - -
中三 12 786456 2 0.434 0.317 - -
中三 12 786552 10 0.438 0.333 - -

(我不完全确定为什么 s160k 和 d = 1的 bash _ sub 总是比 s3的 d > 1慢。)

所有测试都是在运行 xubuntu 20.04的 Intel i7-7500U 上使用 bash 5.0.17进行的。