在Bash中测试一个glob是否有匹配

326
如果我想检查一个文件是否存在,可以使用test -e filename[ -e filename ]进行测试。
如果我有一个通配符(glob),并且想知道是否存在任何与该通配符匹配的文件。这个通配符可能匹配0个文件(在这种情况下我不需要执行任何操作),或者它可能匹配1个或多个文件(在这种情况下我需要执行某些操作)。如何测试一个通配符是否有任何匹配?(我不关心有多少匹配项,最好只用一个if语句而不需要循环(因为我认为这样更可读))。
(如果通配符匹配超过一个文件,则test -e glob*会失败。)

5
我怀疑我的答案在某种程度上是“显然正确”的,其他所有答案都有点不完整。这是一个一行的shell内置命令,已经存在了很长时间,似乎是“这个特定工具的预期工具”。我担心用户会错误地引用这里接受的答案。请随意纠正我,我会撤回我的评论,我非常乐意犯错并从中学习。如果差异没有显得如此明显,我就不会提出这个问题。 - Brian Chrisman
1
我最喜欢的解决此问题的方法是find命令,它适用于任何shell(甚至非Bourne shell),但需要GNU find,以及compgen命令,显然是Bashism。太遗憾了,我不能接受两个答案。 - Ken Bloom
1
注意:自问题发布以来,此问题已被编辑。原始标题为“在bash中测试glob是否有任何匹配项”。在我发布答案后,特定的shell“bash”被从问题中删除。问题标题的编辑使我的答案看起来是错误的。我希望有人可以修改或至少解决这个变化。 - Brian Chrisman
在这里添加一条注释,即“glob”是“通配符”的同义词,以防人们搜索第二个术语。 - tripleee
23个回答

279

Bash 特定解决方案:

compgen -G "<glob-pattern>"

避免使用模式匹配,否则它会被预先扩展为匹配项。

退出状态如下:

  • 无匹配项时为1,
  • '一个或多个匹配项' 时为0

stdout与glob匹配的文件列表。我认为这是在简洁性和最小化潜在副作用方面最好的选择。

例如:

if compgen -G "/tmp/someFiles*" > /dev/null; then
    echo "Some files exist."
fi

