[shell] 对照片的批量重命名 | batch rename photos with shell

在之前[Ruby] 批量重命名照片脚本 Script to batch rename photos一文中,有提到对照片文件批量重命名的ruby脚本。虽然用ruby写的脚本可以跨平台的运行,但还是需要有个ruby环境的支持。既然现在个人的桌面环境已经基本转为ubuntu,自然想用更加贴近ubuntu生态的工具实现功能,也作为对最近几天学习Shell编程的一次作业。

这个脚本的功能为对指定文件夹内的所有指定后缀名的文件进行批量重命名。命名的规则为“该文件夹的名称_序号.后缀名”,序号从001开始计数。原始文件名可能千奇百怪,可能含有空格,这个操作主要是对照片文件夹进行,我个人的习惯一般将文件夹名设为照片的拍摄日期。而一般从SD卡导入的照片文件名都是“IMG_XXXX.jpg”之类的,基本没提供什么信息。我个人的习惯就是使用”2012-11-25_001.jpg”、”2012-11-25_002.jpg”这样的形式。

#! /bin/sh
# name: rename-photos.sh
# author: Weihong Guan (aGuegu) http://aguegu.net
# date: 2012-11-25
# usage: $./rename-photos path extension_name
# example: $ ./rename-photos.sh ~/Pictures/2012-11-25/ jpg

path=${1:-$(pwd)}
# $path

if echo $path | grep ".*/$" > /dev/null
then
	length=$(expr length $path)
	length=$(($length-1))
	path=$(expr substr $path 1 $length)
fi

#echo $path

