如何使用“find”命令自动转义shell元字符?

如何使用“find”命令自动转义shell元字符?

我在目录树下有一堆 XML 文件,我想将它们移动到同一目录树中具有相同名称的相应文件夹中。

这是示例结构(在 shell 中):

touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml"
mkdir -p foo bar "foo/[ foo ]" "bar/( bar )"

所以我这里的方法是:

find . -name "*.xml" -exec sh -c '
  DST=$(
    find . -type d -name "$(basename "{}" .xml)" -print -quit
  )
  [ -d "$DST" ] && mv -v "{}" "$DST/"' ';'

给出以下输出:

‘./( bar ).xml’ -> ‘./bar/( bar )/( bar ).xml’
mv: ‘./bar/( bar )/( bar ).xml’ and ‘./bar/( bar )/( bar ).xml’ are the same file
‘./bar.xml’ -> ‘./bar/bar.xml’
‘./foo.xml’ -> ‘./foo/foo.xml’

但带有方括号 ( ) 的文件[ foo ].xml并未被移动,就好像它已被忽略一样。

我已经检查并basename(例如basename "[ foo ].xml" ".xml")正确转换文件,但是find括号有问题。例如:

find . -name '[ foo ].xml'

将无法正确找到该文件。但是,当转义括号 ( '\[ foo \].xml') 时,它可以正常工作,但不能解决问题,因为它是脚本的一部分,我不知道哪些文件具有这些特殊(shell?)字符。使用 BSD 和 GNU 进行了测试find

find使用with 的参数时是否有任何通用的方法来转义文件名-name,以便我可以更正我的命令以支持带有元字符的文件?

答案1

在这里使用 glob 就容易多了zsh

for f (**/*.xml(.)) (mv -v -- $f **/$f:r:t(/[1]))

或者,如果您想包含隐藏的 xml 文件并查看隐藏目录,如下find所示:

for f (**/*.xml(.D)) (mv -v -- $f **/$f:r:t(D/[1]))

.xml但请注意,名为, ..xmlor的文件...xml会成为问题,因此您可能需要排除它们:

setopt extendedglob
for f (**/(^(|.|..)).xml(.D)) (mv -v -- $f **/$f:r:t(D/[1]))

使用 GNU 工具,避免扫描每个文件的整个目录树的另一种方法是扫描一次并查找所有目录和xml文件,记录它们的位置并最后进行移动:

(export LC_ALL=C
find . -mindepth 1 -name '*.xml' ! -name .xml ! \
  -name ..xml ! -name ...xml -type f -printf 'F/%P\0' -o \
  -type d -printf 'D/%P\0' | awk -v RS='\0' -F / '
  {
    if ($1 == "F") {
      root = $NF
      sub(/\.xml$/, "", root)
      F[root] = substr($0, 3)
    } else D[$NF] = substr($0, 3)
  }
  END {
    for (f in F)
      if (f in D) 
        printf "%s\0%s\0", F[f], D[f]
  }' | xargs -r0n2 mv -v --
)

