如何将“查找”命令结果存储为 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
获取它的标准输入,我们可以让它从<(find . -name $input -print0)
获取它的标准输入。
进程替换非常有用。在命令想要从文件读取的许多地方,您可以指定进程替换<(...)
,而不是文件名。有一个类似的形式,>(...)
,可以用来代替命令要写入到文件的文件名。
与数组一样,进程替换是 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 <<<' abc '; echo ">$var<"
的输出与IFS= read var <<<' abc '; echo ">$var<"
的输出进行比较来轻松测试这一点。在前一种情况下,abc
前后的空格被删除。在后者中,它们不是。以空格开头或结尾的文件名可能不常见,但如果它们存在,我们希望它们得到正确处理。
嗨,在我执行你的代码后,我在意外令牌<'
done 附近收到消息语法错误
注意:可以用更简单的''
代替$'\0'
:n=0; while IFS= read -r -d '' line || [ "$line" ]; do echo "$((++n)):$line"; done < <(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 中,$(<any_shell_cmd>)
有助于运行命令并捕获输出。将其传递给 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>]
-d
delim
的第一个字符用于终止每个输入行,而不是换行符。如果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脚本中执行存储在heredoc中的curl命令?