Shell 技巧 - 编写常用文件重命名操作脚本
我猜我们每个人使用命令行的方式都不同,并且寻求完成不同的任务。我的任务有时非常专业,就像我编写的脚本,它可以让我轻松地将 Mac OS X 内置屏幕截图实用程序的唯一文件名转换为 Web 友好的格式。
在过去的几周里,我意识到我需要另一个相当专业的脚本用于文件重命名,但这一次,我想编写一些尽可能通用的东西。
某些 Linux 版本中已经包含了一个名为 rename 的实用工具,但是,唉,我在我的 Linux/NetBSD 系统上找不到它。如果您有它,它可能复制了我本月创建的功能。不过,请继续阅读。希望这会很有用且有趣!
令人惊讶的是,我经常发现自己在命令行中输入类似这样的内容
for name in xx* do new="$(echo $name | sed 's/xx/yy/')" mv $name $new done
因此,这是我想创建的脚本的第一部分,它让我只需指定旧的和新的文件名模式,然后简单地将所有匹配“OLD”的文件名用替换为“NEW”模式。
例如,假设我有 test-file-1.txt 和 test-file-2.jpg,并且想用“demo”替换“test-file”。目标是拥有像这样的调用
rename test-file demo
第一步实际上是最困难的:匹配任意模式并优雅地捕获任何可能的错误情况。循环最终看起来会像这样
for name in $1*
但是,如果没有匹配项,您会收到一条难看的错误消息,并且脚本看起来很业余。因此,目标实际上是在 for 循环之前确定给定模式有多少个匹配项。
啊,好的,所以ls $1* | wc -l可以解决问题,对吧? 不行,这仍然会生成相同的难看错误消息。
幸运的是,Bash 中有一种方法可以将 stderr 重定向到 stdout(也就是说,让您的错误消息显示为标准消息,可以被重定向、管道等)。
因此,匹配项数量的测试可以像这样完成
matches="$(ls -1 $1* 2>&1 | wc -l)"
我知道,这很复杂。更糟糕的是,快速测试显示,当没有匹配项时,ls -l实际上会生成错误消息ls: 无法访问 'No such file or directory': 没有那个文件或目录。这不好。解决方案?在序列中添加 grep
matches="$(ls -1 $1* 2>&1 | grep -v "No such file" | wc -l)"
这甚至更复杂,但它的工作方式完全符合我们的期望。“matches”在没有匹配项的情况下为零;否则,它具有给定模式的匹配文件和文件夹的数量。
现在的测试让我们产生有意义且内容丰富的错误消息
if [ $matches -eq 0 ] ; then echo "Error: no files match pattern $1*" exit 0 fi
因为我们正在查看 stderr 而不是 stdout,所以我们也可以更正确地将该错误消息路由到 stderr,使用>&2,为了完全正确,我们应该以非零错误代码退出,以指示脚本未能正确执行。我将把这些调整作为练习留给读者。
现在我们知道我们永远不会在没有至少一个匹配项的情况下命中 for 循环,核心代码很简单
for name in $1* do new="$(echo $name | sed "s/$1/$2/")" mv $name $new done
请注意,在这种情况下,您不能在$( )命令替换中使用单引号;如果您这样做,$1 和 $2 将不会被正确扩展。
我们当然可以就此止步,并拥有一个有用的小脚本,但我喜欢非常酷的脚本,所以让我们继续,好吗?
我经常发现自己需要的另一个功能是能够按顺序编号一系列文件。例如,来自照片拍摄的最终照片集可能是 DSC1017、DSC1019、DSC1023 和 DSC1047。在将它们发送给客户之前,能够重新编号这些照片会更有用,以便它们成为 DSC-1、DSC-2、DSC-3 等等。
现在我们有了一个可以重命名一系列文件的脚本,这也非常容易实现。这是我在脚本本身中实现它的方式
if [ $renumber -eq 1 ] ; then suffix="$(echo $name | cut -d. -f2- | tr '[A-Z]' '[a-z]')" new="$2$count.$suffix" count=$(( $count + 1 )) mv $name $new chmod a+r $new fi
在这里,我希望替换整个文件名,所以我剥离并保存文件名后缀(例如,DSC1015.JPG 变为 JPG),以便稍后可以重新附加它。同时,文件名后缀也使用方便的 tr 命令标准化为全部小写。
count 变量跟踪我们正在处理的数字,并注意内置的 shell 表示法$(( ))用于数学计算。
最后,新文件名由新模式 ($2)、计数 ($count) 和文件名后缀 ($suffix) 在此行中构建
new="$2$count.$suffix"
但是,这两个条件需要合并,因此最终脚本以 if-then-else-fi 结构结束。
我无法就此罢休,所以我继续调整脚本,也添加了一些起始标志。为了解析所有内容,我们使用了朋友 getopt
args=$(getopt npt $*) if [ $? != 0 -o $# -lt 2 ] ; then echo "Usage: $(basename $0) {-p} {-n} {-t} PATTERN NEWPATTERN" echo " echo " -p rewrites PNG to png" echo " -n sequentially numbers matching files with" echo " NEWPATTERN as base filename" echo " -t test mode: show what you'll do, don't do it." exit 0 fi set -- $args for i do case "$i" in -n ) renumber=1 ; shift ;; -p ) fixpng=1 ; shift ;; -t ) doit=0 ; shift ;; -- ) shift ; break ;; fi
如果您想阅读有关 getopt 及其在 shell 脚本中复杂用法的更多信息,我之前已经写过关于它的文章 [请参阅 7 月号的 LJ 中的“使用 getopt 解析命令行选项”,www.linuxjournal.com/article/10495]。请注意,脚本用户可以使用三个标志-n调用重新编号功能(这意味着文件名被丢弃,记住);-p是一个特殊情况,其中 .PNG 也被重写为 .png;以及-t是一种“仅回显”模式,其中重命名实际上不会发生,脚本只是显示它会根据给定的模式做什么。
我现在是如何使用它的?像这样
rename -n IMG_ iphone-copy-paste-
每个匹配的 .PNG 文件 (IMG_*) 的名称部分都替换为“iphone-copy-paste-”,并且随着它的进行,“PNG”也被重写为“png”。
完整的重命名脚本可以在 Linux Journal FTP 服务器上找到,地址为 ftp.linuxjournal.com/pub/lj/listings/issue199/10885.tgz。
Dave Taylor 从事 shell 脚本编写已经很长时间了,30 年。他是广受欢迎的 Wicked Cool Shell Scripts 的作者,可以在 Twitter 上找到他 @DaveTaylor,更常见的是在 www.DaveTaylorOnline.com。