如果您想允许任意文件名,您的方法会存在许多问题:

  • 嵌入{}到 shell 代码中的是总是错误的。$(rm -rf "$HOME").xml例如,如果有一个文件被调用怎么办?正确的方法是将它们{}作为参数传递给内联 shell 脚本 ( -exec sh -c 'use as "$1"...' sh {} \;)。
  • 使用 GNU find(此处隐含为您使用的-quit),*.xml将仅匹配由一系列有效字符组成的文件,后跟.xml,以便排除在当前语言环境中包含无效字符的文件名(例如错误字符集中的文件名)。解决这个问题的方法是将区域设置修复为C每个字节都是有效字符的位置(这意味着错误消息将以英语显示)。
  • 如果这些xml文件中的任何一个是目录或符号链接类型,则会导致问题(影响目录扫描,或在移动时破坏符号链接)。您可能想添加一个-type f仅移动常规文件。
  • 命令替换 ( $(...)) 条带全部尾随换行符。这会导致名为foo␤.xml例如的文件出现问题。解决这个问题是可能的,但很痛苦:base=$(basename "$1" .xml; echo .); base=${base%??}。至少可以basename${var#pattern}运算符替换。并尽可能避免命令替换。
  • 您的问题是文件名包含通配符(?[*反斜杠;它们对于 shell 来说并不是特殊的,而是对于模式匹配 ( fnmatch()) 来说find是特殊的,它恰好与 shell 模式匹配非常相似)。你需要用反斜杠来转义它们。
  • .xml上面提到的, ..xml,的问题...xml

因此,如果我们解决上述所有问题,我们最终会得到如下结果:

LC_ALL=C find . -type f -name '*.xml' ! -name .xml ! -name ..xml \
  ! -name ...xml -exec sh -c '
  for file do
    base=${file##*/}
    base=${base%.xml}
    escaped_base=$(printf "%s\n" "$base" |
      sed "s/[[*?\\\\]/\\\\&/g"; echo .)
    escaped_base=${escaped_base%??}
    find . -name "$escaped_base" -type d -exec mv -v "$file" {\} \; -quit
  done' sh {} +

呼……

现在,这还不是全部。有了-exec ... {} +,我们就sh可以尽可能少地运行。如果幸运的话,我们将只运行一个,但如果不是,在第一次sh调用之后,我们将移动许多 xml文件,然后find将继续寻找更多文件,并且很可能找到我们拥有的文件再次进入第一轮(并且很可能尝试将它们移动到原来的位置)。

除此之外,它与 zsh 的方法基本相同。其他一些显着差异:

  • 对于zsh第一个,文件列表是排序的(按目录名和文件名),因此目标目录或多或少是一致的和可预测的。对于find,它基于目录中文件的原始顺序。
  • 使用zsh,如果没有找到将文件移动到的匹配目录,您将收到一条错误消息,而不是使用find上面的方法。
  • 使用 时find,如果某些目录无法遍历,您将收到错误消息,而使用 时则不会zsh

最后一个警告。如果您获得一些文件名不可靠的文件的原因是因为对手可以写入目录树,那么请注意,如果对手可能会在该命令的脚下重命名文件,那么上述解决方案都不安全。

例如,如果您使用 LXDE,攻击者可以创建一个恶意文件foo/lxde-rc.xml,创建一个lxde-rc文件夹,检测您何时运行命令,并将其替换lxde-rc为比赛窗口期间的符号链接~/.config/openbox/(可以根据需要将其设置得尽可能大)在很多方面)在find找到它lxde-rcmv执行rename("foo/lxde-rc.xml", "lxde-rc/lxde-rc.xml")foo也可以更改为该符号链接,使您移动到lxde-rc.xml其他地方)之间。

使用标准甚至 GNU 实用程序来解决这个问题可能是不可能的,您需要用适当的编程语言编写它,进行一些安全的目录遍历并使用renameat()系统调用。

如果目录树足够深,达到了rename()系统调用的路径长度限制(导致失败并显示),则上述所有解决方案也将失败。使用的解决方案也可以解决该问题。mvrename()ENAMETOOLONGrenameat()

答案2

当您将内联脚本与 一起使用时find ... -exec sh -c ...,您应该find通过位置参数将结果传递给 shell,这样您就不必{}在内联脚本中的任何地方使用。

如果有bashor zsh,您可以basename通过以下方式传递输出printf '%q'

find . -name "*.xml" -exec bash -c '
  for f do
    BASENAME="$(printf "%q" "$(basename -- "$f" .xml)")"
    DST=$(find . -type d -name "$BASENAME" -print -quit)
    [ -d "$DST" ] && mv -v -- "$f" "$DST/"
  done
' bash {} +

有了bash,您就可以使用printf -v BASENAME,并且如果文件名包含控制字符或非 ascii 字符,则此方法将无法正常工作。

如果你想让它正常工作,你需要编写一个shell函数来仅转义[、、*?反斜杠。

答案3

好消息:

find . -name '[ foo ].xml'

不被 shell 解释,它通过这种方式传递给 find 程序。然而,Find 将参数解释为-name一种glob模式,这一点需要考虑在内。

如果您喜欢调用find -exec \;或更好find -exec +,则不涉及 shell。

如果您想处理findshell 的输出,我建议您通过set -f在相关代码之前调用来禁用 shell 中的文件名通配符,并通过set +f稍后调用来再次打开它。

答案4

以下是一个相对简单、符合 POSIX 标准的管道。它会扫描层次结构两次,首先扫描目录,然后扫描 *.xml 常规文件。扫描之间的空行表示转换的 AWK 信号。

AWK 组件将基本名称映射到目标目录(如果存在多个具有相同基本名称的目录,则仅记住第一次遍历)。对于每个 *.xml 文件,它会打印一个制表符分隔的行,其中包含两个字段:1) 文件的路径和 2) 相应的目标目录。

{
    find . -type d
    echo
    find . -type f -name \*.xml
} |
awk -F/ '
    !NF { ++i; next }
    !i && !($NF".xml" in d) { d[$NF".xml"] = $0 }
    i { print $0 "\t" d[$NF] }
' |
while IFS='     ' read -r f d; do
    mv -- "$f" "$d"
done

在读取之前分配给 IFS 的值是文本制表符,而不是空格。

这是使用原始问题的 touch/mkdir 框架的文字记录:

$ touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml"
$ mkdir -p foo bar "foo/[ foo ]" "bar/( bar )"
$ find .
.
./foo
./foo/[ foo ]
./bar.xml
./foo.xml
./bar
./bar/( bar )
./[ foo ].xml
./( bar ).xml
$ ../mv-xml.sh
$ find .
.
./foo
./foo/[ foo ]
./foo/[ foo ]/[ foo ].xml
./foo/foo.xml
./bar
./bar/( bar )
./bar/( bar )/( bar ).xml
./bar/bar.xml

相关内容