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
但在你的情况下只应删除两个文件。因此唯一的方法是 运行 subshell。 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
本身就是有问题的。
我想递归遍历所有子目录并删除每个名为“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)"
将不起作用。由于引号,两个文件都将列在一个参数中。因此,rm
会尝试删除 一个 名称为first.pdf\nsecond.pdf
. 的文件
ls
列出了多个文件,我建议
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
但在你的情况下只应删除两个文件。因此唯一的方法是 运行 subshell。 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
本身就是有问题的。