小编典典

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

all

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

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

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


阅读 163

收藏
2022-03-03

共1个答案

小编典典

现有答案的问题:

  • 无法处理带有嵌入空格或换行符的文件名。
  • 如果解决方案rm直接在未引用的命令替换 ( rm…``) 上调用,则会增加意外 globbing 的风险。
  • 无法区分文件和目录(即,如果目录恰好位于最近修改的 5 个文件系统项中,那么您将有效地保留少于5 个文件,并且应用rm到目录将失败)。

解决了这些问题,但解决方案是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
2022-03-03