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

Mat*_*ard 138 unix bash scripting

有一个简单的方法,在一个非常标准的UNIX环境中使用bash运行命令来删除目录中除最新的X文件之外的所有文件吗?

为了给出一个具体的例子,想象一下一些cron作业每小时写一个文件(比如一个日志文件或一个tar-up up备份)到一个目录.我想要一种方法来运行另一个cron作业,它将删除该目录中最旧的文件,直到少于5个.

而且要清楚,只有一个文件存在,它永远不应该被删除.

mkl*_*nt0 98

现有答案存在的问题:

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

wnoise的答案解决了这些问题,但解决方案是GNU特定的(并且非常复杂).

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

为了记录,这里解释为什么解析ls输出通常不是一个好主意:http://mywiki.wooledge.org/ParsingLs

ls -tp | grep -v '/$' | tail -n +6 | xargs -I {} rm -- {}
Run Code Online (Sandbox Code Playgroud)

上面的效率很低,因为xargs必须rm每个文件名调用一次.
您的平台xargs可能允许您解决此问题:

如果你有GNU xargs,使用-d '\n',这使得xargs考虑每个输入线路分离的说法,但经过许多参数作为将适合在命令行上一次:

ls -tp | grep -v '/$' | tail -n +6 | xargs -d '\n' -r rm --
Run Code Online (Sandbox Code Playgroud)

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

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

ls -tp | grep -v '/$' | tail -n +6 | tr '\n' '\0' | xargs -0 rm --
Run Code Online (Sandbox Code Playgroud)

说明:

  • 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
Run Code Online (Sandbox Code Playgroud)

  • 当然比这里的大多数其他答案要好,所以我很乐意提供支持,即使因为我认为忽略换行案例只是谨慎行事. (2认同)
  • 如果你不在当前目录中执行 `ls`,那么文件路径将包含 '/',这意味着 `grep -v '/'` 不会匹配任何内容。我相信 `grep -v '/$'` 是您只想排除目录的内容。 (2认同)

Esp*_*spo 95

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

rm `ls -t | awk 'NR>5'`
Run Code Online (Sandbox Code Playgroud)

  • 简洁可读,也许,但使用起来很危险; 如果试图删除用`touch'hello*world'`创建的文件,这将删除**绝对当前目录中的所有内容**. (13认同)
  • `ls -t | awk'NR> 5'| xargs rm -f`如果你喜欢管道,你需要在没有任何东西要删除的情况下抑制错误. (9认同)
  • ***警告*** 请确保您从要从中删除文件的目录运行此命令!我愚蠢地从大约 100 个文件的工作代码目录中运行了这个,它毁掉了很多 f*&amp;$*ing !!幸运的是,我在 30 分钟前刚刚进行了备份(唷!)(你知道当你的心脏停止跳动并且在垃圾箱中找不到文件时那种沮丧的感觉) (3认同)
  • 我需要这个只考虑我的档案文件.将`ls -t`改为`ls -td*.bz2` (2认同)
  • 我通过将其更改为rm -rf`ls -t |来将其用于目录 awk'NR> 1'`(我只想要最近的).谢谢! (2认同)

the*_*sdj 85

(ls -t|head -n 5;ls)|sort|uniq -u|xargs rm
Run Code Online (Sandbox Code Playgroud)

此版本支持带空格的名称:

(ls -t|head -n 5;ls)|sort|uniq -u|sed -e 's,.*,"&",g'|xargs rm
Run Code Online (Sandbox Code Playgroud)

  • 此命令将无法正确处理名称中包含空格的文件. (19认同)
  • 如果你有5个或更少,这将删除你的所有文件!将`--no-run-if-empty`添加到`xargs`中,如`(ls -t | head -n 5; ls)| sort | uniq -u | xargs --no-run-if-empty rm`请更新答案. (14认同)
  • `(ls -t | head -n 5; ls)`是[命令组](http://tldp.org/LDP/abs/html/special-chars.html#PARENSREF).它打印两次最近的5个文件.`sort`将相同的行放在一起.`uniq -u`删除重复项,以便除了5个最新文件之外的所有文件都保留.`xargs rm`在每个上面调用`rm`. (5认同)
  • 即使是"支持带空格的名字"也是危险的.考虑一个包含文字引号的名称:`touch'foo"bar'`将抛弃整个命令的其余部分. (3认同)
  • ...使用`xargs -d $'\n''比在内容中插入引号更安全,虽然NUL分隔输入流(需要使用除了'ls`以外的其他东西*真正*做正确)理想的选择. (2认同)