foldername=${path##/*/}
#echo $foldername

ext=${2:-"jpg"}

#echo $ext

pics=$(ls $path/*.$ext|sed 's/ /^Z/g') # ^z is CTRL+Z in vi

i=1

for p in $pics
do
#	echo $p
	old_name=$(echo $p | sed 's/^z/\ /g')
	new_name=$(printf "%s/%s_%03d.%s\n" $path $foldername $i $ext)
	echo mv "$old_name" "$new_name"
	mv "$old_name" "$new_name"
	echo $((i += 1)) > /dev/null
done

exit 0

这30行的脚本,倒也包含了也不少shell编程的知识点。

第1行:指示所用shell的位置,/bin/sh表示,使用的是标准的 Unix shell,也就是Bash

第8-9行:将命令后的第一个参数存入至path变量,如果其不存在或为空,则将path设置为当前路径($ pwd);

第11-16行:使用自动补全(tab)生成的目录路径,其末尾会带“/”,而pwd返回的路径却不会。如果有”/”,则将其去除。grep对之前echo的输出通过管道“|”进行正则表达式匹配。

if ... then ... fi

是shell的条件判断语句。如果 path 符合正则表达式 “.*/$” ,即末尾字符为“/”,即该运算有结果输出,判断为真,则执行相应操作。

length=$(expr length $path)

表示,将 (expr length $path) 的结果赋值给length变量,其中 expr length 是计算字符串长度的运算。

length=$(($length-1))

将(length-1)的结果返回给length变量。理论上这里可以使用$((length–))之类的命令,可惜没有测试成功。最后,将 (expr substr $path 1 $length) 的结果返回给变量 path。expr substr 是取子字符串的函数。我们在这里取原path的长度减一,起始位置仍然为首位(1)的子字符串,即去除了末尾的”/”。

在此处以及脚本的其它不少地方都出现了

echo ... > /dev/null

的写法,其核心是“…”的部分,一般为赋值。但是似乎直接这样写,系统会有”permission denied”的错误,感觉是直接改到了环境变量之类。但是默认的 $echo 又会把命令打印到终端,所以将其输出重定向“/dev/null”,该文件是类unix系统都会有的“位桶”文件,按照字面理解,就是把结果输出到一个并不存在的文件,不会在标准输出上显示,而系统会认为已经成功完成了输出。

第20行:取出path中的末端文件夹名,之前去除“/”的操作就是在为这一步服务。${path##/*/}的意思是, 删除path变量中,从开头处开始符合/*/表达式的最长部分。这样,就把路径中最后一个”/”以及之前的所有字符全部删除,得到了文件夹名,也就是我们所需要的批量文件名的前半部分。

第23-24行:将命令行的第2个参数存入ext变量。如果为空,则写入”jpg”,就是以”jpg”为默认值。

第27行:将“ls $path/*.$ext|sed ‘s/ /^z/g’”的结果存入pic变量。ls是最基本的shell命令,相当与DOS的dir。如果我们要遍历pics中所包含的文件名,需要将文件名中包含的空格先用个特殊字符替换,因为遍历默认使用空格作为间隔符。其中^Z是在vi中,按Ctrl+z输出的特殊字符,而不是^和Z。ls的参数包含了path以及ext,输出的结果为path路径下所有后缀名为ext的绝对路径。注意,是绝对路径,如果是相对路径会有问题,因为照片文件并不在shell脚本所在的目录下。ls命令输出的结果默认就会按照文件名进行升序排列,所以不用担心重命名后文件顺序会出现混乱。
sed 是在shell中用的最多的配合正则表达式实现文本替换的工具,非常强大哦。

第29-39行:建立一个计数器(counter)变量”i”。使用

for... in ... do ... done

遍历pics变量。old_name 为还原空格后的文件绝对路径,因为在调用mv的时候进行重命名的时候,还是要使用真实的文件名。用printf命令生成新的文件绝对路径。printf 是经典的格式化输出工具,在bash里面也可以直接用哦,感觉和C语言里面唯一的区别,就是没有了括号和逗号。

接下来就是调用mv,逐一进行重命名,注意参数需要加上双引号。因为mv的参数分割符默认也是空格,而文件名中可能就有空格。用双引号包起来就没什么问题了,原因见这里,否则会出现“mv: target specified is not a directory”错误。不过为了避免这类的问题,无论是linux或是windows,作为良好的习惯,还是尽量别让文件名或文件夹名中包含空格。同时为了命令行以及文件名在各种系统得以在正确显示,还是尽量都用英文吧。比方在用ghost的时候,发现所有的盘符和文件名都是乱码,肯定是非常痛苦的事情。

最后一行,返回0作为退出码,表示脚本已正确执行,退出码在当脚本被别的脚本调用的时候还是很有意义的。

到这里基本算是解释完了。shell有个特点,就是它是很“散”的,拥有很多的工具,每个都有强大的功能以及丰富的扩展,但是要把他们找出来、用好用巧并不容易。但是只要安装了bash(linux系统自带),脚本就能运行(不过我发现android上bash命令少得可怜……),这些工具基本是基于GNU开源协议开发,而且都历史悠久,经过不断优化,执行效率也很高,肯定比自己写得强。还有一个特点Shell不用向C/C++/java哪样,从源代码开始编,还要导入各种头文件或是包,还要编译;也不像ruby那样可能要去找gem。因为是原生的shell,所以未来如果扩展功能,比方说需要遍历所有文件夹的子文件夹同样实现该操作,也同样会感觉比较自然,再整一个shell,对本shell进行调用就好。学习shell也是很好地学习linux 终端命令的好方法,简单来说shell就是一系列终端操作的序列。而把连续的操作都存入到一个shell scipt 之中,也是”Don’t Repear Yourself“的典型表现哦~

参考书籍:《Shell脚本学习指南》

P.S. 虽然编写Java或是arduino的程序还是离不开eclipse,但是这个shell就是在vim里面完成的,vim对shell的缩进支持、代码高亮都控制得很好。

关于aGuegu

向着更高的逼格
此条目发表在Shell分类目录,贴了, , , , , , 标签。将固定链接加入收藏夹。
  • zodiac1111

    有个rename很好用,python版本的那个。

    • python一直没去接触呢,现在就玩shell和ruby了。