如何将“查找”命令结果存储为 Bash 中的数组

Posted

技术标签:

【中文标题】如何将“查找”命令结果存储为 Bash 中的数组【英文标题】:How can I store the "find" command results as an array in Bash 【发布时间】:2014-06-14 22:13:01 【问题描述】:

我正在尝试将find 的结果保存为数组。 这是我的代码:

#!/bin/bash

echo "input : "
read input

echo "searching file with this pattern '$input' under present directory"
array=`find . -name $input`

len=$#array[*]
echo "found : $len"

i=0

while [ $i -lt $len ]
do
echo $array[$i]
let i++
done

我在当前目录下获得 2 个 .txt 文件。 所以我期望'2'作为$len的结果。但是,它打印 1。 原因是它将find 的所有结果作为一个元素。 我该如何解决这个问题?

附言 我在 *** 上找到了几个关于类似问题的解决方案。但是,它们有点不同,所以我不能申请我的情况。我需要在循环之前将结果存储在一个变量中。再次感谢。

【问题讨论】:

【参考方案1】:

适用于 Linux 用户的 2020 年更新:

如果您有最新版本的 bash(4.4-alpha 或更高版本),就像您在 Linux 上可能那样,那么您应该使用Benjamin W.'s answer。

如果您使用的是 Mac OS(我上次检查过)仍然使用 bash 3.2,或者使用的是较旧的 bash,请继续阅读下一节。

回答 bash 4.3 或更早版本

这是将find 的输出放入bash 数组的一种解决方案:

array=()
while IFS=  read -r -d $'\0'; do
    array+=("$REPLY")
done < <(find . -name "$input" -print0)

这很棘手,因为通常文件名可以包含空格、换行符和其他脚本敌对字符。使用find 并使文件名彼此安全分隔的唯一方法是使用-print0,它会打印以空字符分隔的文件名。如果 bash 的 readarray/mapfile 函数支持空分隔字符串但它们不支持,这不会带来太大的不便。 Bash 的 read 确实如此,这将我们引向上面的循环。

[此答案最初写于 2014 年。如果您有最新版本的 bash,请查看下面的更新。]

工作原理

    第一行创建一个空数组:array=()

    每次执行read 语句时,都会从标准输入中读取一个以空值分隔的文件名。 -r 选项告诉read 单独留下反斜杠字符。 -d $'\0' 告诉 read 输入将以空值分隔。由于我们省略了read 的名称,因此shell 将输入放入默认名称:REPLY

    array+=("$REPLY") 语句将新文件名附加到数组array

    最后一行结合了重定向和命令替换,将find 的输出提供给while 循环的标准输入。

为什么要使用进程替换?

如果我们不使用进程替换,循环可以写成:

array=()
find . -name "$input" -print0 >tmpfile
while IFS=  read -r -d $'\0'; do
    array+=("$REPLY")
done <tmpfile
rm -f tmpfile

在上面find 的输出存储在一个临时文件中,该文件用作while 循环的标准输入。进程替换的想法是使此类临时文件变得不必要。所以,不是让while 循环从tmpfile 获取它的标准输入,我们可以让它从&lt;(find . -name $input -print0) 获取它的标准输入。

进程替换非常有用。在命令想要从文件读取的许多地方,您可以指定进程替换&lt;(...),而不是文件名。有一个类似的形式,&gt;(...),可以用来代替命令要写入到文件的文件名。

与数组一样,进程替换是 bash 和其他高级 shell 的一项功能。它不是 POSIX 标准的一部分。

替代方案:lastpipe

如果需要,可以使用lastpipe 代替进程替换(帽子提示:Caesar):

set +m
shopt -s lastpipe
array=()
find . -name "$input" -print0 | while IFS=  read -r -d $'\0'; do array+=("$REPLY"); done; declare -p array

shopt -s lastpipe 告诉 bash 在当前 shell(而不是后台)中运行管道中的最后一个命令。这样,array 在管道完成后仍然存在。因为lastpipe只有在job control关闭的情况下才会生效,所以我们运行set +m。 (在脚本中,与命令行相反,作业控制默认关闭。)

补充说明

以下命令创建一个 shell 变量,而不是一个 shell 数组:

array=`find . -name "$input"`

如果你想创建一个数组,你需要在 find 的输出周围加上括号。所以,天真地,一个人可以:

array=(`find . -name "$input"`)  # don't do this

问题是shell对find的结果进行了分词,所以不能保证数组的元素就是你想要的。

2019 年更新

从 4.4-alpha 版本开始,bash 现在支持 -d 选项,因此不再需要上述循环。相反,可以使用:

mapfile -d $'\0' array < <(find . -name "$input" -print0)

有关这方面的更多信息,请参阅(并投票)Benjamin W.'s answer。

【讨论】:

