有没有一种简单的方法,在带有 bash 的非常标准的 UNIX 环境中,运行命令从目录中删除除最新 X 文件之外的所有文件?
举一个更具体的例子,想象一下某个 cron 作业每小时将一个文件(例如,一个日志文件或一个 tar-ed 备份)写入一个目录。我想要一种运行另一个 cron 作业的方法,该作业将删除该目录中最旧的文件,直到少于 5 个。
为了清楚起见,只有一个文件存在,它永远不应该被删除。
现有答案的问题:
rm
直接在未引用的命令替换 ( rm `...`
) 上调用,则会增加意外通配的风险。rm
到目录将失败)。wnoise 的答案解决了这些问题,但解决方案是特定于GNU的(并且非常复杂)。
这是一个实用的、符合 POSIX 的解决方案,它只有一个警告:它不能处理带有嵌入换行符的文件名——但我认为这对大多数人来说并不是现实世界的担忧。
作为记录,这里解释了为什么解析ls
输出通常不是一个好主意:http: //mywiki.wooledge.org/ParsingLs
ls -tp | grep -v '/$' | tail -n +6 | xargs -I {} rm -- {}
注意:该命令在当前目录下运行;要明确定位目录,请使用带有以下命令的子shell( )(...)
cd
:
(cd /path/to && ls -tp | grep -v '/$' | tail -n +6 | xargs -I {} rm -- {})
这同样适用于以下命令。
以上是低效的,因为必须为每个文件名单独xargs
调用。
但是,您平台的具体实现可能允许您解决此问题:rm
xargs
一个适用于GNU xargs
的解决方案是使用-d '\n'
,这使得xargs
将每个输入行视为一个单独的参数,但同时传递尽可能多的参数以适合命令行:
ls -tp | grep -v '/$' | tail -n +6 | xargs -d '\n' -r rm --
注意: Option -r
( --no-run-if-empty
) 确保在没有 inputrm
时不会调用它。
一种适用于GNU 和 xargs
BSD ( xargs
包括在macOS上)的解决方案——尽管在技术上仍然不符合 POSIX 标准——是在首先将换行符转换为( ) 字符之后使用 -separated 输入,这也传递(通常)所有文件-0
名一次:NUL
NUL
0x0
ls -tp | grep -v '/$' | tail -n +6 | tr '\n' '\0' | xargs -0 rm --
解释:
ls -tp
打印文件系统项的名称,按最近修改的时间排序,降序排列(最近修改的项在前)(-t
),打印的目录用尾随/
标记它们(-p
)。
ls -tp
始终只输出文件/目录名称,而不是完整路径,因此需要上述子shell 方法来定位当前目录以外的目录((cd /path/to && ls -tp ...)
)。grep -v '/$'
然后通过省略-v
带有尾随/
( /$
) 的 ( ) 行,从结果列表中清除目录。
tail -n +6
跳过列表中的前5个条目,实际上返回除5个最近修改的文件(如果有)之外的所有文件。
请注意,为了排除N
文件,N+1
必须传递给tail -n +
.
xargs -I {} rm -- {}
(及其变体)然后rm
在所有这些文件上调用;如果根本没有匹配项,xargs
则不会执行任何操作。
xargs -I {} rm -- {}
定义将每个输入行作为一个整体{}
来表示的占位符,因此对于每个输入行调用一次,但正确处理带有嵌入空格的文件名。rm
--
在所有情况下,确保所有以 . 开头的文件名-
不会被.rm
原始问题的变体,以防匹配文件需要单独处理或收集在 shell 数组中:
# One by one, in a shell loop (POSIX-compliant):
ls -tp | grep -v '/$' | tail -n +6 | while IFS= read -r f; do echo "$f"; done
# One by one, but using a Bash process substitution (<(...),
# so that the variables inside the `while` loop remain in scope:
while IFS= read -r f; do echo "$f"; done < <(ls -tp | grep -v '/$' | tail -n +6)
# Collecting the matches in a Bash *array*:
IFS=$'\n' read -d '' -ra files < <(ls -tp | grep -v '/$' | tail -n +6)
printf '%s\n' "${files[@]}" # print array elements
删除目录中除 5 个(或任何数量)最近的文件之外的所有文件。
rm `ls -t | awk 'NR>5'`
(ls -t|head -n 5;ls)|sort|uniq -u|xargs rm
此版本支持带空格的名称:
(ls -t|head -n 5;ls)|sort|uniq -u|sed -e 's,.*,"&",g'|xargs rm
thelsdj 答案的更简单变体:
ls -tr | head -n -5 | xargs --no-run-if-empty rm
ls -tr 显示所有文件,最旧的在前(-t 最新的在前,-r 反向)。
head -n -5 显示除最后 5 行之外的所有行(即 5 个最新文件)。
xargs rm 为每个选定的文件调用 rm。
find . -maxdepth 1 -type f -printf '%T@ %p\0' | sort -r -z -n | awk 'BEGIN { RS="\0"; ORS="\0"; FS="" } NR > 5 { sub("^[0-9]*(.[0-9]*)? ", ""); print }' | xargs -0 rm -f
-printf 需要 GNU find,-z 需要 GNU sort,“\0”需要 GNU awk,-0 需要 GNU xargs,但处理带有嵌入换行符或空格的文件。
当当前目录中有目录时,所有这些答案都会失败。这是可行的:
find . -maxdepth 1 -type f | xargs -x ls -t | awk 'NR>5' | xargs -L1 rm
这:
当当前目录中有目录时工作
即使无法删除前一个文件(由于权限等),也会尝试删除每个文件
当当前目录中的文件数量过多并且xargs
通常会搞砸你时,安全失败(the -x
)
不适合文件名中的空格(也许您使用了错误的操作系统?)
ls -tQ | tail -n+4 | xargs rm
按修改时间列出文件名,引用每个文件名。排除前 3 个(最近的 3 个)。删除剩余。
在 mklement0 的有用评论后编辑(谢谢!):更正了 -n+3 参数,并注意如果文件名包含换行符和/或目录包含子目录,这将无法按预期工作。
忽略换行符就是忽略安全性和良好的编码。wnoise 有唯一的好答案。这是他的一个变体,将文件名放在数组 $x 中
while IFS= read -rd ''; do
x+=("${REPLY#* }");
done < <(find . -maxdepth 1 -printf '%T@ %p\0' | sort -r -z -n )
如果文件名没有空格,这将起作用:
ls -C1 -t| awk 'NR>5'|xargs rm
如果文件名确实有空格,例如
ls -C1 -t | awk 'NR>5' | sed -e "s/^/rm '/" -e "s/$/'/" | sh
基本逻辑:
我意识到这是一个旧线程,但也许有人会从中受益。此命令将在当前目录中查找文件:
for F in $(find . -maxdepth 1 -type f -name "*_srv_logs_*.tar.gz" -printf '%T@ %p\n' | sort -r -z -n | tail -n+5 | awk '{ print $2; }'); do rm $F; done
这比以前的一些答案更强大,因为它允许将您的搜索域限制为匹配表达式的文件。首先,找到符合您想要的任何条件的文件。打印那些带有时间戳的文件。
find . -maxdepth 1 -type f -name "*_srv_logs_*.tar.gz" -printf '%T@ %p\n'
接下来,按时间戳对它们进行排序:
sort -r -z -n
然后,从列表中删除 4 个最近的文件:
tail -n+5
抓取第二列(文件名,而不是时间戳):
awk '{ print $2; }'
然后将整个事情包装成一个 for 语句:
for F in $(); do rm $F; done
这可能是一个更冗长的命令,但我更幸运能够定位条件文件并针对它们执行更复杂的命令。
用 zsh
假设您不关心当前目录并且您的文件不会超过 999 个(如果需要,请选择更大的数字,或创建一个 while 循环)。
[ 6 -le `ls *(.)|wc -l` ] && rm *(.om[6,999])
在*(.om[6,999])
中,.
表示文件,o
表示排序顺序,m
表示按修改日期(放置a
访问时间或c
inode 更改),[6,999]
选择文件范围,所以不先 rm 5。
在 Sed-Onliners 中发现了有趣的 cmd - 删除最后 3 行 - 发现它非常适合另一种给猫剥皮的方法(好吧不是),但想法:
#!/bin/bash
# sed cmd chng #2 to value file wish to retain
cd /opt/depot
ls -1 MyMintFiles*.zip > BigList
sed -n -e :a -e '1,2!{P;N;D;};N;ba' BigList > DeList
for i in `cat DeList`
do
echo "Deleted $i"
rm -f $i
#echo "File(s) gonzo "
#read junk
done
exit 0
删除除 10 个最新(最新)文件之外的所有文件
ls -t1 | head -n $(echo $(ls -1 | wc -l) - 10 | bc) | xargs rm
如果少于 10 个文件,没有文件被删除,您将有:错误头:非法行数 -- 0
我需要一个针对busybox(路由器)的优雅解决方案,所有xargs 或数组解决方案对我来说都是无用的——那里没有这样的命令可用。find 和 mtime 不是正确的答案,因为我们谈论的是 10 项而不一定是 10 天。埃斯波的回答是最短、最干净的,而且可能是最通用的。
空格错误和不删除文件时都可以通过标准方法简单地解决:
rm "$(ls -td *.tar | awk 'NR>7')" 2>&-
更具教育意义的版本:如果我们以不同的方式使用 awk,我们可以做到这一切。通常,我使用这种方法将变量从 awk 传递(返回)到 sh。正如我们一直在阅读无法做到的那样,我不敢苟同:这是方法。
.tar 文件示例,文件名中的空格没有问题。要进行测试,请将“rm”替换为“ls”。
eval $(ls -td *.tar | awk 'NR>7 { print "rm \"" $0 "\""}')
解释:
ls -td *.tar
列出按时间排序的所有 .tar 文件。要应用于当前文件夹中的所有文件,请删除“d *.tar”部分
awk 'NR>7...
跳过前 7 行
print "rm \"" $0 "\""
构造一行:rm“文件名”
eval
执行它
由于我们使用的是rm
,我不会在脚本中使用上述命令!更明智的用法是:
(cd /FolderToDeleteWithin && eval $(ls -td *.tar | awk 'NR>7 { print "rm \"" $0 "\""}'))
在使用ls -t
命令的情况下不会对诸如:touch 'foo " bar'
和touch 'hello * world'
. 并不是说我们在现实生活中曾经创建过具有此类名称的文件!
边注。如果我们想以这种方式将变量传递给 sh,我们只需修改 print(简单的形式,不允许有空格):
print "VarName="$1
将变量设置VarName
为 的值$1
。可以一次性创建多个变量。这VarName
成为一个普通的 sh 变量,之后可以在脚本或 shell 中正常使用。因此,要使用 awk 创建变量并将它们返回给 shell:
eval $(ls -td *.tar | awk 'NR>7 { print "VarName=\""$1"\"" }'); echo "$VarName"
leaveCount=5
fileCount=$(ls -1 *.log | wc -l)
tailCount=$((fileCount - leaveCount))
# avoid negative tail argument
[[ $tailCount < 0 ]] && tailCount=0
ls -t *.log | tail -$tailCount | xargs rm -f
我把它做成了一个 bash shell 脚本。用法:keep NUM DIR
其中 NUM 是要保留的文件数,DIR 是要清理的目录。
#!/bin/bash
# Keep last N files by date.
# Usage: keep NUMBER DIRECTORY
echo ""
if [ $# -lt 2 ]; then
echo "Usage: $0 NUMFILES DIR"
echo "Keep last N newest files."
exit 1
fi
if [ ! -e $2 ]; then
echo "ERROR: directory '$1' does not exist"
exit 1
fi
if [ ! -d $2 ]; then
echo "ERROR: '$1' is not a directory"
exit 1
fi
pushd $2 > /dev/null
ls -tp | grep -v '/' | tail -n +"$1" | xargs -I {} rm -- {}
popd > /dev/null
echo "Done. Kept $1 most recent files in $2."
ls $2|wc -l