【问题标题】:Test whether a glob has any matches in Bash测试 glob 在 Bash 中是否有任何匹配项
【发布时间】:2011-02-25 14:46:24
【问题描述】:

如果我想检查单个文件是否存在,我可以使用test -e filename[ -e filename ] 进行测试。

假设我有一个 glob,我想知道是否存在名称与 glob 匹配的文件。 glob 可以匹配 0 个文件(在这种情况下我不需要做任何事情),或者它可以匹配 1 个或多个文件(在这种情况下我需要做一些事情)。如何测试 glob 是否有匹配项? (我不在乎有多少匹配项,如果我可以使用一个 if 语句并且没有循环来做到这一点,那将是最好的(因为我发现它最易读)。

(如果 glob 匹配多个文件,test -e glob* 将失败。)

【问题讨论】:

  • 我怀疑我下面的回答是“明显正确的”,在某种程度上,所有其他人都在胡扯。这是一个一直存在的单行 shell-builtin,似乎是“这项特定工作的预期工具”。我担心用户会在这里错误地引用接受的答案。任何人请随时纠正我,我会在这里撤回我的评论,我很高兴犯错并从中吸取教训。如果差异没有显得那么大,我不会提出这个问题。
  • 我最喜欢的这个问题的解决方案是the find command,它可以在任何shell(甚至非Bourne shell)中工作,但需要GNU find,而compgen command显然是一个Bashism。太糟糕了,我不能同时接受这两个答案。
  • 注意:此问题自提出以来已被编辑。最初的标题是“测试一个 glob 在 bash 中是否有任何匹配项”。在我发表我的答案后,特定的 shell 'bash' 已从问题中删除。问题标题的编辑使我的答案似乎有误。我希望有人可以修改或至少解决此更改。
  • 在此处添加注释,“glob”是“通配符”的同义词,以防人们搜索第二个词。

标签: bash glob


【解决方案1】:

普遍认为[ -f file* ] 不起作用。事实上,它确实有效,而且我个人发现它在某些情况下非常有用——当我想在特定位置捕获一个且只有一个文件的名称时。例如,名称中包含版本号的文件。考虑这段代码:

if [ -f "$ROOT"/lib64/libc-*.so ] ;then
  LIBC=$(basename -- "$ROOT"/lib64/libc-*.so .so)
else
  echo "libc ??" ; exit 1
fi

顺便说一句,ShellCheck 看到这样的用法时会哭出声来。 :-) 我希望他们能解决这个问题!

SC2144. -e doesn't work with globs. Use a for loop.

