问题描述
我想递归遍历所有子目录并删除每个名为“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
会尝试删除 一个 名为first.pdf\nsecond.pdf
的文件。
rm "$(ls -t *.pdf | tail -2)"
列出多个文件,我建议
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
,这本身就有缺陷。