将 glob 转换为“查找”

将 glob 转换为“查找”

我一次又一次地遇到这个问题:我有一个 glob,它与正确的文件完全匹配,但导致Command line too long.每次我都会将其转换为适用于特定情况的findgrep的某种组合,但并不是 100% 等效。

例如:

./foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg

是否有一个工具可以将 glob 转换为find我不知道的表达式?或者是否有一个选项可以find匹配 glob 而不匹配子目录中的相同 glob (例如foo/*.jpg不允许 match bar/foo/*.jpg)?

答案1

如果问题是您收到参数列表太长错误,请使用循环或内置 shell。虽然command glob-that-matches-too-much可能会出错,for f in glob-that-matches-too-much但不会,所以你可以这样做:

for f in foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg
do
    something "$f"
done

该循环可能非常慢,但它应该可以工作。

或者:

printf "%s\0" foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg |
  xargs -r0 something

printf大多数 shell 中都内置了上述内容,因此可以解决系统调用的限制execve()

$ cat /usr/share/**/* > /dev/null
zsh: argument list too long: cat
$ printf "%s\n" /usr/share/**/* | wc -l
165606

也适用于 bash。我不确定这到底记录在哪里。


两个 Vim 的glob2regpat()和Python的fnmatch.translate()可以将 glob 转换为正则表达式,但两者也都使用.*for *,跨/.

答案2

find(对于-name/-path标准谓词)使用通配符模式,就像 glob 一样(请注意,这{a,b}不是 glob 运算符;扩展后,您会得到两个 glob)。主要区别是斜杠的处理(并且点文件和目录在 中没有被特殊处理find)。*in globs 不会跨越多个目录。*/*/*将导致列出最多 2 级目录。添加 a-path './*/*/*'将匹配至少 3 级深度的任何文件,并且不会停止find列出任何深度的任何目录的内容。

对于那个特定的

./foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg

几个 glob,很容易翻译,你想要深度为 3 的目录,所以你可以使用:

find . -mindepth 3 -maxdepth 3 \
       \( -path './foo*bar/quux[A-Z].bak/pic[0-9][0-9][0-9][0-9]?.jpg' -o \
          -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg' \) \
       -exec cmd {} +

或者 POSIXly:

find . -path './*/*/*' -prune \
       \( -path './foo*bar/quux[A-Z].bak/pic[0-9][0-9][0-9][0-9]?.jpg' -o \
          -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg' \) \
       -exec cmd {} +

这将保证那些*?无法匹配/字符。

( find,与 glob 相反,会读取除当前目录中的目录之外的目录内容foo*bar,并且不会对文件列表进行排序。但是,如果我们忽略/匹配的内容或关于无效字符[A-Z]的行为的问题是未指定,您将获得相同的文件列表)。*?

但无论如何,作为@muru 已表明find,如果只是为了将文件列表拆分为多次运行来解决execve()系统调用的限制,则无需求助。一些 shell,例如zsh(with zargs) 或ksh93(with command -x) 甚至对此有内置支持。

With zsh(其 glob 也具有相当于-type f和 大多数其他find谓词),例如:

autoload zargs # if not already in ~/.zshrc
zargs ./foo*bar/quux[A-Z](|.bak)/pic[0-9][0-9][0-9][0-9]?.jpg(.) -- cmd

(|.bak)是与 相反的 glob 运算符{,.bak}(.)glob 限定符相当于find's -type f,添加oN到其中可以跳过像 with 那样的排序findD以包含点文件(不适用于此 glob))


1 为了find像 glob 一样爬行目录树,你需要类似的东西:

find . ! -name . \( \
  \( -path './*/*' -o -name 'foo*bar' -o -prune \) \
  -path './*/*/*' -prune -name 'pic[0-9][0-9][0-9][0-9]?.jpg' -exec cmd {} + -o \
  \( ! -path './*/*' -o -name 'quux[A-Z]' -o -name 'quux[A-Z].bak' -o -prune \) \)

那是修剪除 之外的所有级别 1 的目录,以及除或foo*bar之外的所有级别 2 的目录,然后选择级别 3 的目录(并修剪该级别的所有目录)。quux[A-Z]quux[A-Z].bakpic...

答案3

您可以编写一个正则表达式来查找符合您要求的内容:

find . -regextype egrep -regex './foo[^/]*bar/quux[A-Z](\.bak)?/pic[0-9][0-9][0-9][0-9][^/]?\.jpg'

答案4

关于注释的概括我的另一个答案,作为对您问题的更直接回答,您可以使用此 POSIXsh脚本将 glob 转换为find表达式:

#! /bin/sh -
glob=${1#./}
shift
n=$#
p='./*'

while true; do
  case $glob in
    (*/*)
      set -- "$@" \( ! -path "$p" -o -path "$p/*" -o -name "${glob%%/*}" -o -prune \)
      glob=${glob#*/} p=$p/*;;
    (*)
      set -- "$@" -path "$p" -prune -name "$glob"
      while [ "$n" -gt 0 ]; do
        set -- "$@" "$1"
        shift
        n=$((n - 1))
      done
      break;;
  esac
done
find . "$@"

配合使用标准shglob (所以不是示例中使用的两个 glob大括号扩展):

glob2find './foo*bar/quux[A-Z].bak/pic[0-9][0-9][0-9][0-9]?.jpg' \
  -type f -exec cmd {} +

.(除了和之外,不会忽略点文件或点目录,..并且不会对文件列表进行排序)。

该方法仅适用于相对于当前目录的全局变量,没有...组件。通过一些努力,您可以将其扩展到任何 glob,不仅仅是一个 glob...也可以对其进行优化,以便不会像模式那样glob2find 'dir/*'寻找相同的内容。dir

相关内容