Linux find 命令跳过目录

Linux find 命令跳过目录

我在目录中安装了某些网络共享文件夹/media

我想确保当我做类似的事情时sudo find / -name foo应该始终跳过/media目录。

我不想将参数传递给命令...我想以默认情况下始终跳过目录find的方式配置我的系统。find/media

答案1

在这种情况下需要考虑许多边缘情况。find / -path '/media' -prune -o ...仅当搜索路径是绝对路径并且以 开头时,第一种方法才足够/。场景cd / && find * ...永远不会与-path '/media'子句匹配。

幸运的是,-inum参数可以解决这个问题。索引节点号仅针对每个已安装的文件系统是唯一的,因此为了排除,/media我们需要识别由文件系统和索引节点号组成的元组。

以下(长)脚本将为/media您排除,希望能够捕获足够有用的边缘情况。


#!/bin/bash
#
FIND=/usr/bin/find


# Process prefix arguments
#
opt_H= opt_L= opt_P= opt_D= opt_O=
while getopts 'HLPD:O:' opt
do
    case "$opt" in
        H)      opt_H=-H ;;
        L)      opt_L=-L ;;
        P)      opt_P=-P ;;
        D)      opt_D="-D $OPTARG" ;;
        O)      opt_O="-O $OPTARG" ;;
    esac
done
shift $((OPTIND - 1))


# Find the inode number for /media and its filesystem
#
m_inode=$(stat -c '%i' /media 2>/dev/null)
m_fsys=$(stat -c '%m' /media 2>/dev/null)


# Collect the one or more filesystem roots to search
#
roots=()
while [[ 0 -lt $# && "$1" != -* ]]
do
    roots+=("$1")
    shift
done


# Collect the "find" qualifiers. Some of them need to be at the front
# of the list. Unfortunately.
#
pre_args=() args=()
while [[ 0 -lt $# ]]
do
    # We really ought to list all qualifiers here, but I got tired of
    # typing for an example
    #
    case "$1" in
        -maxdepth)      pre_args+=("$1"); pre_args+=("$2"); shift 2 ;;
        -mindepth)      pre_args+=("$1"); pre_args+=("$2"); shift 2 ;;
        -mount|-xdev)   pre_args+=("$1"); shift ;;
        -depth|-d)      pre_args+=("$1"); shift ;;
        -name|-iname)   args+=("$1"); args+=("$2"); shift 2 ;;
        -path|-ipath)   args+=("$1"); args+=("$2"); shift 2 ;;
        *)              args+=("$1") ; shift ;;
    esac
done
test -z "${args[*]}" && args=('-print')


# Iterate across the collected filesystem roots, attempting to skip
# /media only if the filesystem matches
#
exit_ss=0
for root in "${roots[@]}"
do
    fsys=$(stat -c '%m' "$root" 2>/dev/null)
    if [[ -n "$m_inode" && -n "$m_fsys" && "$fsys" == "$m_fsys" ]]
    then
        # Same filesystem. Exclude /media by inode
        #
        "$FIND" ${opt_H:+"$opt_H"} ${opt_L:+"$opt_L"} \
                ${opt_P:+"$opt_P"} ${opt_O:+"$opt_O"} \
                ${opt_O:+"$opt_O"} "$root" "${pre_args[@]}" \
                \( -inum "$m_inode" -prune \) -o \( "${args[@]}" \)
        ss=$?
        [[ 0 -lt $ss ]] && exit_ss="$ss"
    else
        # Different filesystem so we don't need to worry about /media
        #
        "$FIND" ${opt_H:+"$opt_H"} ${opt_L:+"$opt_L"} \
                ${opt_P:+"$opt_P"} ${opt_O:+"$opt_O"} \
                ${opt_O:+"$opt_O"} "$root" "${pre_args[@]}" \
                "${pre_args[@]}" \( "${args[@]}" \)
        ss=$?
        [[ 0 -lt $ss ]] && exit_ss="$ss"
    fi
done


# All done
#
exit $exit_ss

答案2

如果您想坚持“简单”使用find(即不是多个目录,没有选项-H -L -D -P -O)并且可以使用该-xdev选项,请尝试这个简单的答案。这将排除所有已安装的文件系统(例如,$HOME如果它是单独安装的)。

