将Linux文件批量重命名
1. 概述
当我们想在 Linux 中重命名文件时,我们通常会使用mv命令。但是mv命令不能帮助我们批量重命名文件。
在本教程中,我们将研究一些批量重命名用例,以及如何使用几种不同的方法解决它们。
2. 重命名工具
我们选择了三种可以帮助我们重命名的方法:
- rename命令
- perl-rename命名工具
- awk | sh 方法
让我们首先看看如何安装这些工具,或者它们是否可能已经在我们的发行版中可用。
2.1. rename命令
rename 命令来自util-linux 包。它仅替换文件名中某些文本的第一次出现。
由于util-linux包是由kernel.org 发布的标准包,因此默认情况下,** rename命令在所有 Linux 发行版中都可用**。
在本文中,我们将其称为rename。
2.2. perl-rename命名命令
perl-rename命令在现代 Linux 发行版中默认不可用。
在 Ubuntu 系列发行版中,我们可以 使用 apt-get安装perl-rename:
root# apt-get install rename
要在 RedHat 系列发行版上安装perl-rename ,我们需要安装prename包:
root# yum install prename
安装后,perl-rename命令安装在*/usr/bin/prename*。
在 Archlinux 派生的发行版中,我们可以使用pacman安装perl-rename:
root# pacman -Syu perl-rename
因为它支持Perl Compatible Regular Expressions (PCRE) ,** perl-rename命令比默认的rename命令 更强大**并且被广泛使用。
在本文中,我们将使用prename来引用它。
2.3. awk | sh方法
awk本身不是文件重命名工具。然而,凭借其强大的文本处理功能, awk可用于处理文件名并将其转换为“ mv oldName newName ”命令。
我们可以将这些“ mv ”命令通过管道传输到 shell 以运行它们。
由于awk预装在大多数现代 Linux 发行版上,我们可以使用它来批量重命名文件,以防我们没有root权限来安装软件,例如prename实用程序。而且,在某些情况下,awk可以完成prename无法完成的重命名工作。 我们将在重命名场景示例中使用 GNU awk 。
3. 更改文件扩展名
第一种情况很常见。比如我们有很多 *.txt文件:
$ ls
file1.txt file2.txt file3.txt file4.txt file5.txt
假设我们要将所有*.txt文件重命名为*.log。
3.1. rename
命令rename的工作很容易 。但是,在我们了解如何使用它之前,让我们先了解重命名命令的两个重要选项:
- -n:试运行,不做任何改变
- -v:详细,显示哪些文件被重命名
如果我们将两个选项-nv*组合在一起,rename命令将只显示将进行的更改,而不会真正应用更改。*
这对我们在进行更改之前进行检查非常有帮助:
$ rename -nv txt .log *.txt
`file1.txt' -> `file1..log'
`file2.txt' -> `file2..log'
`file3.txt' -> `file3..log'
`file4.txt' -> `file4..log'
`file5.txt' -> `file5..log'
在这个例子中,当我们键入命令行时,我们错误地在替换“ log ”前面多了一个点。
带有“ -nv ”选项的rename命令 清楚地显示了错误,并让我们有机会更正命令。
建议**始终进行试运行以确保更改正确无误。**这是因为批量重命名操作没有“撤消”或“恢复”选项。
现在,让我们使用rename命令将我们的*.txt文件重命名为.log*:
$ rename .txt .log *.txt
$ ls
file1.log file2.log file3.log file4.log file5.log
rename命令 非常简单。它在每个文件名中查找 txt的第一次出现,并将其替换为log。
或者,我们可以使用带有find命令的rename来定位特定文件:
$ ls
log1-backup.txt log1.txt log2-backup.txt log2.txt log3.txt log4.txt
$ find . -iname "log*-backup.txt" -exec rename .txt .xml '{}' \;
$ ls
log1-backup.xml log1.txt log2-backup.xml log2.txt log3.txt log4.txt
-exec参数告诉find对找到的每个匹配文件执行重命名。**在我们的例子中,所有名称中包含“ backup ”的文件都是目标。
请记住“.” 在find命令之后表示当前目录。
3.2. prename
prename 命令根据Perl 的搜索和替换表达式 重命名文件。它还支持*-nv*(试运行和详细)选项。
让我们看看如何 使用prename命令重命名txt文件:
$ prename 's/[.]txt$/.log/' *.txt
$ ls
file1.log file2.log file3.log file4.log file5.log
在此示例中,我们能够使用正则表达式标记*$*来表示匹配项必须位于文件名的末尾。
3.3. awk | sh
awk是一个强大的文本处理实用程序。我们可以将awk生成的mv命令通过管道传输到 shell 以进行批量重命名:
awk '...' | sh
awk没有“试运行”选项 ,但是,**如果我们删除“ | sh ”,awk会将所有生成的mv命令打印 到标准输出,而不执行它们。*这可以用来代替rename和 prename的-nv*选项。
我们可以使用find 命令将文件名通过管道传递给 awk作为输入:
$ find . -name "*.txt" | awk -v mvCmd='mv "%s" "%s"\n' \
'{ old = $0;
sub(/[.]txt$/,".log");
printf mvCmd,old,$0;
}'
mv "./file5.txt" "./file5.log"
mv "./file4.txt" "./file4.log"
mv "./file3.txt" "./file3.log"
mv "./file2.txt" "./file2.log"
mv "./file1.txt" "./file1.log"
我们应该注意,当我们编写 shell 脚本时,我们不应该解析** ls的输出。**这是因为文件名可能包含空格、制表符甚至换行符。ls的输出不能很好地区分它们。
我们需要使用find命令为awk提供文件名。虽然awk可以将文件名表达式作为输入,但它使用该表达式来读取文件的内容,我们希望对名称本身进行文本处理。
如果我们追加 | sh 命令,所有*.txt文件将被重命名为*.log:
$ find . -name "*.txt" | awk -v mvCmd='mv "%s" "%s"\n' \
'{ old=$0;
sub(/[.]txt$/,".log");
printf mvCmd,old,$0;
}' | sh
$ ls
file1.log file2.log file3.log file4.log file5.log
4. 用文件名中的另一个字符串替换一个字符串
我们经常遇到这种重命名问题。例如,这里有一些*.txt文件:
$ ls -1
image1.txt
image2.txt
image3.txt
image4KeepMe.txt
image5KeepMe.txt
假设我们只想在前三个文件中将文本“ image ”替换为“ picture ”,而后两个文件保持不变。
4.1. rename
如果我们使用rename命令,我们就会遇到问题:
$ rename -nv image picture *.txt
`image1.txt' -> `picture1.txt'
`image2.txt' -> `picture2.txt'
`image3.txt' -> `picture3.txt'
`image4KeepMe.txt' -> `picture4KeepMe.txt'
`image5KeepMe.txt' -> `picture5KeepMe.txt'
** rename命令 不支持正则表达式模式匹配。**因此,它不能单独排除最后两个名称中带有“ KeepMe ”的文件。
为了解决这个问题,我们可以使用通配 技巧:
$ rename -nv image picture image?.txt
`image1.txt' -> `picture1.txt'
`image2.txt' -> `picture2.txt'
`image3.txt' -> `picture3.txt'
在这种情况下,glob 表达式帮助我们缩小了重命名文件的范围。
同样,我们可以一起使用find和rename来达到相同的目的。
例如,让我们用“ ignore ”替换“ backup ” :
$ ls *backup*
log1-backup.xml log2-backup.xml
$ find . -iname "*backup*" -exec rename backup ignore '{}' \;
$ ls
log1-ignore.xml log1.txt log2-ignore.xml log2.txt log3.txt log4.txt
4.2. prename
prename命令提供了更强大的 PCRE。因此,排除最后两个文件对prename来说根本不是挑战。 一个简单的否定前瞻 将解决问题:
$ prename 's/image(?!.*KeepMe)/picture/' *.txt
$ ls -1
image4KeepMe.txt
image5KeepMe.txt
picture1.txt
picture2.txt
picture3.txt
4.3. awk | sh
正则表达式是awk编程的基本部分。因此,awk也 可以轻松排除最后两个“ KeepMe ”文件:
$ find . -name "*.txt" | awk -v mvCmd='mv "%s" "%s"\n' \
'!/KeepMe/ {
old=$0;
sub(/image/,"picture");
printf mvCmd,old,$0;
}'| sh
$ ls -1
image4KeepMe.txt
image5KeepMe.txt
picture1.txt
picture2.txt
picture3.txt
5. 将文件名中所有出现的字符串替换为另一个字符串
以前,我们试图替换文件名中出现的单个子字符串。现在,让我们看看当我们想要替换所有出现时会发生什么。 让我们从这些文件开始:
$ ls -1
igm1.igm
igm2_igm3.igm.zip
igm4_igm5_igm6.igm.zip
让我们将所有文件名中所有出现的“ igm ”更改为“ img ”。我们应该注意到字符串“ igm ”出现的次数因文件名而异。
5.1. rename
我们已经知道rename命令只替换字符串的第一次出现。因此,rename命令无法处理这种情况。
5.2. prename
Perl 的替换表达式支持“ g ”(全局)修饰符 。这允许匹配运算符匹配所有出现的模式:
$ prename 's/igm/img/g' *
$ ls -1
img1.img
img2_img3.img.zip
img4_img5_img6.img.zip
5.3. awk | sh
我们已经使用awk 的sub函数来替换字符串的第一次出现。
awk有另一个函数gsub ,它的作用与 Perl 的“ g ”修饰符相同:
$ find . -type f | awk -v mvCmd='mv "%s" "%s"\n' \
'{ old=$0;
gsub(/igm/,"img");
printf mvCmd,old,$0;
}' | sh
$ ls -1
img1.img
img2_img3.img.zip
img4_img5_img6.img.zip
6. 格式化文件名中的数字
在这种情况下,让我们格式化文件名中的数字。比如我们在一个目录下有三个*.txt文件:
$ ls -1
afile-1.txt
bfile-10.txt
cfile-123.txt
为了使列表更易于阅读,我们希望格式化数字以添加前导零:
- afile-1.txt变成afile-001.txt
- bfile-10.txt变成bfile-010.txt
- cfile-123.txt将保留其名称
6.1. rename
因为数字是动态的, rename命令不能在一条命令中完成重命名工作。
6.2. prename
Perl 的替换表达式为sprintf函数提供了“ e ”(eval)修饰符:
$ prename 's/\d+/sprintf("%03d","$&")/e' *.txt
$ ls -1
afile-001.txt
bfile-010.txt
cfile-123.txt
6.3. awk | sh
awk也有sprintf函数。
但是,我们不能像使用prename那样使用它。相反,我们必须在调用sub函数之前使用sprintf格式化数字 :
$ find . -name "*.txt" | awk -F'[.-]' -v mvCmd='mv -n "%s" "%s"\n' \
'{ num=sprintf("%03d", $(NF-1));
old=$0;
sub(/[0-9]+/,num);
printf mvCmd,old,$0;
}'
$ ls -1
afile-001.txt
bfile-010.txt
cfile-123.txt
这里我们使用*-F’[.-]’将文件名拆分为“ . ”或“ - ”。在这种情况下,倒数第二个字段$(NF-1)* 是我们要格式化的数字。
7. 更改文件名中的字符大小写
在这种情况下,让我们将文件名中的所有大写字符转换为小写:
$ ls -1
INSTRUCTION.TxT
Query.SQL
ReadMe.MD
7.1. rename
另一个动态重命名场景,这不是rename命令的工作。
7.2. prename
Perl 有一个lc (小写)函数,用于将输入字符串转换为小写。lc 函数加上“ e ”修饰符可以解决这个问题:
$ prename 's/.*/lc("$&")/e' *
$ ls -1
instruction.txt
query.sql
readme.md
除了lc 函数,我们还可以使用 Perl 的音译运算符“ y ” 将所有大写字符转换为小写:
$ prename 'y/A-Z/a-z/' *
$ ls -1
instruction.txt
query.sql
readme.md
7.3. awk | sh
awk 有两个专门用于大小写转换的函数:tolower 和 toupper。让我们在这里使用tolower:
$ find . -type f | awk -v mvCmd='mv "%s" "%s"\n' \
'{ new=tolower($0);
printf mvCmd,$0,new;
}' | sh
$ ls -1
instruction.txt
query.sql
readme.md
8. 交换文件名中的字符串
假设我们在一个目录下有很多系统日志文件。每个文件名都包含一个格式为DD-MM-YYYY的日期字符串:
$ ls -1
08-08-1992_system.log
18-11-1976_system.log
29-11-2019_system.log
我们将通过将DD-MM-YYYY格式转换为ISO 日期格式 来重命名文件:YYYY-MM-DD。
8.1. rename
另一个动态重命名场景,这不是rename命令的工作。
8.2. prename
在这里,我们可以利用反向引用和捕获组 来实现我们的目标:
$ perl-rename -nv 's/(\d\d)-(\d\d)-(\d{4})/\3-\2-\1/' *.log
$ ls -1
1976-11-18_system.log
1992-08-08_system.log
2019-11-29_system.log
在正则表达式中由*()定义的三个捕获组 在替换表达式中由它们的编号\1、\2*和 \3引用。
8.3. awk | sh
GNU awk的不错的gensub 函数也允许我们处理反向引用:
$ find . -name "*.log" | awk -v mvCmd='mv "%s" "%s"\n' \
'{ new=gensub(/([0-9]{2})-([0-9]{2})-([0-9]{4})/, "\\3-\\2-\\1", "g");
printf mvCmd,$0,new;
}' | sh
$ ls -1
1976-11-18_system.log
1992-08-08_system.log
2019-11-29_system.log
9. 将 Unix 时间戳转换为 ISO 日期格式
在这种情况下,我们将研究更深入的日期格式转换。 让我们看一下这些文件:
$ ls -1
app_1575212161.log
app_217189800.log
app_713302200.log
每个文件名都包含一个Unix 时间戳。 我们想将 Unix 时间戳转换为 ISO 日期格式。
9.1. rename
不过,rename命令不能为我们完成这项工作。
9.2. prename
我们已经了解到,我们可以在 Perl 的搜索中评估 Perl 表达式,并用“ e ”修饰符替换表达式。我们将再次使用这个技巧来解决这个问题:
$ prename 'use POSIX qw(strftime);s/\d+/strftime "%FT%H:%M:%S", localtime($&)/e' *.log
$ ls -1
app_1976-11-18T19:30:00.log
app_1992-08-08T21:30:00.log
app_2019-12-01T15:56:01.log
localtime 函数将Unix 时间戳转换为 Perl 的时间类型。然后,来自POSIX 模块的Perl 的strftime函数负责将时间转换为 ISO 日期格式。
9.3. awk | sh
GNU awk也 有 strftime函数。它可以帮助我们从 Unix 时间戳中获取不同的日期格式:
$ find . -name "*.log" | awk -F'[_.]' -v mvCmd='mv "%s" "%s"\n' \
'{ old=$0;
sub(/[0-9]+/,strftime("%FT%T", $(NF-1)));
printf mvCmd, old, $0;
}' | sh
$ ls -1
app_1976-11-18T19:30:00.log
app_1992-08-08T21:30:00.log
app_2019-12-01T15:56:01.log
除了使用 GNU awk的 strftime函数外, awk 的替代解决方案 也值得一试。
** awk可以调用外部命令并获取输出以供进一步处理。** 我们可以使用getline表达式获取命令的输出并将其分配给变量:
External_Command | getline variable
让我们使用date 命令将 Unix 时间戳转换为 ISO 格式,然后用它的输出替换 Unix 时间戳:
$ find . -name "*.log"|awk -F'[_.]' -v mvCmd='mv "%s" "%s"\n' \
'{ old=$0;
"date +%FT%T -d @"$(NF-1)|getline isoFmt;
gsub(/[0-9]+/,isoFmt);
printf mvCmd, old, $0;
}' | sh
$ ls -1
app_1976-11-18T19:30:00.log
app_1992-08-08T21:30:00.log
app_2019-12-01T15:56:01.log
有了与外部命令配合的能力, awk就变得更加强大了。它可以做许多更复杂的重命名工作,例如:
- 在文件名中添加文件的md5哈希值( md5sum命令)
- 如果文件内容包含某种模式(grep 命令),则在文件名中添加一些标记
- 将文件名中的域名转换为 IP 地址(nslookup 或 host命令)
10. 在文件名中动态格式化数字
我们之前看到了一个数字格式化场景。我们了解到,我们可以通过将预定义的固定格式传递给sprintf函数来添加前导零,例如sprintf(“%03d”,”$&”)。
但是,我们可能不想手动检查文件以确定前导零的数量,尤其是当我们有大量文件时。
有一种方法可以在文件重命名期间根据正在处理的文件自动为我们计算数字格式。
让我们向之前使用的示例添加几个新文件:
$ ls -1
afile-1.txt
bfile-10.txt
cfile-123.txt
dfile-4711.txt
efile-20191201.txt
重命名和 预命名都无法 检测到使所有这些数字长度相同所需的正确填充。
10.1. awk | sh
awk从find命令获取文件名的完整列表,因此awk可以解决我们的问题:
$ find . -name "*.txt" | awk -F'[-.]' -v mvCmd='mv -n "%s" "%s"\n' \
'{ num = $(NF-1);
files[$0] = num;
max = num > max? num : max;
}
END { width = length(max);
for(name in files) {
old = new = name;
formattedNum = sprintf("%0*d", width, files[name]);
sub(files[name],formattedNum,new);
printf mvCmd,old,new;
}
}' | sh
$ ls -1
afile-00000001.txt
bfile-00000010.txt
cfile-00000123.txt
dfile-00004711.txt
efile-20191201.txt
让我们逐行查看 awk脚本,看看那里发生了什么:
- 将find结果通过管道传递给 awk,然后定义字段分隔符和 mv命令模板
- 对于每个文件名,提取倒数第二个字段的数字,并保存在num变量中
- 构建一个名为files的哈希表——键是完整的文件名 ( $0 ),值是num变量
- 从所有文件名中找到最大的num 并将其保存在max变量中
- awk遍历了所有文件名并将所有必需的数据保存在文件和 最大 变量中
- 计算最大数的宽度( max )
- 对于哈希表文件中的每个元素, awk生成 mv命令
- 复制名称以重命名
- 格式化当前名称 中的数字
- 用formattedNum替换文件名 ( new )中的原始数字
- 生成mv命令
- 结束循环
- 最后,将生成的mv命令通过管道传递给sh
10.2. awk | sh作为通用重命名解决方案
awk解决简单重命名问题的方法可能不像其他重命名工具(例如prename )那么紧凑。
但是,awk非常有用,值得学习。这是因为其强大的脚本语言几乎可以解决任何类型的批量重命名问题。
awk可以做的不仅仅是重命名文件。