Contents

bash中读取命令输出到数组

1. 概述

当我们编写 shell 脚本时,我们经常调用一个命令并将输出保存到一个变量中以供进一步处理。有时,我们希望将多行输出保存到 Bash 数组中。

在本教程中,我们将讨论执行此操作的一些常见陷阱,并说明如何以正确的方式执行此操作。

2. 常见的陷阱

首先,让我们定义我们的问题。我们将执行一个命令并将其多行输出保存到一个 Bash 数组中。每行都应该是数组的一个元素。

乍一看,问题看起来很简单。我们可以在括号之间放置一个命令替换 来初始化一个数组:

my_array=( $(command) )

我们以seq 命令为例,试试上面的方法是否有效:

$ seq 5
1
2
3
4
5
$ my_array=( $(seq 5) )
$ declare -p my_array
declare -a my_array=([0]="1" [1]="2" [2]="3" [3]="4" [4]="5")

太好了,它有效!

我们使用带有 -p选项的 Bash 内置声明来检查数组。它表明数组已经按照我们的预期进行了初始化。 嗯,到目前为止,一切都很好。然而,这不是一个稳定的解决方案。让我们看看它有什么问题。

2.1. 输出可能包含空格

命令的输出通常可以包含空格。让我们稍微改变一下seq命令并检查我们的解决方案是否仍然有效:

$ seq -f 'Num %g' 5
Num 1
Num 2
Num 3
Num 4
Num 5
$ my_array=( $(seq -f 'Num %g' 5) )
$ declare -p my_array
declare -a my_array=([0]="Num" [1]="1" [2]="Num" [3]="2" [4]="Num" [5]="3" [6]="Num" [7]="4" [8]="Num" [9]="5")

输出中的空格打破了我们的解决方案。上面的输出告诉我们,my_array现在有十个元素,而不是五个。

修复可能会立即浮现在脑海中:将IFS 设置为换行符,以便可以将整行分配给数组元素。

让我们试试它是否可以解决问题:

$ IFS=$'\n'
$ my_array=( $(seq -f 'Num %g' 5) )
$ declare -p my_array
declare -a my_array=([0]="Num 1" [1]="Num 2" [2]="Num 3" [3]="Num 4" [4]="Num 5")

是的!那解决了它!

不幸的是,即使它正确处理了空格,该解决方案仍然很脆弱。

让我们看看它还有什么问题。

2.2. 输出可能包含通配符

命令的某些输出可能包含通配符 ,例如 *、*[…]*或 ? 等。

让我们再次更改seq命令并在我们的工作目录下创建几个文件:

$ seq -f 'Num*%g' 5
Num*1
Num*2
Num*3
Num*4
Num*5
$ touch Number.app.log.{4..5}
$ ls -1
Number.app.log.4
Number.app.log.5

现在,让我们检查一下我们的解决方案是否仍然可以正确地将输出转换为数组:

$ my_array=( $(seq -f 'Num*%g' 5) )
$ declare -p my_array
declare -a my_array=([0]="Num*1" [1]="Num*2" [2]="Num*3" [3]="Number.app.log.4" [4]="Number.app.log.5")

哎呀!最后两个元素由两个文件名填充,而不是预期的“ Num*4”和 “Num*5”。这是因为如果通配符匹配我们工作目录中的某些文件名,则将选择文件名而不是原始字符串。

好吧,我们可以通过set -f快速修复禁用文件名通配。但是,通过更改IFSset -f 来修复脆弱的技术并不明智。

接下来,我们来看看更合适的解决问题的方法。

3. 使用readarray命令

** readarray是一个内置的 Bash 命令。**它是在 Bash 版本 4 中引入的。

我们可以使用内置的readarray来解决这个问题:

$ readarray -t my_array < <(seq 5)
$ declare -p my_array
declare -a my_array=([0]="1" [1]="2" [2]="3" [3]="4" [4]="5")
$ readarray -t my_array < <(seq -f 'Num %g' 5)
$ declare -p my_array
declare -a my_array=([0]="Num 1" [1]="Num 2" [2]="Num 3" [3]="Num 4" [4]="Num 5")
$ ls -1
Number.app.log.4
Number.app.log.5
$ readarray -t my_array < <(seq -f 'Num*%g' 5)
$ declare -p my_array
declare -a my_array=([0]="Num*1" [1]="Num*2" [2]="Num*3" [3]="Num*4" [4]="Num*5")

上面的输出**表明readarray -t my_array < <(COMMAND)总是可以正确地将COMMAND的输出转换为my_array。**无论COMMAND输出是否包含空格或通配符,这都有效。

现在,让我们了解它为什么起作用。

** readarray将标准输入中的行读取到数组变量中:my_array-t选项将从每行中删除尾随的换行符。**

我们使用*< <(COMMAND)技巧将 COMMAND输出重定向到标准输入。< (COMMAND)称为进程替换 。它使COMMAND的输出看起来像一个文件。然后,我们使用< FILE*将文件重定向到标准输入。

因此,  readarray命令可以读取 COMMAND的输出并将其保存到我们的my_array。

4. 使用 read命令

我们已经看到,通过使用readarray命令,我们可以很方便地解决这个问题。由于readarray命令是在 Bash 版本 4 中引入的,因此如果我们使用的是较旧的 Bash 版本,则它不可用。

Bash shell 有另一个内置命令:  read,它从标准输入中读取一行文本并将其拆分为单词。

我们可以使用read命令解决这个问题:

IFS=$'\n' read -r -d '' -a my_array < <( COMMAND && printf '\0' )

让我们对其进行测试,看看它是否适用于不同的情况:

$ IFS=$'\n' read -r -d '' -a my_array < <( seq 5 && printf '\0' )
$ declare -p my_array
declare -a my_array=([0]="1" [1]="2" [2]="3" [3]="4" [4]="5")
$ IFS=$'\n' read -r -d '' -a my_array < <( seq -f 'Num %g' 5 && printf '\0' )
$ declare -p my_array
declare -a my_array=([0]="Num 1" [1]="Num 2" [2]="Num 3" [3]="Num 4" [4]="Num 5")
$ IFS=$'\n' read -r -d '' -a my_array < <( seq -f 'Num*%g' 5 && printf '\0' )
$ declare -p my_array
declare -a my_array=([0]="Num*1" [1]="Num*2" [2]="Num*3" [3]="Num*4" [4]="Num*5")

输出显示它也适用于我们的示例。该命令看起来比readarray长一点,但也不难理解。 让我们分解它来解释它的作用:

  • COMMAND && printf ‘\0’:这里我们将一个空字节*’\0’ 附加到**COMMAND*的输出中,以便稍后 读取将在此处停止读取
  • < <(COMMAND && printf ‘\0’):这对我们来说并不新鲜。我们将COMMAND输出与尾随的空字节一起重定向到标准输入
  • IFS=$’\n’:我们也学到了这一点,我们将IFS设置为换行符,以便读取命令将从流中读取整行
  • read -r: -r选项告诉 读取命令不要将反斜杠解释为转义序列
  • -d ”:我们让 读取命令在空字节处停止读取
  • -a my_array:这很简单,我们告诉 读取命令在读取时填充 数组my_array

值得一提的是,  IFS变量更改只会为 read语句设置变量。它不会干扰当前的 shell 环境。