您可以定义一个 bash 函数find,使其无法进入其他文件系统。将其放入您的~/.bashrc(假设您正在使用bash

find () {
  local path="${1}"
  shift
  command find "${path}" -xdev "${@}"
}

说明:我们需要使用函数而不是别名,因为find对其参数的顺序非常挑剔。 Thepath必须是第一个参数。因此,我们将 保存path在局部变量中并将其从参数列表中弹出 ( shift)。然后我们command find使用路径和所有剩余参数运行原始查找$@。前面commandfind确保我们不会以递归调用结束。

对于新~/.bashrc文件,您需要先获取它

source ~/.bashrc

然后,您就可以使用新版本的find.您始终可以使用检查定义

> type find
find is a function
find ()
{
    local path="${1}";
    shift;
    command find "${path}" -xdev "${@}"
}

答案3

(   set -e -- "$(command -v find)"
    [ -x "${1:?}"  ]
    [ ! -e "$1cmd" ]
    [ ! -L "$1cmd" ]
    mv -- "$1"  "$1cmd"
    cat > "$1"
    chmod +x -- "$1"
)   <<""
#!/bin/sh -f
eval '  exec    "$0cmd" '"${1$(                       # f!'"ing colors
        unset   i L O M rt IFS
        chk()   case   ${O+$2}${2--}    in            # $O must be set or $2
                (-maxdepth"$2") M=      ;;            # unset to match "$2$2"
                ([\(!]"$2"|-*"$2")                    # this is the last match
                        printf  %s${1+%b}%.d\
                               "$rt" \\c 2>&-   &&    # printf fails if ! $1  
                        chk(){  ${1+:} exit; }  ;;    # chk() = !!$1 || exit
                (-?*)   shift   $((OPTIND=1))         # handle -[HLP] for 
                        while   getopts :HLP    O     # path resolution w/
                        do      case    $O${L=} in    # NU$L expansions
                                (P)     unset L ;;    # $[HL]=:- $P=-
                                (\?)    rt= chk ''    # opt unexpected  &&
                                        return  ;;    # abandon parse
                        esac;   done;   unset O ;;    # $O is unset until 
                (${M-${O=?*}})                        # above matches fail
                      ! [ ! -L "${L-$2}" ]      ||    # ! -P ||!! -L ||
                        [ !  / -ef "$2"  ]      ||    # !  / == $2   ||
                        rt=$rt' ! \( -path "${'$i'%/}/media/*" -prune \)'
                esac
        while   chk ${1+$((i+=1)) "$1"}               # loop while args remain
        do      printf ' "${'$i}\"                    # printf args to eval
                shift                                 # shift args away
        done                                          # done
)"

有一个包装脚本find将插入一些参数来禁止 realfind查找/media/其任何路径参数 equal /

上面的脚本有两个部分:实际的脚本(这是以下所有内容<<""\n)并且顶部有运行一次安装位(这是第一对匹配的括号之间的所有(内容)

安装

(   set -e -- "$(command -v find)"         #get /path/to/find
    [ -x "${1:?}"  ]                       #else loudly fail
    [ ! -e "$1cmd" ]                       #fail if /path/to/findcmd
    [ ! -L "$1cmd" ]                       #and double-check
    mv -- "$1"  "$1cmd"                    #rename .../find -> .../findcmd
    cat > "$1"                             #copy stdin to .../find
    chmod +x -- "$1"                       #set new find's executable bit
)   <<"" ###stdin

安装有点需要小心不是成功完成,除非有合理的机会它可以在不直接修改系统上的任何内容的情况下完成,但你的$PATHdfind可执行文件的文件名 - 它想要将其更改为/path/to/find,并且如果尚不存在,/path/to/findcmd则会进行尝试。/path/to/findcmd如果它的测试证明是正确的 - 并且如果您具有应用命令的适当权限 - 它将重命名find可执行文件并安装一个新的 shell 脚本find来代替它。

此后,安装的脚本将永远依赖于findcmd保留在原处的重命名的可执行文件(因此,如果您使用的话,您可能需要通知您的包管理器)每次调用它时,它都会$0cmd在查看完所有参数后将其自身替换为 Called 及其所有参数。如果您不采取任何必要措施来使安装永久化,那么最终大多数软件包管理器都会find在某个时候用新更新的二进制文件覆盖已安装的脚本,因此您将回到开始的地方,除了您还将在系统目录中拥有一个较旧的find名称。findcmd../bin

给予适当的权限并且您的系统不会保证任何不当的意外,整个脚本应该可以通过复制粘贴到 shell 提示符中来自行安装(尽管你最后需要做一个额外的 RETURN )。如果它不起作用,那么至少,这种尝试应该不会造成任何伤害。

新发现

#!/bin/sh -f
eval '  exec    "$0cmd" '"${1+$(                      # f!'"ing colors
        unset   i L O M rt IFS
        chk()   case   ${O+$2}${2--}    in            # $O must be set or $2
                (-maxdepth"$2") M=      ;;            # unset to match "$2$2"
                ([\(!]"$2"|-*"$2")                    # this is the last match
                        printf  %s${1+%b}%.d\
                               "$rt" \\c 2>&-   &&    # printf fails if ! $1  
                        chk(){  ${1+:} exit; }  ;;    # chk() = !!$1 || exit
                (-?*)   shift   $((OPTIND=1))         # handle -[HLP] for 
                        while   getopts :HLP    O     # path resolution w/
                        do      case    $O${L=} in    # NU$L expansions
                                (P)     unset L ;;    # $[HL]=:- $P=-
                                (\?)    rt= chk ''    # opt unexpected  &&
                                        return  ;;    # abandon parse
                        esac;   done;   unset O ;;    # $O is unset until 
                (${M-${O=?*}})                        # above matches fail
                      ! [ ! -L "${L-$2}" ]      ||    # ! -P ||!! -L ||
                        [ !  / -ef "$2"  ]      ||    # !  / == $2   ||
                        rt=$rt' ! \( -path "${'$i'%/}/media/*" -prune \)'
                esac
        while   chk ${1+$((i+=1)) "$1"}               # loop while args remain
        do      printf ' "${'$i}\"                    # printf args to eval
                shift                                 # shift args away
        done                                          # done
)}"

