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


14

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


包含或排除子目录?并且是否应该以递归方式(在目录树中)完成?
maxschlepzig

2
许多(大多数?)* nix文件系统没有存储创建日期,因此您不能确定最早的文件。通常可用的属性是atime(上次访问),ctime(上次权限更改)和mtime(上次修改)。ls -t发现的 printf "%T"使用mtime......看来,根据这个链接,我的ext4分区是能够 处理创建日期,但lsfindstat不具备相应的选项(但)...
Peter.O

Answers:


13

解析的输出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)

这是怎么回事

首先,这些find命令将所有文件和目录定位在当前目录(.)中,而不是当前目录(-maxdepth 1)的子目录中,然后输出:

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

时间戳很重要。该%T@格式说明用于-printf分解成T,这表示文件(修改时间)和“最后修改时间” @,其指示“从1970年开始秒”,包括分数秒。

该空间仅仅是一个任意的定界符。文件的完整路径是我们以后可以引用的路径,而NULL字符是终止符,因为它是文件名中的非法字符,因此可以确保我们知道到达该路径的末尾。文件。

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

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

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

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

的结果findsort管道是通过传递进程替换while它,仿佛它是标准输入一个文件被读取。while依次调用read以处理输入。

在上下文中,read我们将该IFS变量设置为nothing,这意味着空格不会被不恰当地解释为定界符。read被告知-r,禁用逃生扩张,以及-d $'\0',这使得最终的行分隔符NULL,匹配我们的输出findsort管道。

代表最早的文件路径的第一个数据块(其时间戳和空格之后)被读入变量line。接下来,将参数替换与表达式一起使用,该表达式#*简单地替换了从字符串开头到第一个空格(包括空格)在内的所有字符。这将删除修改时间戳,仅保留文件的完整路径。

此时,文件名已存储在其中$file,您可以使用它进行任何操作。完成使用$filewhile语句的操作时,该语句将循环并read再次执行该命令,提取下一个块和下一个文件名。

有没有更简单的方法?

不,更简单的方法是越野车。

如果您使用ls -t或管道传输到headtail(或其他任何内容),则会在文件名中使用换行符的文件上断开。如果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调用中。

谢谢

感谢enzotibПавелТанков对这个答案的改进。


排序不应该使用-n。至少在我的版本中,它不能正确排序十进制数字。您必须删除日期中的点或使用-printf '%TY-%Tm-%TdT%TH:%TM:%TS %p\0' | sort -rz,ISO日期或其他方式。
l0b0 2012年

@ l0b0:我知道此限制。我假设不要求该级别的粒度就足够了(也就是说,超出.必须与您无关的排序。)说起来很清楚sort -z -n -t. -k1
Sorpigal 2012年

@ l0b0:无论如何,您的解决方案都会出现相同的错误:%TS还会显示形式为的“小数部分” 00.0000000000,因此也会丢失粒度。最近的GNU sort可以通过使用-V“版本排序” 来解决此问题,该版本将按预期处理这种类型的浮点。
Sorpigal 2012年

不,因为我在“ YYYY-MM-DDThh:mm:ss”上进行了字符串排序,而不是数字排序。字符串排序不关心小数,因此它应该可以工作到10000年:)
l0b0 2012年

@ l0b0:然后对字符串进行排序%T@也可以,因为它是零填充的。
Sorpigal 2012年

4

这在zsh中最简单,您可以在其中使用Om glob限定符按日期对匹配项进行排序(最旧的优先),而[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

ls -t输出与tail或组合head

简单示例,仅当所有文件名仅包含可打印字符(除了空白和)时才起作用\[*?

 mv $(ls -1tr | head -20) other_folder

1
将-A选项添加到ls:ls -1Atr
Arcege 2011年

1
-1,危险。在这里,我来举例说明:touch $'foo\n*'。如果执行mv“ $(ls)”并且该文件坐在那里会发生什么?
2012年

1
@Sorpigal认真吗?说“让我想出一个您明确说过的例子是行不通的。嘿,看,它行不通”
Michael Mrozek

1
@Sorpigal这不是一个坏主意,它在99%的情况下都有效。答案是“如果您使用普通名称的文件,则可以使用。如果您是疯了的人,将换行符嵌入文件名中,则不会。” 完全正确
Michael Mrozek

1
@MichaelMrozek:这是一个坏主意,但是很糟糕,因为有时它会失败。如果您可以选择做有时会失败的事情,哪些不会失败,那么您应该选择不做的事情(而做的不好的事情)。交互式地执行您想做的任何事情,但是要在脚本文件中并在给出建议时正确执行。
Sorpigal 2012年

3

您可以为此使用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_direcho如果已经测试了命令行,请删除。


2

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

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/*

+1,仅是在我之前有用的答案(并且总是有测试数据。)
Sorpigal 2012年

0

使用GNU最简单find。我每天在Linux DVR上使用它来删除比一天还早的视频监视系统中的记录。

语法如下:

find /path/to/files/* -mtime +number_of_days -exec mv {} /path/to/folder \;

请记住,这find将一天定义为从执行时间起的24小时。因此,在11 pm修改的文件不会在1 am删除。

您甚至可以find与结合使用cron,因此可以通过以root身份运行以下命令来自动计划删除:

crontab -e << EOF
@daily /usr/bin/find /path/to/files/* -mtime +number_of_days -exec mv {} /path/to/folder \;
EOF

您始终可以find通过参考手册页获取更多信息:

man find

0

由于其他答案不符合我和问题的目的,因此该外壳已在CentOS 7上进行了测试:

oldestDir=$(find /yourPath/* -maxdepth 0 -type d -printf '%T+ %p\n' | sort | head -n 1 | tr -s ' ' | cut -d ' ' -f 2)
echo "$oldestDir"
rm -rf "$oldestDir"
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.