使用bash批量重命名大量图像文件

我需要重命名大约70,000个文件。例如: 从sb_606_HBO_DPM_0089000sb_606_dpm_0089000等等。 数字范围从00890000163022。只需要更改名称的第一部分。所有文件都在一个目录中,并按顺序编号(图像序列)。数字必须保持不变。 当我尝试在bash中执行此操作时,它会告诉我“参数列表太长”。 编辑: 我首先尝试使用mv重命名单个文件:
mv sb_606_HBO_DPM_0089000.dpx sb_606_dpm_0089000.dpx
然后我尝试重命名一个范围(上周在这里学到了如何移动一堆文件,所以我以为相同的语法可能适用于重命名文件...)。我我尝试了以下内容(或类似的内容):
mv sb_606_HBO_DPM_0{089000..163023}.dpx sb_606_dpm_0{089000..163023}.dpx

4给评论者们:我不认为这是一个重复的问题;其他问题中的大多数CLI答案在这里都行不通,因为文件数量过多超出了shell的ARG_MAX限制。由于这个问题明确要求命令行解决方案,所以其他问题中可能相等的GUI解决方案也不适用。 - dessert
1我不认为这是一个重复的问题,因为关于文件重命名的问题可以有多个。请不要关闭那些不能真正回答特定问题的通用资源。 - Zanna
大家好,这绝对不是故意重复提问!在发布问题之前,我确实搜索了解决我的问题的答案。在工作中,我必须使用Linux作为操作系统来进行特定软件的视频合成,所以命令行的东西对我来说并不自然。我谦卑地建议,如果一个问题是已知的重复问题,我们只需指引提问者朝正确的方向去寻找答案。我对命令行的了解非常有限,所以在这里得到的所有帮助都非常感激 - 谢谢大家的回答,我学到了很多! - rich
1@rich 如果你能明确地编辑进你尝试过的命令,那么这就更清楚地表明这不是一个重复问题。(这向我们展示了你对这种方法的了解。)祝好。 - Sparhawk
@Sparhawk 好的,已经完成了 - 希望能有所帮助。再次说明,我的知识非常有限,所以正在通过艰难的方式学习。 - rich
2富裕,你的问题不是一个重复的问题,因为它是一个具体的问题。不要担心这个。更重要的是,在一个问题收到一些赞同的答案之后,编辑它可能不是一个好主意,因为你的编辑可能会使现有的答案变得不那么有效。现在我觉得我的回答应该解释一下为什么 mv {1..2} {3..4} 不起作用,这是一个与 ARG_MAX 完全不同的问题...其他回答的人可能也会有同样的感受!所以,从我的角度来看,我希望你能撤销你最后的编辑,并且如果你愿意的话,提出一个关于使用范围进行移动的全新问题。 - Zanna
1@Sparhawk,OP从问题的第一个版本起就非常清楚地写明了问题是“argument list too long”错误。没有必要进一步澄清,这显然不是个重复问题,因为我们需要找到处理ARG_MAX的解决办法,而提出的重复问题中的答案并不能解决这个问题。 - terdon
@terdon是的,我明白这不是重复问题,但有人之前投票关闭它,并将其视为重复问题,所以这些额外的信息应该可以防止再次发生。同时,看到提问者已经尝试过什么也是有帮助的。 - Sparhawk
9个回答

一种方法是使用find命令配合-exec+选项。这样可以构建一个参数列表,但会将列表分成多个调用,以便在不超过最大参数列表的情况下操作所有文件。当所有参数都需要相同处理时,这种方法非常适用。对于rename来说是这样的,但对于mv则不是。 您可能需要安装Perl重命名工具:
sudo apt install rename
然后你可以使用,例如:
find . -maxdepth 1 -exec rename -n 's/_HBO_DPM_/_dpm_/' {} +
在测试完成后,删除-n以实际重命名文件。

我打算提出三个替代方案。每个都是一个简单的单行命令,但我会提供更复杂情况的变体,主要是为了处理与其他文件混在同一个目录中的文件。

mmv

我会使用mmv命令 来自于同名软件包