@JuneyoungOh 很高兴它有帮助。我添加了一段流程替换。 @Rockallite 这是一个很好的观察,但不完整。虽然我们确实不会拆分成多个单词,但我们仍然需要IFS= 以避免从输入行的开头或结尾删除空格。您可以通过将read var &lt;&lt;&lt;' abc '; echo "&gt;$var&lt;" 的输出与IFS= read var &lt;&lt;&lt;' abc '; echo "&gt;$var&lt;" 的输出进行比较来轻松测试这一点。在前一种情况下,abc 前后的空格被删除。在后者中,它们不是。以空格开头或结尾的文件名可能不常见,但如果它们存在,我们希望它们得到正确处理。 嗨,在我执行你的代码后,我在意外令牌&lt;' done 附近收到消息语法错误 注意:可以用更简单的''代替$'\0'n=0; while IFS= read -r -d '' line || [ "$line" ]; do echo "$((++n)):$line"; done &lt; &lt;(printf 'first\nstill first\0second\0third') @theeagle 我假设您打算写BLAH=$(find . -name '*.php')。正如答案中所讨论的,这种方法将在有限的情况下工作,但它一般不适用于所有文件名,并且它不会像 OP 预期的那样产生 array.【参考方案2】:

你可以这样做:

#!/bin/bash
echo "input : "
read input

echo "searching file with this pattern '$input' under present directory"
array=(`find . -name '*'$input'*'`)

for i in "$array[@]"
do :
    echo $i
done

【讨论】:

谢谢。很多。但正如@anishsane 指出的那样,在我的程序中应该考虑文件名中的空格。无论如何谢谢!【参考方案3】:

如果您使用的是bash 4 或更高版本,您可以将find 的使用替换为

shopt -s globstar nullglob
array=( **/*"$input"* )

globstar 启用的 ** 模式匹配 0 个或多个目录,允许模式匹配当前目录中的任意深度。如果没有 nullglob 选项,模式(在参数扩展之后)将按字面意思处理,因此如果没有匹配项,您将拥有一个包含单个字符串而不是空数组的数组。

如果你想遍历隐藏目录(如.ssh)并匹配隐藏文件(如.bashrc),也将dotglob选项添加到第一行。

【讨论】:

也可以nullglob... 是的,我总是忘记这一点。 请注意,这不会包括隐藏的文件和目录,除非设置了dotglob(这可能需要也可能不需要,但也值得一提)。【参考方案4】:

你可以尝试类似

array=(`find . -type f | sort -r | head -2`)
,为了打印数组值,你可以尝试类似 echo "$array[*]"

【讨论】:

如果文件名包含空格或全局字符,则中断。【参考方案5】:

在 bash 中,$(&lt;any_shell_cmd&gt;) 有助于运行命令并捕获输出。将其传递给 IFS 并使用 \n 作为分隔符有助于将其转换为数组。

IFS='\n' read -r -a txt_files <<< $(find /path/to/dir -name "*.txt")

【讨论】:

这只会将find的结果的第一个文件放入数组中。【参考方案6】:

Bash 4.4 为 readarray/mapfile 引入了 -d 选项,因此现在可以使用以下方法解决此问题

readarray -d '' array < <(find . -name "$input" -print0)

适用于任意文件名的方法,包括空格、换行符和通配符。这要求您的 find 支持 -print0,例如 GNU find 所做的那样。

来自manual(省略其他选项):

mapfile [-d <i>delim</i>] [<i>array</i>]

-ddelim 的第一个字符用于终止每个输入行,而不是换行符。如果 delim 是空字符串,mapfile 将在读取 NUL 字符时终止一行。

readarray 只是mapfile 的同义词。

【讨论】:

【参考方案7】:

以下内容似乎适用于 macOS 上的 Bash 和 Z Shell。

#! /bin/sh

IFS=$'\n'
paths=($(find . -name "foo"))
unset IFS

printf "%s\n" "$paths[@]"

【讨论】:

这适用于包含空格和其他特殊字符的文件,但在文件名称中包含换行符的(非常罕见的)情况下会失败。您可以使用printf "%b" "file name with spaces, a star * ...\012and a second line\0" | xargs -0 touch 为测试创建一个 也许我在这里遗漏了一些东西,但这对于 99% 的案例来说似乎是更清晰、更简单的解决方案 绝对适用于 macOS Big Sur 上的 zsh :) 谢谢! - 但我也知道我的文件集没有带有换行符的名称,因为谁这样做?我从来没有在野外见过一个,我制作了文件,所以我知道这不是问题。【参考方案8】:

这些解决方案都不适合我,因为我不想学习 readarray 和 mapfile。这是我想出的。

#!/bin/bash

echo "input : "
read input

echo "searching file with this pattern '$input' under present directory"
# The only change is here. Append to array for each non-empty line.
array=()
while read line; do
    [[ ! -z "$line" ]] && array+=("$line")
done; <<< $(find . -name $input -print)

len=$#array[@]
echo "found : $len"

i=0

while [ $i -lt $len ]
do
echo $array[$i]
let i++
done

【讨论】:

以上是关于如何将“查找”命令结果存储为 Bash 中的数组的主要内容,如果未能解决你的问题,请参考以下文章

可恶的bash脚本的执行结

使用 jq 和 bash 为数组中的每个对象运行命令

argparse 处理 bash 命令中的字符串和空格

如何在bash脚本中执行存储在heredoc中的curl命令?

用于查找最大子数组的分而治之算法 - 如何同时提供结果子数组索引?

将空格分隔的字符串读入 Bash 中的数组