繁体   English   中英

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

[英]Delete all but the most recent X files in bash

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

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

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

现有答案的问题:

  • 无法处理带有嵌入空格或换行符的文件名。
    • 对于直接在未加引号的命令替换 ( 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 /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 --

-r ( --no-run-if-empty ) 确保在没有输入时不调用rm

如果您有BSD xargs (包括在macOS 上),您可以在首先将换行符转换为NUL ( 0x0 ) 字符后使用-0处理NUL分隔的输入,这也(通常)一次传递所有文件名(也适用于GNU xargs ):

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

上的原始问题的变型中,在壳体的匹配文件需要被单独处理或收集在壳数组

# 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

这个:

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

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

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

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

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

基本逻辑:

  • 按时间顺序获取文件列表,一列
  • 获取除前 5 项之外的所有项(本例中 n=5)
  • 第一个版本:将它们发送给 rm
  • 第二个版本:生成一个可以正确删除它们的脚本

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

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

用 bash 计算文件

我需要一个优雅的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 "file name"

eval执行它

由于我们使用的是rm ,我不会在脚本中使用上述命令! 更明智的用法是:

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

在使用ls -t命令的情况下,不会对诸如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"

使用一些参数调整@mklement0 的出色答案,而无需导航到包含要删除的文件的文件夹...

TARGET_FOLDER="/my/folder/path"
FILES_KEEP=5
ls -tp "$TARGET_FOLDER"**/* | grep -v '/$' | tail -n +$((FILES_KEEP+1)) | xargs -d '\n' -r rm --

[参考文献:https://stackoverflow.com/a/3572628/3223785]

谢谢!

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

对于 Linux(GNU 工具),在删除 rest 的同时将n最新文件保留在当前目录中的可靠方法:

n=5

find . -mindepth 1 -maxdepth 1 -type f -printf '%T@\t%p\0' |
sort -znr -k1,1 |
sed -z -e "1,${n}d" -e 's/^[^\t]*\t//' |
xargs -0r rm -f

对于 BSD(我找不到一种有效且健壮的方法,所以我发布了这个不处理路径中的换行符的方法):

n=5
t=$(printf '\t')

find . -mindepth 1 -maxdepth 1 -type f -exec stat -f '%m%t%N' {} + |
sort -nr -k1,1 |
sed -e "1,${n}d; s/^[^$t]*$t//" -e 's/[^[:alnum:]]/\\&/g' |
xargs rm -f

如果要指定路径,请修改@Fabien 答案的版本。 如果您在其他地方运行脚本,这很有用。

ls -tr /path/foo/ | head -n -5 | xargs -I% --no-run-if-empty rm /path/foo/%

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM