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

use*_*598 17 shell bash shell-script timestamps files

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

Sor*_*gal 15

解析 的输出ls不可靠的

相反,使用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)
Run Code Online (Sandbox Code Playgroud)

这一切在做什么?

首先,该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 -t和管道到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)
Run Code Online (Sandbox Code Playgroud)

像这样调用:

move-oldest /mnt/backup/ /var/log/foo/ 20
Run Code Online (Sandbox Code Playgroud)

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

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

谢谢

感谢enzotib????? ??????为了改进这个答案。


Gil*_*il' 5

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

mv -- *(Om[1,20]) target/
Run Code Online (Sandbox Code Playgroud)

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

如果你没有 zsh,这里有一个 Perl 单行(你可以用少于 80 个字符来完成,但在清晰度方面需要付出更多代价):

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

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

仅使用 POSIX 工具甚至 bash 或 ksh,按日期对文件进行排序是很痛苦的。您可以使用 轻松完成ls,但解析 的输出ls是有问题的,因此这仅适用于文件名仅包含换行符以外的可打印字符的情况。

ls -tr | head -n 20 | while IFS= read -r file; do mv -- "$file" target/; done
Run Code Online (Sandbox Code Playgroud)


max*_*zig 5

您可以为此使用 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
Run Code Online (Sandbox Code Playgroud)

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


ktf*_*ktf 4

用或组合ls -t输出。tailhead

简单的示例,仅当所有文件名仅包含除空格之外的可打印字符且不\[*?以 开头时才有效-

 mv $(ls -1tr | head -20) other_folder
Run Code Online (Sandbox Code Playgroud)