Shell 技巧 - 编写常用文件重命名操作脚本

作者:Dave Taylor

我猜我们每个人使用命令行的方式都不同,并且寻求完成不同的任务。我的任务有时非常专业,就像我编写的脚本,它可以让我轻松地将 Mac OS X 内置屏幕截图实用程序的唯一文件名转换为 Web 友好的格式。

在过去的几周里,我意识到我需要另一个相当专业的脚本用于文件重命名,但这一次,我想编写一些尽可能通用的东西。

某些 Linux 版本中已经包含了一个名为 rename 的实用工具,但是,唉,我在我的 Linux/NetBSD 系统上找不到它。如果您有它,它可能复制了我本月创建的功能。不过,请继续阅读。希望这会很有用且有趣!

Rename/Pattern/Newpattern

令人惊讶的是,我经常发现自己在命令行中输入类似这样的内容

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

加载 Disqus 评论