编写包装脚本时我的首要规则是:放手。如果我需要一个程序,我会尝试编写一个程序,但由于我已经有了一个值得包装的程序,我会尝试让它不受阻碍地完成它已经做的事情,并尽可能少地修改其行为以实现我的最终目标。这意味着我不应该做任何可能以与包装目的不直接相关的方式影响其执行环境的事情。所以我不设置变量,不解释参数,不接触 i/o 流,也不改变包装程序的进程组或其父 pid。在所有事情中,包装器应该尽可能瞬态和透明。

上面的脚本实现了这个目标,现在比以前更能实现。我之前并不满意 - 特别是在路径解析方面 - 但我相信我已经解决了这个问题。为了正确地做到这一点,我必须跟踪[HLP]状态,以便我可以正确地将符号链接与/其中一个-H-L多个选项有效且-P不会否定它们的时间进行比较。如果链接测试通过,则检查当前参数是否有-ef相同的文件 inode 匹配/- 这意味着几乎任何名称/都可以使用-H(当或-L有效时包含符号链接)。所以我对这些东西感觉更好,并且我已将其设置为默认阻止/proc/sys/dev搜索。/

它特别擅长的是避免在将其传递给 之前修改任何被调用的状态$0cmd。它明确拒绝解释包含任何它不准备处理的选项的参数集,并且在这些情况下将整个集传递为$0cmd未触及的,因此,虽然在这些情况下它可能不会阻止路径搜索,但也不会影响find的任何其他方式的行为。正是由于这个原因,该eval "exec wrapped_program $(arg-handler)"方法是我最喜欢处理这类事情的方法。

顶层

事实上,如上所述,在其顶层,整个 shell 脚本仅相当于一个简单的命令 - 该命令告诉它用另一个可执行文件替换自身。任何完成的工作都是在$(命令替换子)shell 中完成的,并且其所有状态(修改与否)都是完全本地化的。其背后的目的eval是再次查看脚本的参数,而不需要实际不必要地影响它们 - 这就是这个包装器的全部内容。

$(命令 sub)完成其工作后,生成的exec'd 命令将是:

exec "$0cmd" "${1}" ... ! \( -path "${[num]%/}/media/*" -prune \) "${2}" ...

...其中所有原始参数(如果有)均按原始且未更改的形式按顺序和数字引用(甚至空参数)除了这六个( !,,,,,,, )\(-path​​​"${[num]%/}/media/*"-prune\)/ -ef "${num}"arg 扫描期间每次成功测试都会发生一组插入。否则它会很简单:

exec "$0cmd" "${1}" "${2}" "${3}" "${4}" ...

...其中所有原始参数都以完全相同的方式引用,根本没有插入。