【讨论】:

    【解决方案2】:

    Bash 中扩展 glob (extglob) 的解决方案:

    bash -c $'shopt -s extglob \n /bin/ls -1U <ext-glob-pattern>'
    

    如果至少有一个匹配项,则退出状态为 0,如果没有匹配项,则退出状态为非零 (2)。标准输出包含一个以换行符分隔的匹配文件列表(以及包含空格的文件名)。

    或者,略有不同:

    bash -c $'shopt -s extglob \n compgen -G <ext-glob-pattern>'
    

    与基于ls 的解决方案的区别:可能更快(未测量),输出中未引用空格的文件名,不匹配时退出代码1(不是2 :shrug:)。

    示例用法:

    不匹配:

    $ bash -c $'shopt -s extglob \n /bin/ls -1U @(*.foo|*.bar)'; echo "exit status: $?"
    /bin/ls: cannot access '@(*.foo|*.bar)': No such file or directory
    exit status: 2
    

    至少一场比赛:

    $ bash -c $'shopt -s extglob \n /bin/ls -1U @(*.ts|*.mp4)'; echo "exit status: $?"
    'video1 with spaces.mp4'
    video2.mp4
    video3.mp4
    exit status: 0
    

    使用的概念:

    • ls'退出代码行为(为efficiency添加-U,为输出控制添加-1)。
    • 不在当前 shell 中启用 extglob(通常不需要)。
    • 使用$ 前缀,以便解释\n,这样扩展的glob 模式与shopt -s extglob 位于不同的行——否则扩展的glob 模式将是一个语法错误!

    注意 1: 我致力于此解决方案,因为其他答案中建议的 compgen -G "&lt;glob-pattern&gt;" 方法似乎不适用于 brace expansion;但是我需要一些更高级的通配符功能。

    注意 2: 扩展 glob 语法的可爱资源:extglob

    【讨论】:

      【解决方案3】:

      Bash 中这样(测试文件包含pattern):

      shopt -s nullglob
      compgen -W *pattern* &>/dev/null
      case $? in
          0) echo "only one file match" ;;
          1) echo "more than one file match" ;;
          2) echo "no file match" ;;
      esac
      

      compgen -G好得多:因为我们可以判别更多的情况,更准确。

      它只能使用一个通配符*

      【讨论】:

        【解决方案4】:
        set -- glob*
        if [ -f "$1" ]; then
          echo "It matched"
        fi
        

        说明

        glob* 不匹配时,$1 将包含'glob*'。测试 -f "$1" 不会为真,因为 glob* 文件不存在。

        为什么这比替代品更好

        这适用于 sh 和衍生产品:KornShell 和 Bash。它不会创建任何子外壳。 $(..)`...` 命令创建一个子shell;他们分叉一个进程,因此比这个解决方案慢。

        【讨论】:

        【解决方案5】:

        ls | grep -q "glob.*"

        不是最有效的解决方案(如果目录中有大量文件,它可能会很慢),但它简单、易于阅读,并且还具有正则表达式比普通 Bash glob 模式更强大的优势。

        【讨论】:

          【解决方案6】:

          Bash——具体解决方案:

          compgen -G "<glob-pattern>"
          

          转义模式,否则它会被预先扩展为匹配项。

          退出状态为:

          • 1 表示不匹配,
          • 0 表示“一个或多个匹配项”

          stdout与 glob 匹配的文件的列表。 我认为从简洁性和最小化潜在副作用的角度来看,这是最佳选择。

          例子:

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

          【讨论】:

          • 请注意,compgenbash 特定的内置命令,不是 POSIX 标准 Unix shell 指定的内置命令的一部分。 pubs.opengroup.org/onlinepubs/9699919799 pubs.opengroup.org/onlinepubs/9699919799/utilities/… 因此,避免在需要移植到其他 shell 的脚本中使用它。
          • 在我看来,没有 bash 内置函数的类似效果是使用任何其他作用于 glob 的命令,如果没有匹配的文件则失败,例如 ls:if ls /tmp/*Files 2&gt;&amp;1 &gt;/dev/null; then echo exists; fi - 可能对代码有用高尔夫球?如果有一个与 glob 同名的文件(glob 不应该匹配)则失败,但如果是这种情况,您可能会遇到更大的问题。
          • @DewiMorgan 这更简单:if ls /tmp/*Files &amp;&gt; /dev/null; then echo exists; fi
          • 是的,引用它或文件名通配符将被预先扩展。 compgen "dir/*.ext"
          • 如果 glob 包含 {},例如 ./dir/*.{ext1,ext2},则不起作用,否则 bash 可以扩展它
          【解决方案7】:

          我还有另一个解决方案:

          if [ "$(echo glob*)" != 'glob*' ]
          

          这对我很有效。我可能错过了一些极端情况。

          【讨论】:

          • 有效,除非文件实际命名为“glob*”。
          • 确实适用于将 glob 作为变量传递 - 当有多个匹配项时会出现“参数过多”错误。 "$(echo $GLOB)" 没有返回单个字符串,或者至少它没有被解释为单个字符串,因此参数过多错误
          • @DKebler :它应该被解释为单个字符串,因为它是用双引号括起来的。
          • 如果设置了nullglob shell 选项,这将失败,并且它总是不必要的慢(因为$(...) 涉及到分叉一个新的 shell 副本)。
          【解决方案8】:

          根据他的想法,稍微简化miku's answer

          M=(*py)
          if [ -e ${M[0]} ]; then
            echo Found
          else
            echo Not Found
          fi
          

          【讨论】:

          • 关闭,但是如果你匹配[a],有一个名为[a]的文件,但没有名为a的文件?为此,我仍然喜欢nullglob。有些人可能认为这是迂腐的,但我们不妨在合理的情况下完全正确。
          • @sondra.kinsey 错了; glob [a] 应该只匹配a,而不是文字文件名[a]
          【解决方案9】:
          (ls glob* &>/dev/null && echo Files found) || echo No file found
          

          【讨论】:

          • 如果有与glob* 匹配的目录并且例如您没有列出这些目录的写入权限,也会返回false。
          【解决方案10】:
          [ `ls glob* 2>/dev/null | head -n 1` ] && echo true
          

          【讨论】:

          • 为了获得更好的答案,请尝试在您的代码中添加一些解释。
          • 在这种情况下,它甚至不是好的代码。如果你有一个名为glob -a foo = bar 的文件,即使它应该是真的,你也会得到一个错误的结果;而且它极其效率低下。
          • 这不会测试文件,它会测试与 glob* 匹配的目录
          • 一如既往,don't use ls in scripts.
          【解决方案11】:

          如果你设置了 globfail,你可以使用这个疯狂的(你真的不应该这样做)

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

          q=( * ) && echo 0 || echo 1
          

          【讨论】:

          • noop 失败的绝妙用法。永远不应该使用......但真的很漂亮。 :)
          • 你可以把shopt放在parens里面。这样它只会影响测试:(shopt -s failglob; : *) 2&gt;/dev/null &amp;&amp; echo exists
          【解决方案12】:

          我喜欢

          exists() {
              [ -e "$1" ]
          }
          
          if exists glob*; then
              echo found
          else
              echo not found
          fi
          

          这既可读又高效(除非有大量文件)。
          主要缺点是它比看起来要微妙得多,有时我觉得不得不添加长评论。
          如果有匹配项,"glob*" 将由 shell 扩展,所有匹配项都传递给 exists(),后者检查第一个并忽略其余的。
          如果没有匹配,"glob*" 被传递给exists() 并且发现那里也不存在。

          编辑:可能存在误报,见comment

          【讨论】:

          • 如果 glob 类似于 *.[cC](可能没有 cC 文件,但有一个名为 *.[cC] 的文件),它可能会返回误报,或者如果例如,从该文件扩展的第一个文件是指向不存在的文件或您无权访问的目录中的文件的符号链接(您想添加一个|| [ -L "$1" ])。
          • 有趣。 Shellcheck 报告说,当有 0 或 1 个匹配时,通配符仅适用于 -e。它不适用于多场比赛,因为那会变成[ -e file1 file2 ],这会失败。另请参阅 github.com/koalaman/shellcheck/wiki/SC2144 了解基本原理和建议的解决方案。
          【解决方案13】:

          在 Bash 中,您可以 glob 到一个数组;如果 glob 不匹配,您的数组将包含与现有文件不对应的单个条目:

          #!/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,是的,我通常认为nounset 处于活动状态。此外,与检查文件是否存在相比,测试字符串是否为非空可能(稍微)便宜。不过不太可能,因为我们刚刚执行了一个 glob,这意味着目录内容在操作系统的缓存中应该是新的。
          【解决方案14】:

          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
          

          显式 -print -quit 操作用于 find,而不是默认的隐式 -print 操作,以便 find 将在找到与搜索条件匹配的第一个文件后立即退出。在大量文件匹配的情况下,这应该比echo glob*ls glob* 运行得快得多,并且它还避免了扩展命令行的可能性(某些shell 有4K 长度限制)。

          如果 find 感觉有点矫枉过正,并且可能匹配的文件数量很少,请使用 stat:

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

          【讨论】:

          • find 似乎完全正确。它没有极端情况,因为 shell 没有进行扩展(并将未扩展的 glob 传递给其他命令),它在 shell 之间是可移植的(尽管显然不是您使用的所有选项都由 POSIX 指定),而且它比ls -d glob*(之前接受的答案)因为它在到达第一场比赛时停止。
          • 请注意,此答案可能需要shopt -u failglob,因为这些选项似乎以某种方式发生冲突。
          • find 解决方案也将匹配没有全局字符的文件名。在这种情况下,这就是我想要的。只是需要注意一些事情。
          • 因为其他人决定编辑我的答案,让它这么说,显然。
          • unix.stackexchange.com/questions/275637/… 讨论如何替换 POSIX 查找的 -maxdepth 选项。
          【解决方案15】:

          这个可憎的东西似乎起作用了:

          #!/usr/bin/env bash
          shopt -s nullglob
          if [ "`echo *py`" != "" ]; then
              echo "Glob matched"
          else
              echo "Glob did not match"
          fi
          

          它可能需要 bash,而不是 sh。

          这是有效的,因为如果没有匹配项,nullglob 选项会导致 glob 评估为空字符串。因此,任何来自 echo 命令的非空输出都表明 glob 匹配了某些内容。

          【讨论】:

          • 你应该使用if [ "`echo *py`" != "*py"]
          • 如果有一个名为*py的文件,那将无法正常工作。
          • 如果没有以py 结尾的文件,`echo *py` 将被评估为*py
          • 是的,但是如果有一个名为*py的文件也会这样做,这是错误的结果。
          • 如果我错了,请纠正我,但如果没有匹配 *py 的文件,您的脚本会回显“Glob 匹配”?
          【解决方案16】:

          test -e 有一个不幸的警告,它认为损坏的符号链接不存在。因此,您可能也想检查这些。

          function globexists {
            test -e "$1" -o -L "$1"
          }
          
          if globexists glob*; then
              echo found
          else
              echo not found
          fi
          

          【讨论】:

          • 这仍然不能解决文件名中包含 glob 特殊字符的误报问题,就像 Stephane Chazelas 为 Dan Bloch 的回答指出的那样。 (除非你用 nullglob 猴子)。
          • 你应该避免在test/[中使用-o-a。例如,在这里,如果 $1 在大多数实现中是 =,它就会失败。请改用[ -e "$1" ] || [ -L "$1" ]
          【解决方案17】:

          基于flabdablet's answer,对我来说,最简单(不一定是最快)只是使用 find 本身,同时在 shell 上保留 glob 扩展,例如:

          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 更“容易”。
          • 请注意 &> 重定向是一种 bashism,并且会悄悄地在其他 shell 中做错事。
          • 这似乎比 flabdablet 的 find 答案更好,因为它接受 glob 中的路径并且更简洁(不需要 -maxdepth 等)。它似乎也比他的stat 答案更好,因为它不会继续在每个额外的全局匹配中执行额外的stating。如果有人能提供这不起作用的极端案例,我将不胜感激。
          • 经过进一步考虑,我会添加-maxdepth 0,因为它可以更灵活地添加条件。例如假设我只想将结果限制为匹配文件。我可能会尝试 find $glob -type f -quit ,但如果 glob 不匹配文件,但确实匹配 包含 文件的目录(甚至递归),那将返回 true。相比之下,find $glob -maxdepth 0 -type f -quit 只会在 glob 本身与至少一个文件匹配时才返回 true。请注意,maxdepth 不会阻止 glob 具有目录组件。 (仅供参考2&gt; 就足够了。不需要&amp;&gt;
          • 首先使用find 的目的是避免让shell 生成并排序一个潜在的巨大glob 匹配列表; find -name ... -quit 最多匹配一个文件名。如果脚本依赖于将 shell 生成的 glob 匹配列表传递给 find,则调用 find 只会获得不必要的进程启动开销。简单地直接测试结果列表的非空性会更快更清晰。
          【解决方案18】:
          #!/bin/bash
          set nullglob
          touch /tmp/foo1 /tmp/foo2 /tmp/foo3
          FOUND=0
          for FILE in /tmp/foo*
          do
              FOUND=$((${FOUND} + 1))
          done
          if [ ${FOUND} -gt 0 ]; then
              echo "I found ${FOUND} matches"
          else
              echo "No matches found"
          fi
          

          【讨论】:

          • 当恰好一个文件匹配时,此版本会失败,但您可以使用nullglob shell 选项来避免 FOUND=-1 混乱。
          • @Ken: 嗯,我不会把nullglob 称为杂牌。将单个结果与原始模式进行比较是一种混乱(并且容易出现错误结果),而使用 nullglob 则不是。
          • @Chris:我认为你误读了。我没有给nullglob 打电话。
          • @Ken:确实,我看错了。请接受我对我无效批评的歉意。
          【解决方案19】:
          #!/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
          

          【讨论】:

          • 为了避免可能的错误“不匹配”设置nullglob,而不是检查单个结果是否等于模式本身。某些模式可以匹配与模式本身完全相同的名称(例如a*b;但不能匹配例如a?b[a])。
          • 我认为这极不可能可能会失败,因为实际上有一个名为 glob 的文件。 (例如有人跑了touch '*py'),但这确实为我指明了另一个好的方向。
          • 我喜欢这个作为最通用的版本。
          • 也是最短的。如果您只期待一场比赛,您可以使用"$M" 作为"${M[0]}" 的简写。否则,您已经在数组变量中进行了 glob 扩展,因此您需要将其作为列表传递给其他事物,而不是让它们重新扩展 glob。
          • 不错。您可以使用 if [[ $M ]]; then ... 更快地测试 M(更少的字节并且不产生 [ 进程)
          【解决方案20】:
          if ls -d $glob > /dev/null 2>&1; then
            echo Found.
          else
            echo Not found.
          fi
          

          请注意,如果有很多匹配项或文件访问速度很慢,这可能会非常耗时。

          【讨论】:

          • 如果在文件[a] 存在且文件a 不存在时使用[a] 之类的模式,这将给出错误答案。即使它应该匹配的唯一文件 a 实际上并不存在,它也会说“找到”。
          • 这个版本应该在普通的 POSIX /bin/sh 中工作(没有 bashisms),如果我需要它,glob 无论如何都没有括号,我没有不需要担心非常病态的病例。但我想没有一种好方法可以测试任何文件是否与 glob 匹配。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-03-10
          • 2018-11-22
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-02-17
          相关资源
          最近更新 更多