删除bash中除最新X文件以外的所有文件


157

在带有bash的漂亮标准UNIX环境中,是否有一种简单的方法来运行命令以从目录中删除除最近的X文件以外的所有文件?

再举一个具体的例子,想象一下某些cron作业每小时将一个文件(例如,日志文件或已压缩的备份)写到目录中。我希望有一种方法可以运行另一个cron作业,该作业将删除该目录中最旧的文件,直到少于5个文件为止。

需要明确的是,只有一个文件存在,永远不要删除它。

Answers:


117

现有答案存在的问题:

  • 无法处理带有嵌入式空格或换行符的文件名。
    • 如果解决方案rm直接在无引号的命令替换(rm `...`)上调用,则存在意外滚动的风险。
  • 无法区分文件和目录(即,如果目录恰好是最近修改的5个文件系统项之一,则您实际上将保留少于 5个文件,并且应用于rm目录将失败)。

wnoise的答案解决了这些问题,但是解决方案是特定于GNU的(而且相当复杂)。

这是一个实用的,符合POSIX的解决方案,只有一个警告:它无法处理带有嵌入式换行符的文件名-但我认为对于大多数人来说,这并不是现实问题。

作为记录,以下是为什么通常不是解析ls输出的好主意的说明:http : //mywiki.wooledge.org/ParsingLs

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 --

-r--no-run-if-empty)确保rm在没有输入的情况下不会调用它。

如果你有BSD xargs(包括Mac系统),你可以使用-0处理NUL-分隔输入,经过第一平移换行至NUL0x0)字符,这也传递(典型值)的所有文件名。在一次(也将与GNU工作xargs):

ls -tp | grep -v '/$' | tail -n +6 | tr '\n' '\0' | xargs -0 rm --

说明:

  • ls -tp打印文件系统项的名称,该文件系统项的名称按降序排列(降序排列)(首先是最近修改的项)(-t),并在目录上打印尾随/以将其标记为-p
  • grep -v '/$'然后通过省略(-v)带有尾随//$)的行,从结果列表中清除目录。
    • 警告:由于指向目录符号链接从技术上说本身并不是目录,因此不会排除此类符号链接。
  • tail -n +6跳过前5个的上市项目,实际上返回所有,但 5个最近修改的文件,如果有的话。
    请注意,要排除N文件,N+1必须将传递给tail -n +
  • xargs -I {} rm -- {}(及其变体)然后rm在所有这些文件上调用;如果根本没有匹配项,xargs则不会执行任何操作。
    • xargs -I {} rm -- {}定义占位符{},该占位符代表每个输入行作为一个整体,因此rm将为每个输入行调用一次,但使用正确处理了嵌入空格的文件名。
    • --在任何情况下确保了发生在开始任何文件名-是不误选项通过rm

变化上的原始问题,在情况下,匹配的文件需要被处理单独收集在壳阵列

# 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

2
肯定比这里的大多数其他答案好,因此,即使我考虑忽略换行符情况也必须谨慎行事,因此我很乐意给予支持。
Charles Duffy

2
如果您ls不在当前目录中,则文件的路径将包含“ /”,这意味着grep -v '/'不匹配任何内容。我相信grep -v '/$'您只想排除目录。
waldol1 2016年

