用于移动最旧文件的 shell 脚本?

用于移动最旧文件的 shell 脚本?

如何编写脚本将 20 个最旧的文件从一个文件夹移动到另一个文件夹?有没有办法获取文件夹中最旧的文件?

答案1

ls解析is的输出不可靠的

相反,使用find来定位文件并按sort时间戳对它们进行排序。例如:

while IFS= read -r -d $'\0' line ; do
    file="${line#* }"
    # do something with $file here
done < <(find . -maxdepth 1 -printf '%T@ %p\0' \
    2>/dev/null | sort -z -n)

这一切到底是在做什么?

首先,这些find命令查找当前目录 ( .) 中的所有文件和目录,但不查找当前目录 ( -maxdepth 1) 的子目录中的所有文件和目录,然后打印:

  • 时间戳
  • 空间
  • 文件的相对路径
  • 空字符

时间戳很重要。的格式%T@说明-printf符分为T,表示文件的“上次修改时间”(mtime) 和@,表示“自 1970 年以来的秒数”,包括小数秒。

空格只是一个任意的分隔符。文件的完整路径是为了方便我们稍后引用它,而 NULL 字符是终止符,因为它是文件名中的非法字符,因此可以让我们确定已到达文件路径的末尾。文件。

我已包含在内2>/dev/null,以便排除用户无权访问的文件,但会抑制有关它们被排除的错误消息。

该命令的结果find是当前目录中所有目录的列表。该列表通过管道传送到sort,指示:

  • -z将 NULL 视为行终止符而不是换行符。
  • -n按数字排序

由于 1970 年以来的秒数总是增加,因此我们需要时间戳是最小数字的文件。第一个结果sort将是包含最小编号时间戳的行。剩下的就是提取文件名。

find,管道的结果sort通过流程替代while它被读取的地方,就好像它是标准输入上的文件一样。while依次调用read来处理输入。

read在我们将变量设置为空的上下文中IFS,这意味着空格不会被不恰当地解释为分隔符。read被告知-r,它禁用转义扩展,并且-d $'\0',它使行尾分隔符为 NULL,与我们的find,sort管道的输出相匹配。

第一个数据块代表最旧的文件路径,前面带有时间戳和空格,被读入变量line。下一个,参数替换与表达式 一起使用#*,它只是将从字符串开头到第一个空格(包括空格)的所有字符替换为空。这会去掉修改时间戳,只留下文件的完整路径。

此时,文件名已存储$file,您可以用它做任何您喜欢的事情。当您完成某项操作后,$filewhile语句将循环,并且该read命令将再次执行,提取下一个块和下一个文件名。

难道就没有更简单的方法吗?

不。更简单的方法是有缺陷的。

如果您使用ls -tand 管道到heador tail(或任何事物)您将在文件名中带有换行符的文件上中断。如果mv $(anything)名称中包含空格的文件将导致损坏。如果mv "$(anything)"名称中带有尾随换行符的文件将导致损坏。如果read-d $'\0'这样做,您将破坏名称中带有空格的文件。

也许在特定情况下,您确信更简单的方法就足够了,但如果可以避免的话,您永远不应该将这样的假设写入脚本中。

解决方案

#!/usr/bin/env bash

# move to the first argument
dest="$1"

# move from the second argument or .
source="${2-.}"

# move the file count in the third argument or 20
limit="${3-20}"

while IFS= read -r -d $'\0' line ; do
    file="${line#* }"
    echo mv "$file" "$dest"
    let limit-=1
    [[ $limit -le 0 ]] && break
done < <(find "$source" -maxdepth 1 -printf '%T@ %p\0' \
    2>/dev/null | sort -z -n)

打电话像:

move-oldest /mnt/backup/ /var/log/foo/ 20

将最旧的 20 个文件从 移动/var/log/foo//mnt/backup/.

请注意,我包含文件目录。对于文件,仅添加-type ffind调用中。

谢谢

谢谢恩佐替布帕维尔·塔尼克夫对此答案的改进。

答案2

您可以使用 GNU find 来实现此目的:

find -maxdepth 1 -type f -printf '%T@ %p\n' \
  | sort -k1,1 -g | head -20 | sed 's/^[0-9.]\+ //' \
  | xargs echo mv -t dest_dir

其中 find 打印修改时间(从 1970 年开始以秒为单位)和当前目录的每个文件的名称,输出根据第一个字段排序,最旧的 20 个被过滤并移动到dest_dir.echo如果您已经测试了命令行,请删除。

答案3

在 zsh 中最简单,您可以在其中使用Om 全局限定符按日期(最旧的在前)和限定符对匹配项进行排序,[1,20]以仅保留前 20 个匹配项:

mv -- *(Om[1,20]) target/

D如果您还想包含点文件,请添加限定符。.如果您只想匹配常规文件而不匹配目录,请添加。

如果您没有 zsh,这里有一个 Perl 单行代码(您可以用少于 80 个字符来完成它,但在清晰度上会付出进一步的代价):

perl -e '@files = sort {-M $b <=> -M $a} glob("*"); foreach (@files[0..1]) {rename $_, "target/$_" or die "$_: $!"}'

但请注意,它的精度仅达到秒(修改时间的纳秒部分(如果可用)不被考虑)。

仅使用 POSIX 工具甚至 bash 或 ksh,按日期对文件进行排序是一件痛苦的事情。您可以使用 轻松完成此操作ls,但解析 的输出ls是有问题的,因此仅当文件名仅包含除换行符之外的可打印字符时才有效。

ls -tr | head -n 20 | while IFS= read -r file; do mv -- "$file" target/; done

答案4

还没有人发布一个 bash 示例来满足文件名中嵌入换行符(嵌入任何内容)的需求,所以这里是一个。它移动 3 个最旧的 (mdate) 常规文件

move=3
find . -maxdepth 1 -type f -name '*' \
 -printf "%T@\t%p\0" |sort -znk1 | { 
  while IFS= read -d $'\0' -r file; do
      printf "%s\0" "${file#*$'\t'}"
      ((--move==0)) && break
  done } |xargs -0 mv -t dest

这是测试数据片段

# make test files with names containing \n, \t and "  "
rm -f '('?[1-4]'  |?)'
for f in $'(\n'{1..4}$'  |\t)' ;do sleep .1; echo >"$f" ;done
touch -d "1970-01-01" $'(\n4  |\t)'
ls -ltr '('?[1-4]'  |'?')'; echo
mkdir -p dest

这里是检查结果片段

  ls -ltr '('?[1-4]'  |'?')'
  ls -ltr   dest/*

相关内容