递归搜索具有排除和包含的文件

递归搜索具有排除和包含的文件

我想使用 find 递归搜索文件并将它们打印到文件中。我设想这样的命令:

find /local/data/ --exclude 'database/session*' --include='database/session_*.db' > temp.txt

此命令不起作用。如何使用有效的排除和包含?

答案1

总结

类似于

find /local/data/ \
   ! -path '/local/data/database/session*' \
   -o -path '/local/data/database/session_*.db'

前言

我知道的实现中没有简单的--include和指令。无论如何,您可以构建一个按您的意愿工作的测试序列,因为中的测试机制是经过精心设计的,允许基于任何标准(即不一定基于路径名)的任何测试(甚至是自定义测试)。要执行您想要的操作,您需要将排除/包含模式转换为测试序列。要正确执行此操作,您需要知道它是如何工作的。它的机制比排除/包含的概念更通用。--excludefindfindfind

在这里我将主要依靠POSIX 规范find(所有引文均来自本文档)。超出本规范的实现会扩展该工具,而不会改变其总体理念。


理论

为了理解和有效使用,find您需要了解以下几点:

  1. 术语:

    • 很少有可能选项(例如-L)可能出现在 之后find。对于此答案的目的而言,它们并不重要。
    • 然后有一个或多个起点./local/data/在您的示例中是起点。某些实现允许零起点(则../为默认起点)。
    • 接下来的一切构成了一个表达。表达式由零个或多个受支持的操作数组成:基本操作数如-name-exec;操作符如-o((通常应将其转义或加引号以保护其免受 shell 攻击)或!。其中一些需要自定义的附加操作数(例如模式),这些操作数也属于表达式。
  2. 表达式中的几乎所有内容都是测试。我的 Ubuntu 中的 GNU 手册find将支持的操作数分为几类:测试、操作等。但大多数操作数仍可视为测试;即任何主要操作都返回 true 或 false,这会影响find下一步的操作。在此回答中,我以非常广泛的意义使用了“测试”一词。

  3. find从指定的起点开始,按照特定顺序递归下降目录层次结构。某些操作数可以改变顺序 ( -depth) 甚至缩短顺序 ( -prune)。

  4. find计算每个表达式的值文件分别地。

  5. find从左到右计算表达式。如果此操作不影响整体输出(不仅仅是输出到 stdout,注意-exec可以做任何事情),该工具可能会重新排列测试,一些实现这样做是为了提高性能;即使这样,表达式也应该像从左到右计算一样工作。不过,有些操作数无论在表达式中的位置如何都可以工作(-depth-xdev)。

  6. 对于给定文件,表达式的某些部分可能根本无法求值。运算符-a-o(+定义表达式的逻辑。)!

    可以使用以下运算符来组合原色(按优先级从高到低的顺序):

    ( expression )
    如果expression为真,则为真。

    ! expression
    对基本运算符进行否定;一元非运算符。

    expression [-a] expression
    主元的结合;AND 运算符由两个主元的并置暗示或由可选运算符明确表示-a。如果第一个表达式为假,则不应评估第二个表达式。

    expression -o expression
    主元交替;或运算符。如果第一个表达式为真,则不应计算第二个表达式。

    想象一下-test1-test2并且-test3是测试find理解。让表达式为

    ! -test1 -test2 -o -test3
    

    相当于

    ( ( ! -test1 ) -a -test2 ) -o -test3
    

    在 shell 中完整命令分别为:

    find /starting/point ! -test1 -test2 -o -test3
    find /starting/point \( \( ! -test1 \) -a -test2 \) -o -test3
    

    可能的结果:

    • -test1对每个测试文件进行评估。
      • 如果-test1为假,( ! -test1 )则为真。然后-test2进行评估,因为这就是-a工作原理。
        • 如果-test2为假,则外括号中的表达式为假。然后-test3进行求值,因为这就是-o工作原理。
          • 如果-test3为假,则整个表达式为假。
          • 如果-test3为真,则整个表达式为真。
        • 如果-test2为真,则外括号中的表达式为真。然后-test3不进行求值,因为这就是-o工作原理。整个表达式为真。
      • 如果-test1为真,( ! -test1 )则为假。然后-test2不进行求值,因为这是-a工作原理。外括号中的表达式为假。然后-test3进行求值,因为这是-o工作原理。
        • 如果-test3为假,则整个表达式为假。
        • 如果-test3为真,则整个表达式为真。

    注意 从逻辑上讲( ( NOT A ) AND B ) OR C等价于C OR ( B AND ( NOT A ) ),但与find下列表达式并不等价,一般来说它们两两不同:

    ! -test1 -test2 -o -test3
    -test2 ! -test1 -o -test3
    -test3 -o ! -test1 -test2
    -test3 -o -test2 ! -test1
    

    如果有一个或多个测试,则尤其如此-exec。通常-exec用于有条件地做某事(例子),因此它将在其他测试(条件)之后,我们宁愿说它是一个动作,而不是一个测试。但你可以用-exec(例子),这非常强大;在这种情况下,-exec甚至可能是第一个测试,即始终要评估的测试。不仅逻辑结果(真或假)会-exec执行find或跳过文件的后续测试。什么-exec(例如,假设它删除了一些附带文件)可能会以不明显的方式影响后续测试(针对同一文件甚至其他文件)。

  7. 括号是重要的. 似乎行为不当的问题-o通常通过使用括号 (例子)。

  8. 在某些情况下-print会隐式添加:

    如果不存在表达式,-print则应使用作为表达式。否则,如果给定的表达式不包含任何原语-exec-ok-print,则给定的表达式应被有效地替换为:

    ( given_expression ) -print
    

    笔记

    • 在这种情况下,-print当且仅当给定表达式求值为真时,才会求值(执行)。上面,我写到“整个表达式为假”或“整个表达式为真”,我的意思是对于隐式-print(如果适用)来说什么是重要的。
    • 实现中可以使用其他(非 POSIX)主键来扩展集合“ -exec, -ok, ”。-print

解决方案

问题是关于基于路径名的排除/包含。以下主要内容很有用:

  • -name pattern
    pattern如果当前路径名的基本名称使用模式匹配符号 匹配,则主要将评估为真 […]

  • -path pattern
    pattern如果当前路径名使用模式匹配符号匹配, 则主路径将评估为真[…]

  • -prune
    主路径应始终评估为真;find如果当前路径是目录,则不下降当前路径名。如果-depth指定了主路径,则 -prune 主路径将不起作用。

(“基本名称”或“路径名称”等术语的定义这里

实现中可以添加其他有用的原色(例如-regex-iname

通常-prune排除给定目录内容的正确方法(带或不带目录本身)。但它完全阻止find进入目录;因此,如果您无论如何都想在目录中查找(包含)某些文件,那么您就不能使用-prune

我想你想要这个:

  • 打印目录层次结构中从 开始的每个文件的路径名/local/data/
  • 但不要如果匹配/local/data/database/session*
  • 但要如果匹配/local/data/database/session_*.db

以下find命令可以完成此操作:

find /local/data/ \
   ! -path '/local/data/database/session*' \
   -o -path '/local/data/database/session_*.db'

换行符之前的位置\告诉 shell 命令将在下一行继续。引用很重要(您可能知道,您在问题中引用了)。

它的工作原理如下:

  • 对于起点下(包括起点)但不匹配排除模式的每个文件,! -path …为真;不执行第二次测试,整个表达式为真。
  • 对于起点下(包括起点)的每个文件并且匹配排除模式,! -path …都是错误的;只有这样才会执行第二次测试。
    • 如果第二个测试为真,则整个表达式为真。
    • 如果第二个测试结果为假,则整个表达式也为假。

笔记:

  • -print这个是添加隐式的情况。
  • 按相反顺序进行的这些测试同样有效。

一般情况

使用括号-a-o!可以创建相当复杂的排除+包含方案。特别是:

  • 嵌套(例如 排除./foo/*,但 包括./foo/bar/*,但 排除./foo/bar/baz/*,但...);
  • 基于路径名以外的标准(例如,完全排除根拥有的目录)。

尽管创建完美地实现复杂方案的表达式可能并不容易。


陷阱

  1. 模式中的元字符(例如*)不会对/或进行.特殊处理。片段session_*.db匹配session_5.db,它也匹配session_foo/bar/baz.db

  2. 在可以使用的情况下-prune,记住-prune评估为真。使用隐式-print 这可能会让你感到惊讶这就是为什么我写道“-prune排除给定目录内容的正确方法(有或没有目录本身)”。

  3. 在您可以使用的情况下-prune,请确保在需要时对其进行评估。

    例子:

    mkdir -p test/ab/a; cd test
    
    find .    -name 'a*' -print        -o -name '*b' -prune             #1
    find .    -name '*b' -prune        -o -name 'a*' -print             #2
    find .    -name '*b' -prune -print -o -name 'a*' -print             #3
    find . \( -name '*b' -prune        -o -name 'a*'        \) -print   #4
    find .    -name '*b' -prune        -o -name 'a*'                    #5
    

    ab在第一种情况下,将打印指定的目录,并不是修剪。在第二种情况下,它将被修剪并且不打印。在第三种情况下,它将被修剪并打印一次。第四种情况相当于第三种情况,-print已放在括号后面(就像数学中的公因数)。第五种情况相当于第四种情况,-print是隐式的。

    第一种情况是一个更普遍的问题(错误)的例子,其中某些文件(这里是ab目录)从未达到为其设计的测试和正确的操作,因为它意外地与为其他文件设计的早期测试匹配,并触发了不必要的操作。

  4. 使用的路径名-pathfind“认为”的路径名,而不是realpath打印的路径名。模式必须考虑到这一点。

    例子:

    cd /bin && find .    -path '/bin*'   # will find nothing
    cd /bin && find .    -path '.*'      # will find "everything"
    cd /bin && find /bin -path '/bin*'   # will find "everything"
    cd /bin && find /bin -path '.*'      # will find nothing
    

    类似地,对于起点,所使用的基本名称-name取决于起点的确切表示。虽然存在一些极端情况,但仍然如此:

    • / 对于/,,///等等////
    • . 对于.,,,,等.//.​​/bin/./bin/../.
    • ..对于..、、、等等/..​​/../..////bin/..
  5. 每个起点定义一个单独的层次结构。该工具不关心层次结构是否重叠。

    例如:如果/bin/bash/bin/dash存在,则以下命令将找到bash四次(使用三个不同的路径名)和dash三次(使用两个不同的路径名):

    cd /bin && find . /bin /bin ../bin/bash -name '[bd]ash'
    

相关内容