1
@ waldol1:谢谢;我已经将答案更新为包含您的建议,这也使grep命令在概念上更加清晰。但是请注意,您所描述的问题不会只用一个目录路径就可以解决。例如,ls -p /private/var仍然只打印纯文件名。只有传递了多个文件参数(通常通过glob),您才会在输出中看到实际的路径。例如,ls -p /private/var/*(除非您也包含,否则您还将看到匹配子目录的内容-d)。
mklement0 '16

108

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

rm `ls -t | awk 'NR>5'`

2
我只需要考虑存档文件即可。更改ls -tls -td *.bz2
James T Snell 2014年

3
我通过将其更改为rm -rf来用于目录ls -t | awk 'NR>1'(我只想要最新的)。谢谢!
lohiaguitar91 2014年

11
ls -t | awk 'NR>5' | xargs rm -f 如果您更喜欢管道,并且如果没有要删除的内容,则需要抑制该错误。
H2ONaCl 2014年

16
简洁,易读,但使用起来很危险;如果尝试删除使用创建的文件touch 'hello * world',这将绝对删除当前目录中的所有内容
Charles Duffy

1
即使在2008年得到了答复,它的作用还是很吸引人,正是我需要从特定目录中删除旧备份所需要的。太棒了
Rens Tillmann

86
(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

20
此命令将无法正确处理名称中带有空格的文件。
tylerl

5
(ls -t|head -n 5;ls)是一个命令组。它将5个最新文件打印两次。sort将相同的线放在一起。uniq -u删除重复项,以便保留除5个最新文件以外的所有文件。xargs rm呼吁rm他们每个人。
Fabien 2014年

15
如果您的文件数少于或等于5,则会删除所有文件!加入--no-run-if-emptyxargs(ls -t|head -n 5;ls)|sort|uniq -u|xargs --no-run-if-empty rm请更新答案。
Gonfi den Tschal,2015年

3
甚至“用空格支持名称”也是危险的。考虑一个包含文字引号的名称:touch 'foo " bar'将抛出命令的其余部分。
Charles Duffy

2
... xargs -d $'\n'比将引号插入您的内容更安全,尽管NUL分隔输入流(这需要使用其他方法ls才能真正做到正确)是理想的选择。
Charles Duffy

59

thelsdj答案的更简单变体:

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

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

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

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


15
需要在xargs中添加--no-run-if-empty,以便在文件少于5个时不会失败。
汤姆

ls -1tr | 头-n -5 | xargs rm <----------您需要在ls上添加-1,否则将无法获得列表输出以使磁头无法正常工作
Al Joslin 2015年

3
@AlJoslin -1是输出到管道时的默认值,因此此处不是必需的。这具有更大的问题,与xargs解析带有空格,引号和&c的名称时的默认行为有关。
Charles Duffy

似乎--no-run-if-empty在我的shell中无法识别。我在Windows上使用Cmder。
StayFoolish

-0如果文件名可能包含空格,则可能需要使用该选项。尚未测试过。 来源
Keith

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查找,对于-z需要GNU排序,对于“ \ 0”需要GNU awk,对于-0需要GNU xargs,但是需要处理带有嵌入式换行符或空格的文件。


2
如果要删除目录,只需将-f更改为-d并将-r添加到rm。找 。-maxdepth 1 -type d -printf'%T @%p \ 0'| 排序-r -z -n | awk'BEGIN {RS =“ \ 0”; ORS =“ \ 0”; FS =“”} NR> 5 {sub(“ ^ [0-9] *(。[0-9] *)?”,“”); 打印}'| xargs -0 rm -rf
Alex

1
乍一看,我对awk逻辑的复杂性(或就此而言的必要性)感到惊讶。我是否在OP的问题中遗漏了一些必要的要求?
Charles Duffy

@Charles Duffy:sub()删除时间戳,这是对时间戳的排序。由“%T @”产生的时间戳可以包括小数部分。使用FS在空间上分割会破坏具有嵌入式空间的路径。我想删除第一个太空作品,但是几乎很难读懂。不能在命令行上设置RS和ORS分隔符,因为它们是NUL。
wnoise

1
@wnoise,我通常的处理方法是进入shell while read -r -d ' '; IFS= -r -d ''; do ...循环-第一个读取终止于该空间,而第二个读取继续进行到NUL。
查尔斯·达菲

@查尔斯·达菲(Charles Duffy):我总是对未加工的贝壳持怀疑态度,这可能是由于拜占庭式的报价引起的。我现在认为GNU sed -z -e 's/[^ ]* //; 1,5d'是最清晰的。(或sed -n -z -e 's/[^ ]* //; 6,$p'。)
2016年

14

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

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

这个:

  1. 当前目录中有目录时有效

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

  3. 当当前目录中的文件数量过多时,安全失败,xargs通常会使您烦恼(-x

  4. 不能满足文件名中的空格(也许您使用的是错误的OS?)


5
如果find返回的文件名超过单个命令行中可以传递的文件名,该ls -t怎么办?(提示:您会获得的多次运行ls -t,每个运行仅是单独排序,而不是具有全局正确的排序顺序;因此,当使用足够大的目录运行时,此答案将被严重破坏)。
Charles Duffy

12
ls -tQ | tail -n+4 | xargs rm

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

在mklement0的有用注释后进行编辑(谢谢!):更正了-n + 3参数,请注意,如果文件名包含换行符和/或目录包含子目录,则此操作将无法正常工作。


-Q我的机器上似乎不存在该选项。
Pierre-Adrien Buisson 2014年

4
嗯,该选项已经存在于GNU核心工具中约20年了,但在BSD变体中并未提及。您在Mac上吗?
2014年

我的确是。没想到在最新的系统之间这种真正基本的命令没有区别。感谢您的回答 !
Pierre-Adrien Buisson,2014年

3
@Mark:的++ -Q。是的,-Q是GNU扩展(这是POSIX ls规范)。一个小警告(实际上很少有问题):-Q将文件名中嵌入的换行符编码为文字\nrm无法识别。为了排除第3,该xargs参数必须+4。最后,警告也适用于大多数其他答案:如果当前目录中没有子目录,则命令将仅按预期运行。
mklement0

1
如果没有要删除的内容,则可以通过以下--no-run-if-empty选项调用xargs :ls -tQ | tail -n+4 | xargs --no-run-if-empty rm
Olivier Lecrivain

8

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

while IFS= read -rd ''; do 
    x+=("${REPLY#* }"); 
done < <(find . -maxdepth 1 -printf '%T@ %p\0' | sort -r -z -n )

2
我建议IFS您清除-否则,您可能会丢失文件名中的尾随空格。可以将其while IFS= read -rd ''; do
限定

1
为什么"${REPLY#* }"呢?
msciwoj

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
  • 第二个版本:生成脚本,将其正确删除

不要忘记while read处理空间的窍门: ls -C1 -t | awk 'NR>5' | while read d ; do rm -rvf "$d" ; done
pinkeen 2014年

1
@pinkeen,不是那里给出的那么安全。while IFS= read -r d会更好一些- -r防止反斜杠文字被占用read,并IFS=防止尾部空白自动修剪。
查尔斯·达菲

4
顺便说一句,如果有人担心文件名存在恶意,这是一种极其危险的方法。考虑使用以下命令创建的文件touch $'hello \'$(rm -rf ~)\' world':文件名中的文字引号将与您添加的文字引号相抵消sed,从而导致文件名中的代码被执行。
Charles Duffy

1
(显然,上面的“ this”是指| sh具有外壳注入漏洞的表单)。
查尔斯·达菲

2

用zsh

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

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

在中*(.om[6,999]).均值文件,o均值排序顺序,m均值按修改日期(a用于访问时间或c用于inode更改),[6,999]选择文件范围,因此不先管理5。


有趣,但是对于我一生来说,我无法使排序glob限定符(om)正常工作(我尝试过的任何排序均未显示效果-对OSX 10.11.2均无影响(使用zsh 5.0.8和5.1.1进行了尝试) ,也不在Ubuntu 14.04(zsh 5.0.2)上)-我缺少什么?至于范围端点:无需对其进行硬编码,只需使用-1以引用最后一个条目,从而包括所有剩余文件:[6,-1]
mklement0 '16

2

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

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

这可能是一个更冗长的命令,但是我能以条件文件为目标并针对它们执行更复杂的命令要好得多。


1

在Sed-Onliners中发现了有趣的cmd-删除最后三行-查找它是另一种为猫皮化的方法的完美选择(可以),但是请注意:

 #!/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

1

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

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

如果少于10个文件,则不会删除任何文件,并且您将收到:错误标题:非法行数-0

用bash计数文件


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,因此我不会在脚本中使用以上命令!Wiser用法是:

(cd /FolderToDeleteWithin && eval $(ls -td *.tar | awk 'NR>7 { print "rm \"" $0 "\""}'))

在使用ls -tcommand 的情况下,不会对以下愚蠢的示例造成任何伤害:touch 'foo " bar'touch 'hello * world'。并不是说我们曾经在现实生活中使用此类名称创建文件!

边注。如果我们想以这种方式将变量传递给sh,我们只需修改打印(简单形式,不能容忍空格):

print "VarName="$1

将变量设置VarName为的值$1。可以一次创建多个变量。这VarName成为一个普通的sh变量,之后可以在脚本或shell中正常使用。因此,要使用awk创建变量并将其返回给shell,请执行以下操作:

eval $(ls -td *.tar | awk 'NR>7 { print "VarName=\""$1"\""  }'); echo "$VarName"

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

2
xargs没有-0或至少-d $'\n'是不可靠的;观察文件名中带有空格或引号字符的文件的行为。
查尔斯·达菲

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.