![用于移动最旧文件的 shell 脚本?](https://linux22.com/image/18803/%E7%94%A8%E4%BA%8E%E7%A7%BB%E5%8A%A8%E6%9C%80%E6%97%A7%E6%96%87%E4%BB%B6%E7%9A%84%20shell%20%E8%84%9A%E6%9C%AC%EF%BC%9F.png)
如何编写脚本将 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
,您可以用它做任何您喜欢的事情。当您完成某项操作后,$file
该while
语句将循环,并且该read
命令将再次执行,提取下一个块和下一个文件名。
难道就没有更简单的方法吗?
不。更简单的方法是有缺陷的。
如果您使用ls -t
and 管道到head
or 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 f
到find
调用中。
谢谢
答案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/*