Fab*_*ien 59

更简单的thelsdj答案:

ls -tr | head -n -5 | xargs --no-run-if-empty rm 
Run Code Online (Sandbox Code Playgroud)

ls -tr显示所有文件,最早的文件(-t最新的第一个,-r反向).

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

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

  • 需要在xargs中添加--no-run-if-empty,以便在少于5个文件时不会失败. (14认同)
  • @AlJoslin,`-1`是输出到管道时的默认值,因此这里不是必需的.这有很大的问题,与使用空格,引号和c解析名称时`xargs`的默认行为有关. (3认同)

wno*_*ise 16

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
Run Code Online (Sandbox Code Playgroud)

需要GNU查找-printf,GNU排序为-z,GNU awk表示"\ 0",GNU xargs表示-0,但处理带有嵌入换行符或空格的文件.

  • 如果要删除目录,只需将-f更改为-d并将-r添加到rm.找 .-maxdepth 1-type d -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 -rf (2认同)

小智 13

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

find . -maxdepth 1 -type f | xargs -x ls -t | awk 'NR>5' | xargs -L1 rm
Run Code Online (Sandbox Code Playgroud)

这个:

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

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

  3. 失败时的文件在当前目录数量过多安全,xargs通常会去你的过(的-x)

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

  • 如果`find`返回的文件名多于可以在单个命令行上传递给`ls -t`的文件名,会发生什么?(提示:你得到多次运行`ls -t`,每个运行只是单独排序,而不是具有全局正确的排序顺序;因此,当运行足够大的目录时,这个答案会严重破坏). (4认同)

Mar*_*ark 12

ls -tQ | tail -n+4 | xargs rm
Run Code Online (Sandbox Code Playgroud)

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

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

  • 嗯,该选项已经在GNU核心工具中使用了大约20年,但在BSD变体中没有提到.你在Mac上吗? (4认同)
  • @Mark:++表示`-Q`.是的,`-Q`是GNU扩展(这里是[POSIX`ls`规范](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/ls.html)).一个小警告(在实践中很少出现问题):`-Q`将文件名中的嵌入_newlines_编码为文字`\n`,其中`rm`将无法识别.要排除第一个_3_,`xargs`参数必须为'+ 4`.最后,一个警告也适用于大多数其他答案:如果当前目录中没有_subdirectories_,则命令将仅按预期工作. (3认同)

Ian*_*ing 8

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

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

  • 我建议清除`IFS` - 否则你可能会丢失文件名中的尾随空格.可以将其范围扩展到read命令:`IFS = read -rd''; do` (2认同)

Fra*_*ona 8

对于 Linux(GNU 工具),一种有效且可靠的方法可以将n最新文件保留在当前目录中,同时删除其余文件:

\n
n=5\n\nfind . -maxdepth 1 -type f -printf \'%T@ %p\\0\' |\nsort -z -nrt \' \' -k1,1 |\nsed -z -e "1,${n}d" -e \'s/[^ ]* //\' |\nxargs -0r rm -f --\n
Run Code Online (Sandbox Code Playgroud)\n
\n

对于 BSDfind没有谓词-printfstat不能输出 NULL 字节,并且sed+awk不能处理NULL- 分隔的记录。

\n

这是一个不支持路径中换行符的解决方案,但可以通过过滤掉换行符来防止换行符:

\n
#!/bin/bash\nn=5\n\nfind . -maxdepth 1 -type f ! -path $\'*\\n*\' -exec stat -f \'%.9Fm %N\' {} + |\nsort -nrt \' \' -k1,1 |\nawk -v n="$n" -F\'^[^ ]* \' \'NR > n {printf "%s%c", $2, 0}\' |\nxargs -0 rm -f --\n
Run Code Online (Sandbox Code Playgroud)\n

注意:我之所以使用它是bash因为$\'\\n\'符号。因为sh您可以定义一个包含文字换行符的变量并使用它。

\n
\n

POSIX解决方案(灵感来自 @mklement0答案)。

\n

这个为 POSIX 添加了正确的转义xargs,但是当文件或目录的名称中包含换行符时,它仍然会中断;如果您想解决这个问题,那么除了清除或重命名这些文件之外别无选择。

\n
n=5\n\nls -tp . |\ngrep -v \'/$\' |\nhead -n +"$((n+1))" |\nsed -e \'s/"/"\\\\""/g\' -e \'s/.*/"&"/\' |\nxargs rm --\n
Run Code Online (Sandbox Code Playgroud)\n

