find -exec 如果单引号和双引号已经在使用的解决方案

问题描述

我想递归遍历所有子目录并删除每个名为“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 将(尝试)为每个找到的目录删除相同的文件。
  • 如果 rm "$(ls -t *.pdf | tail -2)" 列出多个文件,
  • ls 将不起作用。由于引号,两个文件都将列在一个参数中。因此,rm 会尝试删除 一个 名为 first.pdf\nsecond.pdf 的文件。

我建议

cmd='cd "$1" && 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 但在你的情况下只应该删除两个文件。因此唯一的方法是运行子shell。 Socowi 已经提供了很好的解决方案,但是如果您的文件名不包含制表符或换行符,另一种解决方法是 find while read 循环。

这将按 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 循环为“one-liner”

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 "$1" -maxdepth 1 -type f -iname \*.$2 -printf "%T+\t%p\0" | sort -z | sed -zn 's,\S*\t\(.*\),\1,p' | grep -zim$3 \.$2$ | 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="$1"

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

  # this works like eval
  dir="${dir#?}"
  dir="${dir%?}"
  dir="${dir//"'$'\t''"/$'\011'}"
  dir="${dir//"'$'\n''"/$'\012'}"
  dir="${dir//$'\047'\\$'\047'$'\047'/$'\047'}"

  find "$dir" -maxdepth 1 -type f -iname \*.$2 -printf '%T+\t%p\0' | sort -z | sed -zn 's,p' | grep -zim$3 \.$2$ | 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,这本身就有缺陷。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...