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

ken*_*orb 4 find shell-script quoting wildcards

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

这是示例结构(在外壳中):

touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml"
mkdir -p foo bar "foo/[ foo ]" "bar/( bar )"
Run Code Online (Sandbox Code Playgroud)

所以我的方法是:

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

这给出了以下输出:

‘./( 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’
Run Code Online (Sandbox Code Playgroud)

但是带有方括号 ( [ foo ].xml)的文件并没有像被忽略一样移动。

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

find . -name '[ foo ].xml'
Run Code Online (Sandbox Code Playgroud)

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

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

Sté*_*las 7

使用zshglobs 这里要容易得多:

for f (**/*.xml(.)) (mv -v -- $f **/$f:r:t(/[1]))
Run Code Online (Sandbox Code Playgroud)

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

for f (**/*.xml(.D)) (mv -v -- $f **/$f:r:t(D/[1]))
Run Code Online (Sandbox Code Playgroud)

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

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

使用 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 --
)
Run Code Online (Sandbox Code Playgroud)

如果您想允许任意文件名,您的方法有很多问题:

  • 嵌入{}到 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}操作符替换。并尽可能避免命令替换。
  • 您的问题,包含通配符的字符的文件名(?[*和反斜线;他们不是特别的外壳,但对模式匹配(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 {} +
Run Code Online (Sandbox Code Playgroud)

呼...

现在,这还不是全部。使用-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()给完成的系统调用的路径长度限制mv(导致rename()失败ENAMETOOLONG),则上述所有解决方案也将失败。使用的解决方案renameat()也可以解决该问题。