find -exec if single and double quotes already in use 的解决方案

Solution for find -exec if single and double quotes already in use

我想递归遍历所有子目录并删除每个名为“bak”的子文件夹中最旧的两个 PDF:

作品:

find . -type d -name "bak" \
  -exec bash -c "cd '{}' && pwd" \;

不起作用,因为双引号已被使用:

find . -type d -name "bak" \
  -exec bash -c "cd '{}' && rm "$(ls -t *.pdf | tail -2)"" \;

双引号难题的任何解决方案?

在双引号字符串中,您可以使用反斜杠来转义其他双引号,例如

find ... "rm \"$(...)\""

如果太复杂使用变量:

cmd='$(...)'
find ... "rm $cmd"

不过,我认为你的 find -exec 问题远不止于此。

  • 在命令字符串 "cd '{}' ..." 中使用 {} 是有风险的。如果文件名中有 ',事情就会中断并可能执行意外命令。
  • $() 将在 find 运行之前扩展 bash。所以 ls -t *.pdf | tail -2 只会在顶级目录 . 中执行一次,而不是对每个找到的目录执行一次。 rm 将(尝试)为每个找到的目录删除相同的文件。
  • 如果 ls 列出了多个文件,
  • rm "$(ls -t *.pdf | tail -2)" 将不起作用。由于引号,两个文件都将列在一个参数中。因此,rm 会尝试删除 一个 名称为 first.pdf\nsecond.pdf.
  • 的文件

我建议

cmd='cd "" && ls -t *.pdf | tail -n2 | sed "s/./\\&/g" | xargs rm'
find . -type d -name bak -exec bash -c "$cmd" -- {} \;

您明确要求find -exec。通常我只是连接 find -exec find -delete 但在你的情况下只应删除两个文件。因此唯一的方法是 运行 subshel​​l。 Socowi 已经给出了很好的解决方案,但是如果您的文件名不包含制表符或换行符,另一个解决方法是 find while read loop.

这将按 mtime 对文件进行排序

find . -type d -iname 'bak' | \
while read -r dir;
  do
    find "$dir" -maxdepth 1 -type f -iname '*.pdf' -printf "%T+\t%p\n" | \
    sort | head -n2 | \
    cut -f2- | \
    while read -r file;
      do
        rm "$file";
    done;
done;

上面的 find while read 循环为“一行”

find . -type d -iname 'bak' | while read -r dir; do find "$dir" -maxdepth 1 -type f -iname '*.pdf' -printf "%T+\t%p\n" | sort | head -n2 | cut -f2- | while read -r file; do rm "$file"; done; done;

find while read循环也可以处理以NUL结尾的文件名。但是 head 无法处理这个问题,所以我确实改进了其他答案并使其适用于非平凡的文件名(仅 GNU + bash)


'realpath'替换为rm

#!/bin/bash

rm_old () {
  find "" -maxdepth 1 -type f -iname \*. -printf "%T+\t%p[=12=]" | sort -z | sed -zn 's,\S*\t\(.*\),,p' | grep -zim \.$ | xargs -0r realpath
}

export -f rm_old

find -type d -iname bak -execdir bash -c 'rm_old "{}" pdf 2' \;

然而 bash -c 可能仍然可以利用,为了使其更安全让 stat %N 进行引用

#!/bin/bash

rm_old () {
  local dir=""

# we don't like eval
#  eval "dir=$dir"

  # this works like eval
  dir="${dir#?}"
  dir="${dir%?}"
  dir="${dir//"'$'\t''"/$'1'}"
  dir="${dir//"'$'\n''"/$'2'}"
  dir="${dir//$'7'\$'7'$'7'/$'7'}"

  find "$dir" -maxdepth 1 -type f -iname \*. -printf '%T+\t%p[=13=]' | sort -z | sed -zn 's,\S*\t\(.*\),,p' | grep -zim \.$ | xargs -0r realpath
}

find -type d -iname bak -exec stat -c'%N' {} + | while read -r dir; do rm_old "$dir" pdf 2; done

你有一个更根本的问题;因为您在整个脚本周围使用较弱的双引号,所以 $(...) 命令替换将由解析 find 命令的 shell 解释,而不是由 bash shell 你正在开始,它只会收到一个包含命令替换结果的静态字符串。

如果您在脚本周围切换为单引号,则 大部分 都是正确的;但如果您找到的文件名包含双引号,那仍然会失败(就像您尝试使用单引号的文件名会失败一样)。正确的解决方法是将匹配的文件作为命令行参数传递给 bash 子进程。

但更好的解决方法仍然是使用 -execdir,这样您就不必将目录名称传递给子 shell:

find . -type d -name "bak" \
  -execdir bash -c 'ls -t *.pdf | tail -2 | xargs -r rm' \;

这可能会以有趣的方式失败,因为你 parsing ls 本身就是有问题的。