mmv '*HBO_DPM*' '#1dpm#2'
请注意,参数以字符串形式传递,因此在shell中不会发生通配符扩展。该命令接收确切的两个参数,然后在内部查找相应的文件,对文件数量没有严格限制。还请注意,上述命令假设所有与第一个通配符匹配的文件都将被重命名。当然,您可以更具体地指定:
mmv 'sb_606_HBO_DPM_*' 'sb_606_dpm_#1'

如果您的文件位于同一目录中请求的数字范围之外,您可以考虑在此答案下方提供的循环遍历数字。但是您也可以使用适当模式的一系列 mmv 调用:

mmv 'sb_606_HBO_DPM_0089*'       'sb_606_dpm_0089#1'    # 0089000-0089999
mmv 'sb_606_HBO_DPM_009*'        'sb_606_dpm_009#1'     # 0090000-0099999
mmv 'sb_606_HBO_DPM_01[0-5]*'    'sb_606_dpm_01#1#2'    # 0100000-0159999
mmv 'sb_606_HBO_DPM_016[0-2]*'   'sb_606_dpm_016#1#2'   # 0160000-0162999
mmv 'sb_606_HBO_DPM_01630[01]?'  'sb_606_dpm_01630#1#2' # 0163000-0163019
mmv 'sb_606_HBO_DPM_016302[0-2]' 'sb_606_dpm_016302#1'  # 0163020-0163022
循环遍历数字 如果你想避免安装任何东西,或者需要通过数字范围选择来避免匹配超出此范围的内容,并且你愿意等待74,023次命令调用,你可以使用一个简单的bash循环:
for i in {0089000..0163022}; do mv sb_606_HBO_DPM_$i sb_606_dpm_$i; done
这在这里特别有效,因为序列中没有间隙。否则,您可能需要检查源文件是否实际存在。
for i in {0089000..0163022}; do
  test -e sb_606_HBO_DPM_$i && mv sb_606_HBO_DPM_$i sb_606_dpm_$i
done
请注意,与for ((i=89000; i<=163022; ++i))相比,花括号扩展可以处理前导零,因为几年前的某个Bash版本已经进行了更改。实际上,这是我提出的一个改变请求,所以我很高兴看到它的用例。 进一步阅读:在Bash信息页面中花括号扩展,特别是关于{x..y[..incr]}的部分。

循环遍历文件

另一个选项是循环遍历适当的通配符,而不仅仅是在所讨论的整数范围内进行循环。类似于以下内容:
for i in *HBO_DPM*; do mv "$i" "${i/HBO_DPM/dpm}"; done
再次,每个文件只有一个mv调用。循环仍然是在一个长列表的元素上进行,但整个列表不作为参数传递给子进程,而是由bash内部处理,所以限制不会引起问题。 进一步阅读:Bash信息页面中的Shell Parameter Expansion,其中包括${parameter/pattern/string}等内容的文档。 如果您想将数字范围限制在您提供的范围内,可以添加一个检查:
for i in sb_606_HBO_DPM_+([0-9]); do
  if [[ "${i##*_*(0)}" -ge 89000 ]] && [[ "${i##*_*(0)}" -le 163022 ]]; then
    mv "$i" "${i/HBO_DPM/dpm}"
  fi
done