因此,该包装器可以对其包装目标的环境进行的唯一两个可能的修改是:

  • 它将进程名称从它自己的名称更改为它的名称 +cmd。这种事总是会发生。

  • 它可以将每个根匹配的六个参数注入到调用它的参数列表中。

只要第一次修改被认为是可以接受的(虽然这是可以避免的),这里关于行为修改存在一个单点故障 - 即参数插入是否有效。与调用相关的任何错误都超出了范围,应该由包装目标处理,并且该包装器会尝试关注其业务。

arg 处理程序

在命令替换中,我首先初始化要取消设置的变量,因为字符串""是最好的方法不是需要时匹配路径。然后,我声明该chk()函数,然后为while循环的每次迭代调用它,该循环将为$i脚本的每个调用参数增加 1。它将打印$i用引号和大括号括起来的每个增量,前面有一个空格和一个美元符号到命令子的标准输出:

printf ' "${'$i}\"

...

 "${1}"

它循环调用chk()每次迭代获取其参数的副本,然后shift将其删除,直到没有剩余并且循环完成。chk()将其参数与其模式进行比较并采取适当的操作:

  • (-maxdepth"$2") M= ;;

    • $M设置时,最后一个模式只能匹配空字符串,该空字符串只能失败其块的后续​​路径比较测试,等等rt=$rt+!\(在这种情况下,等等永远不会发生。否则什么也不做。

    • POSIX 规范仅要求-[HL]在任何操作数之前被识别[...path...],其他任何操作数均未指定。以下是关于哪些是[...path...]操作数、哪些是测试操作数的说明:

      第一个操作数和后续操作数(直到但不包括以 a 开头的第一个操作数,或者是 a! 或 a ()应被解释为[...path...]操作数。如果第一个操作数以 a 开头,或者是 a! 或 a (,则行为未指定。每个路径操作数都是文件层次结构中起始点的路径名。

  • ([\(!]"$2"|-*"$2")

    • 当前参数是单个(左括号或!感叹号,或者它以-*破折号开头但不是,-maxdepth并且最后一个模式已至少匹配一次。

    • printf %s ${1+%b}%.d "$rt" \\c 2>&- &&

      • 将 - 如果有 -的值写入$rt命令替换的标准输出,然后成功进行转义的零长度写入,或者如果未设置且参数结尾已设置,\c %b则将失败转换为%.d相同且具有相同长度的小数$1到达。此失败将结束while循环。
    • chk(){ ${1+:} exit; }

      • 如果printf成功,则将chk()进行一次且唯一一次尝试修改任何参数。从此时开始,while循环可能会继续处理和打印其余的参数,但chk()不会执行任何操作,直到所有这些参数都用尽为止,此时它将只是exit子 shell。因此,一旦第二个模式匹配一​​次,其他模式就不会再匹配。
  • (-?*)

    • 当前参数至少有两个字符,并以破折号开头。这个图案是更多的-*"$2"它上面的模式一旦$O设置是独占的,因此它只能匹配,直到至少有一个参数匹配它。这样,所有初始选项都将被拆分getopts并与 进行匹配[HPL]。如果任何初始选项不适合该模式,该函数将递归调用自身以匹配其上方的模式并重新定义chk()。通过这种方式,任何未显式处理的 arg 序列仅逐字传递,并对findcmd结果执行任何操作。

    • -[HL]对于与标志变量匹配的每个初始选项,$L设置为空字符串。对于每个匹配的-P $Lunset.

  • (${M-${O=?*}})

    • 第一个出现的不匹配的参数-?*将触发$O被设置为?*模式。此后,前两个模式中的任何一个都可以匹配${O+$2}${2--}。如果ever-maxdepth$2匹配并M=设置为空字符串,则此模式永远不会再匹配另一个非空参数,并且只需要第二个模式的一次匹配即可停止匹配其中任何一个的所有尝试。

    • -[HLP]在第一个选项序列之后和另一个-*或参数之前出现的任何非空参数都[\(?!]与此模式匹配,并测试路径解析。如果$L未设置,则如果是符号链接或无效路径名,则! ! -L "${L-$2}"测试将通过,但否则总是会失败,因为没有路径名可以与空字符串匹配。$2${L=}

    • 只有那些未通过前一个测试的参数才会被检查是否与!否定的 inode 匹配,/并且任何未通过两个测试的参数都将被$rt设置为其自身加上! \( -path "${[num]%/}/media/* -prune \)字符串,该字符串直到第二个模式匹配或到达参数末尾(以先到者为准)才会被写入。

相关内容