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


96

我试图将结果保存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作为一个元素。我怎样才能解决这个问题?

PS
我发现了几个解决方案,在计算器上有关类似问题。但是,它们有些不同,因此我无法申请。我需要在循环之前将结果存储在变量中。再次感谢。

Answers:


137

Linux用户的2020更新:

如果您具有bash的最新版本(4.4-alpha或更高版本)(如在Linux上一样),则应使用Benjamin W.的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,请参阅下面的更新。]

这个怎么运作

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

  2. 每次read执行该语句时,都会从标准输入中读取以空分隔的文件名。该-r选项告诉read您保留反斜杠字符。该-d $'\0'告诉read输入将以空分隔。由于我们省略了名称read,因此外壳程序将输入内容放入默认名称:中REPLY

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

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

为什么要使用流程替代?

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

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仅在关闭作业控制后才会生效,所以我们运行set +m。(在脚本中,相对于命令行,默认情况下,作业控制处于关闭状态。)

补充说明

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

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

如果要创建一个数组,则需要将括号放在find的输出周围。因此,天真的,一个人可以:

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

问题在于外壳程序对的结果执行单词拆分,find因此不能保证数组的元素是您想要的。

更新2019

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

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

有关此的更多信息,请参阅(并赞扬)Benjamin W.的答案


1
@JuneyoungOh很高兴提供了帮助。我添加了一部分流程替换。
John1024

3
@Rockallite这是一个很好的观察,但是不完整。尽管确实没有将我们分成多个单词,但仍然需要IFS=避免从输入行的开头或结尾删除空格。您可以将的输出read var <<<' abc '; echo ">$var<"与的输出进行比较,从而轻松地进行测试 IFS= read var <<<' abc '; echo ">$var<"。在前一种情况下,将abc删除之前和之后的空格。在后者中则不是。以空格开头或结尾的文件名可能不常见,但是,如果存在,我们希望它们正确处理。
John1024 '17

1
嗨,我执行您的代码后,收到意外令牌附近的消息语法错误<' <<(找到aaa /-不是-newermt“ $ last_build_timestamp_v” -type f -print0)'
PrzemysławSienkiewicz

1
注意:''可以使用更简单的方法代替$'\0'n=0; while IFS= read -r -d '' line || [ "$line" ]; do echo "$((++n)):$line"; done < <(printf 'first\nstill first\0second\0third')
glenn jackman

1
@theeagle我以为你打算写BLAH=$(find . -name '*.php')。正如答案中所讨论的那样,该方法仅在有限的情况下适用,但通常不适用于所有文件名,并且不会产生OP所期望的array
John1024 '19

36

Bash 4.4-dreadarray/引入了一个选项mapfile,因此现在可以通过以下方式解决

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

适用于使用任意文件名(包括空格,换行符和通配符)的方法。这需要您的find支持-print0,例如GNU find。

手册中(省略其他选项):

mapfile [-d delim] [array]

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

并且readarray只是的同义词mapfile


18

如果您使用bash4或更高版本,您可以取代你使用的find

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

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

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


4
也许nullglob也是…
kojiro 2014年

1
是的,我总是忘记这一点。
chepner 2014年

5
请注意,除非dotglob已设置,否则这将不包括隐藏的文件和目录(可能需要也可能不需要,但这也值得一提)。
gniourf_gniourf 2014年

10

你可以尝试像

array=(`find . -type f | sort -r | head -2`)
,并且为了打印数组值,您可以尝试像echo这样的东西。 "${array[*]}"


8
如果文件名带有空格或glob字符,则中断。
gniourf_gniourf

2

以下内容似乎适用于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%的情况,这似乎是一种更清晰,更轻松的解决方案
Matt Korostoff

-1

在bash中,$(<any_shell_cmd>)有助于运行命令并捕获输出。将IFS\n作为定界符传递给它有助于将其转换为数组。

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

4
这只会将结果的第一个文件find放入数组。
本杰明·

-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

1
谢谢。很多。但是正如@anishsane所指出的,在我的程序中应考虑文件名中的空格。总之感谢!
Juneyoung,2014年

-3

对我来说,这在cygwin上效果很好:

declare -a names=$(echo "("; find <path> <other options> -printf '"%p" '; echo ")")
for nm in "${names[@]}"
do
    echo "$nm"
done

这适用于空格,但不适用于目录名称中的双引号(“)(无论如何在Windows环境中是不允许的)。

当心-printf选项中的空间。


3
损坏和危险:不会处理引号,并且会受到任意代码注入的影响。不使用。
gniourf_gniourf

2
似乎有人标记了此帖子以将其删除。“错了”不是删除SO的原因。用户尝试回答,它是主题,并且满足回答的条件。下投票按钮用于衡量有用性和正确性,而不是删除按钮。
Frambot

3
正如gniourf所指出的,它不适用于其他人在您的系统上输入选项的环境,例如网页。但并非每个人都为该环境编程。我用它来重命名目录中的文件。
R Risack '19
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.