20
请注意,compgen是_Bash_特定的内置命令,不属于 POSIX 标准 Unix shell 指定的内置命令。因此,在编写需要可移植性到其他 shells 的脚本时,请避免使用它。 - Diomidis Spinellis
6
在没有bash内置命令的情况下,我认为可以使用任何基于通配符并在没有匹配文件时失败的其他命令来实现类似的效果,比如ls命令: if ls /tmp/*Files 2>&1 >/dev/null; then echo exists; fi - 这对代码高尔夫可能有用。如果存在与通配符同名但不应该匹配的文件,则会失败,但如果出现这种情况,你可能会有更大的问题。 - Dewi Morgan
9
这是更简单的写法:if ls /tmp/*Files &> /dev/null; then echo exists; fi。意思是,如果 /tmp/ 目录下存在以 Files 结尾的文件,则输出 exists - Clay Bridges
3
好的,引用它或者文件名通配符将被预先扩展。使用compgen命令时,需要加上引号,如"dir/*.ext"。 - Brian Chrisman
3
@DewiMorgan:你的建议是:如果ls /tmp/*Files 2>&1 >/dev/null;那么echo exists;fi基本上是不错的,但重定向无法按照所写的方式工作,你需要:如果ls /tmp/*Files >/dev/null 2>&1;那么echo exists;fi。 - John Vincent
显示剩余10条评论

209

nullglob shell选项确实是bashism。

为了避免需要繁琐的保存和恢复nullglob状态,我只会在扩展glob的子shell中设置它:

if test -n "$(shopt -s nullglob; echo glob*)"
then
    echo found
else
    echo not found
fi

为了更好的可移植性和更灵活的文件匹配,请使用 find 命令:

if test -n "$(find . -maxdepth 1 -name 'glob*' -print -quit)"
then
    echo found
else
    echo not found
fi

为了让 find 尽快找到符合搜索条件的第一个文件并退出,明确使用 -print -quit 操作代替默认的隐式 -print 操作。当匹配的文件很多时,这样做比 echo glob*ls glob* 更快,并且还避免了扩展命令行的可能性(一些 shell 的长度限制为 4K)。

如果认为 find 太过复杂,而且可能匹配的文件数量很少,可以使用 stat 命令:

if stat -t glob* >/dev/null 2>&1
then
    echo found
else
    echo not found
fi

12
find似乎完全正确。它没有边角情况,因为shell没有执行扩展(将未扩展的glob传递给其他命令),它在各种shell之间是可移植的(虽然你使用的选项显然不都是由POSIX指定的),而且比ls -d glob*(先前接受的答案)更快,因为它在找到第一个匹配项后停止搜索。 - Ken Bloom
1
请注意,此答案可能需要 shopt -u failglob,因为这些选项似乎会产生冲突。 - Calimo
“find”解决方案也将匹配没有通配符的文件名。在这种情况下,这正是我想要的。但需要注意的是。 - We Are All Monica
1
https://unix.stackexchange.com/questions/275637/limit-posix-find-to-specific-depth 讨论了如何替换 POSIX find 的 -maxdepth 选项。 - Ken Bloom
1
stat-t 选项不具备可移植性。在 BSD 上,它用于格式化时间戳显示。 - Mark Adler
显示剩余3条评论

33

我喜欢

exists() {
    [ -e "$1" ]
}

if exists glob*; then
    echo found
else
    echo not found
fi

这种方法既易于阅读,又高效(除非有大量文件)。主要缺点是它比看起来更加微妙,有时我会觉得需要添加一个长注释。
如果匹配成功,"glob*" 将被 shell 展开并传递给 exists(),它会检查第一个匹配项并忽略其余的。
如果没有匹配项,"glob*" 将被传递给 exists() 并发现在那里也不存在。

编辑:可能会出现误报,请参见 评论


17
如果 glob 是 *.[cC] 这样的形式,可能会出现误报(可能不存在名为 cC 的文件,但是有一个名为 *.[cC] 的文件),如果第一个从 glob 展开的文件是符号链接到不存在的文件或者指向你无法访问的目录中的文件,则可能会出现误判,这时需要添加 || [ -L "$1" ] - Stephane Chazelas
1
有趣。Shellcheck 报告说,当匹配项为 0 或 1 个时,通配符只能与 -e 一起使用。如果有多个匹配项,则无法工作,因为这将变成 [ -e file1 file2 ],并且会失败。请参见 https://github.com/koalaman/shellcheck/wiki/SC2144 了解原理和建议的解决方案。 - Thomas Praxl
考虑 [ -e "$1" ] || [ -L "$1" ] -- 目前写成这样,如果 glob 有匹配项,但第一个匹配项是损坏的符号链接,则会返回 false。 - Charles Duffy

27
#!/usr/bin/env bash

# If it is set, then an unmatched glob is swept away entirely -- 
# replaced with a set of zero words -- 
# instead of remaining in place as a single word.
shopt -s nullglob

M=(*px)

if [ "${#M[*]}" -ge 1 ]; then
    echo "${#M[*]} matches."
else
    echo "No such files."
fi

2
为避免可能出现的错误“无匹配项”,请设置nullglob而不是检查单个结果是否等于模式本身。有些模式可以匹配与模式本身完全相等的名称(例如a*b;但不包括例如a?b[a])。 - Chris Johnsen
我想,这种情况非常不可能发生,因为很少有文件名与glob匹配(例如,有人运行了“touch'* py'”),但这确实让我找到了另一个好的方向。 - Ken Bloom
我喜欢这个版本,因为它是最通用的版本。 - Ken Bloom
如果您只期望一个匹配项,可以使用"$M"作为"${M[0]}"的简写。否则,由于您已经将glob扩展存储在数组变量中,因此可以将其作为列表传递给其他内容,而无需重新扩展glob。 - Peter Cordes
1
很好。您可以使用if [[ $M ]]; then ...更快地测试M(更少的字节且无需生成[进程)。 - Tobia

14

如果你设置了globfail,你可以使用这个疯狂的选项(但实际上你不应该这样做)

shopt -s failglob # exit if * does not match 
( : * ) && echo 0 || echo 1
或者
q=( * ) && echo 0 || echo 1

3
一个美妙的noop失败用法。永远不应该被使用...但真的非常漂亮。 :) - Brian Chrisman
你可以将shopt放在括号里面。这样它只会影响测试:(shopt -s failglob; : *) 2>/dev/null && echo exists - flabdablet

8

test -e有一个不幸的缺陷,它认为已损坏的符号链接不存在。因此,您可能还需要检查这些内容。

function globexists {
  test -e "$1" -o -L "$1"
}

if globexists glob*; then
    echo found
else
    echo not found
fi

5
即使像 Stephane Chazelas 指出 Dan Bloch 的回答一样,仍无法解决文件名中包含 glob 特殊字符的误报问题(除非你使用 nullglob)。请注意不要更改原文意思并尽量使翻译通俗易懂。 - Peter Cordes
4
test/[命令中,应避免使用-o-a选项。例如,在这里,如果$1等于=,则大多数实现会失败。而应该使用[ -e "$1" ] || [ -L "$1" ]来代替。 - Stephane Chazelas

5
我有另一个解决方案:
if [ "$(echo glob*)" != 'glob*' ]

这对我来说很好用。可能有一些我忽略的特殊情况。

2
除非文件实际命名为“glob*”,否则它可以正常工作。 - Ian Kelling
当将glob作为变量传入时,它可以工作,但是当有多个匹配项时会出现“参数过多”的错误。 "$(echo $GLOB)"没有返回单个字符串,或者至少没有被解释为单个字符串,因此会出现“参数过多”的错误。 - DKebler
@DKebler:它应该被解释为单个字符串,因为它被双引号包裹起来了。 - user1934428
如果设置了nullglob shell选项,这将失败,并且它总是不必要地慢(因为$(...)涉及分叉出一个新的shell副本)。 - Charles Duffy

4
为了简化miku的回答,基于他的想法,如下所述:
M=(*py)
if [ -e ${M[0]} ]; then
  echo Found
else
  echo Not Found
fi

5
接近正确,但如果您正在匹配[a],有一个名为[a]的文件,但是没有名为a的文件呢?我仍然喜欢使用nullglob。有些人可能认为这很迂腐,但我们最好尽可能地准确无误。 - Chris Johnsen
@sondra.kinsey 这是错误的;通配符[a]应该只匹配字符a,而不是字面文件名[a] - tripleee

4

根据flabdablet的回答,在我看来,最简单(不一定是最快的)的方法就是直接使用find本身,同时保留shell中的通配符扩展,如下:

find /some/{p,long-p}ath/with/*globs* -quit &> /dev/null && echo "MATCH"

或者在 if 中使用:

if find $yourGlob -quit &> /dev/null; then
    echo "MATCH"
else
    echo "NOT-FOUND"
fi

这个与我之前使用stat展示的版本完全一样;不确定find怎么比stat“更容易”。 - flabdablet
3
请注意,&> 重定向是 Bash 独有的功能,使用其他 shell 时可能会默默地出错。 - flabdablet
这个解决方案似乎比flabdablet的find答案更好,因为它接受glob中的路径并且更简洁(不需要-maxdepth等)。它似乎也比他的stat答案更好,因为它不会在每个额外的glob匹配上继续执行额外的stat操作。如果有人能够提供此解决方案无法处理的特殊情况,我将不胜感激。 - drwatsoncode
1
经过进一步考虑,我会添加-maxdepth 0,因为它允许在添加条件时更加灵活。例如,假设我想将结果限制为仅匹配文件。我可以尝试 find $glob -type f -quit,但如果glob未匹配文件但匹配包含文件的目录(甚至是递归的),它将返回true。相比之下,find $glob -maxdepth 0 -type f -quit只有在glob本身至少匹配一个文件时才返回true。请注意,maxdepth不会阻止glob具有目录组件。(FYI 2>就足够了,不需要&>。) - drwatsoncode
2
使用find的原因是避免shell生成和排序可能巨大的glob匹配列表;find -name ... -quit最多只会匹配一个文件名。如果脚本依赖于将由shell生成的glob匹配列表传递给find,那么调用find除了不必要的进程启动开销之外什么也没有实现。直接测试结果列表是否非空将更快更清晰。 - flabdablet

3
在Bash中,您可以使用通配符将内容存储到数组中;如果通配符没有匹配成功,则您的数组将包含一个单独的条目,该条目不对应任何现有文件。
#!/bin/bash

shellglob='*.sh'

scripts=($shellglob)

if [ -e "${scripts[0]}" ]
then stat "${scripts[@]}"
fi

注意:如果你已经设置了nullglobscripts将会是一个空数组,此时你需要使用[ "${scripts[*]}" ][ "${#scripts[*]}" != 0 ]来进行测试。如果你正在编写一个必须能够在有或没有nullglob的情况下正常工作的库,那么你可能需要
if [ "${scripts[*]}" ] && [ -e "${scripts[0]}" ]

这种方法的优点是你可以拥有想要处理的文件列表,而不必重复使用glob操作。

为什么在设置了nullglob并且数组可能为空的情况下,你仍然不能使用 if [ -e "${scripts[0]}" ]... 进行测试?你是否也考虑到了shell选项nounset的可能性? - johnraff
@johnraff,是的,我通常默认nounset已经激活。此外,检测字符串是否为空可能比检查文件是否存在要稍微便宜一些。尽管这种情况不太可能发生,因为我们刚刚执行了一个glob,意味着目录内容应该还保存在操作系统的缓存中。 - Toby Speight

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接