177

有没有一种简单的方法,在带有 bash 的非常标准的 UNIX 环境中,运行命令从目录中删除除最新 X 文件之外的所有文件?

举一个更具体的例子,想象一下某个 cron 作业每小时将一个文件(例如,一个日志文件或一个 tar-ed 备份)写入一个目录。我想要一种运行另一个 cron 作业的方法,该作业将删除该目录中最旧的文件,直到少于 5 个。

为了清楚起见,只有一个文件存在,它永远不应该被删除。

4

16 回答 16

157

现有答案的问题:

  • 无法处理带有嵌入空格或换行符的文件名。
    • 如果解决方案rm直接在未引用的命令替换 ( rm `...`) 上调用,则会增加意外通配的风险。
  • 无法区分文件和目录(即,如果目录恰好位于最近修改的 5 个文件系统项中,那么您实际上保留的文件少于5 个,并且应用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一次NULNUL0x0

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
于 2016-01-18T19:24:33.597 回答
119

删除目录中除 5 个(或任何数量)最近的文件之外的所有文件。

rm `ls -t | awk 'NR>5'`
于 2008-08-25T08:41:24.227 回答
89
(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
于 2008-08-25T08:42:05.487 回答
68

thelsdj 答案的更简单变体:

ls -tr | head -n -5 | xargs --no-run-if-empty rm 

ls -tr 显示所有文件,最旧的在前(-t 最新的在前,-r 反向)。

head -n -5 显示除最后 5 行之外的所有行(即 5 个最新文件)。

xargs rm 为每个选定的文件调用 rm。

于 2012-04-12T08:25:06.247 回答
18
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,但处理带有嵌入换行符或空格的文件。

于 2008-11-18T19:51:39.977 回答
14

当当前目录中有目录时,所有这些答案都会失败。这是可行的:

find . -maxdepth 1 -type f | xargs -x ls -t | awk 'NR>5' | xargs -L1 rm

这:

  1. 当当前目录中有目录时工作

  2. 即使无法删除前一个文件(由于权限等),也会尝试删除每个文件

  3. 当当前目录中的文件数量过多并且xargs通常会搞砸你时,安全失败(the -x

  4. 不适合文件名中的空格(也许您使用了错误的操作系统?)

于 2008-11-18T17:44:25.383 回答
13
ls -tQ | tail -n+4 | xargs rm

按修改时间列出文件名,引用每个文件名。排除前 3 个(最近的 3 个)。删除剩余。

在 mklement0 的有用评论后编辑(谢谢!):更正了 -n+3 参数,并注意如果文件名包含换行符和/或目录包含子目录,这将无法按预期工作。

于 2013-07-25T05:51:10.667 回答
8

忽略换行符就是忽略安全性和良好的编码。wnoise 有唯一的好答案。这是他的一个变体,将文件名放在数组 $x 中

while IFS= read -rd ''; do 
    x+=("${REPLY#* }"); 
done < <(find . -maxdepth 1 -printf '%T@ %p\0' | sort -r -z -n )
于 2009-06-13T12:03:34.263 回答
4

如果文件名没有空格,这将起作用:

ls -C1 -t| awk 'NR>5'|xargs rm

如果文件名确实有空格,例如

ls -C1 -t | awk 'NR>5' | sed -e "s/^/rm '/" -e "s/$/'/" | sh

基本逻辑:

  • 按时间顺序获取文件列表,一列
  • 获取除前 5 个以外的所有内容(本例中 n=5)
  • 第一个版本:将那些发送到 rm
  • 第二个版本:生成将正确删除它们的脚本
于 2008-08-25T08:43:44.830 回答
4

我意识到这是一个旧线程,但也许有人会从中受益。此命令将在当前目录中查找文件:

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

这可能是一个更冗长的命令,但我更幸运能够定位条件文件并针对它们执行更复杂的命令。

于 2017-01-10T22:55:39.533 回答
2

用 zsh

假设您不关心当前目录并且您的文件不会超过 999 个(如果需要,请选择更大的数字,或创建一个 while 循环)。

[ 6 -le `ls *(.)|wc -l` ] && rm *(.om[6,999])

*(.om[6,999])中,.表示文件,o表示排序顺序,m表示按修改日期(放置a访问时间或cinode 更改),[6,999]选择文件范围,所以不先 rm 5。

于 2011-11-21T18:08:20.997 回答
1

在 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
于 2016-09-01T21:09:57.413 回答
1

删除除 10 个最新(最新)文件之外的所有文件

ls -t1 | head -n $(echo $(ls -1 | wc -l) - 10 | bc) | xargs rm

如果少于 10 个文件,没有文件被删除,您将有:错误头:非法行数 -- 0

用 bash 计数文件

于 2017-05-29T17:14:56.100 回答
1

我需要一个针对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"
于 2018-10-03T17:48:32.400 回答
0
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
于 2013-06-07T07:34:08.353 回答
0

我把它做成了一个 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
于 2016-02-03T17:47:32.550 回答