在这里,${i##pattern}会从$i中删除最长的与pattern匹配的前缀。这个最长的前缀被定义为任意字符,然后是一个下划线,接着是零个或多个零。后者被写成*(0),它是一个基于glob模式的扩展,取决于是否设置了extglob选项。去除前导零非常重要,以将该数字视为十进制而不是八进制。循环参数中的+([0-9])是另一个扩展的glob,用于匹配一个或多个数字,以防万一存在以相同开头但结尾不是数字的文件。


谢谢!这个方法真是太好用了:for i in {0089000..0163022}; do mv sb_606_HBO_DPM_$i sb_606_dpm_$i; done - 我不得不添加文件扩展名才能让它正常工作,但它确实做到了我想要的,并且我甚至理解了语法。谢谢 @MvG - rich
@rich: 很高兴能帮到你,也希望能帮到未来的访问者。别忘了接受最有用的答案。如果有更好的答案出现,你随时可以改变那个勾选标记。 - MvG

一种绕过ARG_MAX限制的方法是使用bash shell的内置命令printf
printf '%s\0' sb_* | xargs -0 rename -n 's/HBO_DPM/dpm/'

例子

rename -n 's/HBO_DPM/dpm/' sb_*
bash: /usr/bin/rename: Argument list too long
但是
printf '%s\0' sb_* | xargs -0 rename -n 's/HBO_DPM/dpm/'
rename(sb_606_HBO_DPM_0089000, sb_606_dpm_0089000)
.
.
.
rename(sb_606_HBO_DPM_0163022, sb_606_dpm_0163022)

find . -type f -exec bash -c 'echo $1 ${1/HBO_DPM/dpm}' _ {} \;
./sb_606_HBO_DPM_0089000 ./sb_606_dpm_0089000

在当前目录中查找所有文件-type f,并将找到的文件$1重命名,替换HBO_DPMdmp 一个接一个地执行-exec ... \;

echo替换为mv以执行重命名。


你可以逐个文件地进行操作(可能需要一些时间),使用
sudo apt install util-linux  # if you don't have it already
for i in *; do rename.ul HBO_DPM dpm "$i"; done
就像其他答案中使用的Perl rename一样,rename.ul也有一个用于测试的选项-n--no-act

我已经删除了你对Zanna回答的评论,请编辑Zanna的回答或留下一个评论。 - fosslinux
@ubashu 这不是对我的回答的评论,而是指我在测试中使用的-n标志,并建议它也可以在rename.ul中使用。 - Zanna

你可以写一个小的Python脚本,类似这样:
import os
for file in os.listdir("."):
    os.rename(file, file.replace("HBO_DPM", "dpm"))
将其保存为一个文本文件,命名为rename.py,放在文件所在的文件夹中,然后在该文件夹中使用终端执行以下操作:
python rename.py

我看到没有人邀请我的好朋友 sed 参加派对 :). 以下的 for 循环将实现你的目标:
for i in sb_606_HBO_DPM*; do
  mv "$i" "$(echo $i | sed 's/HBO_DPM/dpm/')";
done
有很多工具可以完成这样的工作,选择一个对你来说最容易理解的。这个工具简单易用,可以轻松地根据需要进行修改,适用于此或其他目的...

虽然在这个特定情况下并不太相关,但如果文件名中包含换行符,这将失败。我提到这一点是因为大多数(或者全部?)其他答案都是健壮的,可以处理任意文件名,或者仅适用于原帖中的文件命名方案。 - terdon
...新行、空格、通配符等,其中一些可以通过在命令替换中引用$i来避免,但没有简单的方法来处理文件名中的尾部换行符。 - muru

既然我们提供了选择,这里有一个Perl的方法。进入目标目录并运行以下命令:cd
perl -e 'foreach(glob("sb_*")){rename $_, s/_HBO_DPM_/_dpm_/r}'

解释

  • perl -e:运行由-e给出的脚本。
  • foreach(glob){}:对于glob的每个结果,运行{ }中的任何内容。
  • glob("sb_*"):返回当前目录中所有文件和目录的列表,其名称与shell globsb*匹配。
  • rename $_,s/_HBO_DPM_/_dpm_/r:perl magic。 $_是一个特殊变量,它保存我们正在迭代的每个元素(在foreach中)。所以这里,它将是找到的每个文件。 s/_HBO_DPM_/_dpm_/_dpm_替换第一次出现的_HBO_DPM_。默认情况下,它在$_上运行,因此它将在每个文件名上运行。 / r 表示“将此替换应用于目标字符串(文件名)的副本并返回修改后的字符串”。 rename执行您期望的操作:重命名文件。因此,整个过程将使用_dpm_替换_HBO_DPM_将当前文件名($_)重命名为其自身。
你可以将同样的内容写成一个扩展的(更易读的)脚本:
#! /usr/bin/env perl
use strict;
use warnings;

foreach my $fileName (glob("sb_*")){
  ## Copy the name to a new variable
  my $newName = $fileName;
  ## change the copy. $newName is now the changed version
  $newName =~ s/_HBO_DPM_/_dpm_/;
  ## rename
  rename $fileName, $newName;
}

根据您所设想的重命名方式,使用 vidir 进行多行编辑可能是可以满足需求的。
在您的特定情况下,您可以选择文本编辑器中的所有行,并且通过几个按键来删除文件名中的 "HBO" 部分。


是的,vi具有全局查找和替换功能。 - Jasen
2请问您能否详细阐述一下,并且给出一个使用vidir来实现OP目标的例子? - dessert