备注:实际上您可以将其替换grep | head | sedawk -v n="$n" \'/[^/]$/ && --n < 0 {gsub(/"/, "\\"\\\\\\\\\\"\\""); print "\\"" $0 "\\""}\'

\n
\n

针对UNIX 和 Linux的解决方案(灵感来自 AIX/HP-UX/SunOS/BSD/Linux ls -b):

\n

某些平台不提供find -printf、 、stat、 也不支持使用/ / / /NUL分隔的记录。这就是为什么 using可能是解决该问题的最便携的方法,因为它默认在几乎每个操作系统中都可用。statsortawksedxargsperl

\n

我本可以把整个事情写下来,perl但我没有。我只用它来替换stat和编码-解码-转义文件名。核心逻辑与之前的方案相同,都是通过POSIX工具实现的。

\n

注意: perl \ 的默认stat分辨率为秒,但从这里开始,您可以通过模块的功能perl-5.8.9获得亚秒分辨率(当操作系统和文件系统都支持它时)。这就是我在这里使用的;如果您不提供它,那么您可以从命令行中删除它。statTime::HiResperl\xe2\x80\x91MTime::HiRes=stat

\n
n=5\n\nfind . \'(\' -name \'.\' -o -prune \')\' -type f -exec \\\nperl -MTime::HiRes=stat -le \'\n    foreach (@ARGV) {\n        @st = stat($_);\n        if ( @st > 0 ) {\n            s/([\\\\\\n])/sprintf( "\\\\%03o", ord($1) )/ge;\n            print sprintf( "%.9f %s", $st[9], $_ );\n        }\n        else { print STDERR "stat: $_: $!"; }\n    }\n\' {} + |\n\nsort -nrt \' \' -k1,1 |\n\nsed -e "1,${n}d" -e \'s/[^ ]* //\' |\n\nperl -l -ne \'\n    s/\\\\([0-7]{3})/chr(oct($1))/ge;\n    s/(["\\n])/"\\\\$1"/g;\n    print "\\"$_\\""; \n\' |\n\nxargs -E \'\' sh -c \'[ "$#" -gt 0 ] && rm -f -- "$@"\' sh\n
Run Code Online (Sandbox Code Playgroud)\n

说明:

\n
    \n
  • 对于找到的每个文件,第一个perl获取修改时间并将其与编码的文件名一起输出(每个newlinebackslash字符分别替换为文字\\012\\134)。

    \n
  • \n
  • 现在每个流都time filename保证是单行的,因此 POSIXsort可以sed安全地使用该流。

    \n
  • \n
  • 第二个perl解码文件名并将其转义为 POSIX xargs

    \n
  • \n
  • 最后,xargs呼吁rm删除这些文件。该sh命令是一个技巧,可以防止在没有要删除的文件时xargs运行。rm

    \n
  • \n
\n

  • @mklement0,...也许不是一个_常见_现实世界问题,但不常见的现实世界问题仍然是现实世界问题。当我戴上红队帽子时,攻击者就会故意制造未经处理的情况;当我戴上防御性编码帽子时,随机二进制垃圾转储到文件名中是我在职业生涯中见过的事情(由于 100% 无意的错误),当垃圾处理不当时,会带来灾难性的结果。 (2认同)

Top*_*her 5

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

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
Run Code Online (Sandbox Code Playgroud)

这比之前的一些答案更强大一点,因为它允许将搜索域限制为匹配表达式的文件。首先,找到符合您想要的任何条件的文件。打印这些文件,旁边带有时间戳。

find . -maxdepth 1 -type f -name "*_srv_logs_*.tar.gz" -printf '%T@ %p\n'
Run Code Online (Sandbox Code Playgroud)

接下来,按时间戳对它们进行排序:

sort -r -z -n
Run Code Online (Sandbox Code Playgroud)

然后,从列表中删除 4 个最新文件:

tail -n+5
Run Code Online (Sandbox Code Playgroud)

获取第二列(文件名,而不是时间戳):

awk '{ print $2; }'
Run Code Online (Sandbox Code Playgroud)

然后将整个事情包装成一个 for 语句:

for F in $(); do rm $F; done
Run Code Online (Sandbox Code Playgroud)

这可能是一个更详细的命令,但我的运气要好得多,能够定位条件文件并针对它们执行更复杂的命令。