背景
现在做的项目有个批量修改类名的需求,包括文件名、类名、工程文件中的名字。去github上搜了一下还真找到一个似乎看起来比较满足需求的脚本:<br /> rename-xcode-files
不过毕竟不能完全满足自己的需求比如类名的前缀匹配比如,ATestXXX=>BTestXXX这种形式。
先把这个脚本的源码部分贴一下:
#!/bin/bash
1) PROJECT_DIR=.
2) RENAME_CLASSES=rename_classes.txt
#First, we substitute the text in all of the files.
5) sed_cmd=`sed -e 's@^@s/[[:<:]]@; s@[[:space:]]\{1,\}@[[:>:]]/@; s@$@/g;@' ${RENAME_CLASSES} `
6) find ${PROJECT_DIR} -type f \
7) \( -name "*.pbxproj" -or -name "*.h" -or -name "*.m" -or -name "*.xib" -or -name "*.storyboard" \) \
8) -exec sed -i.bak "${sed_cmd}" {} +
# Now, we rename the .h/.m files
11) while read line; do
12) class_from=`echo $line | sed "s/[[:space:]]\{1,\}.*//"`
13) class_to=`echo $line | sed "s/.*[[:space:]]\{1,\}//"`
14) find ${PROJECT_DIR} -type f -regex ".*[[:<:]]${class_from}[[:>:]][^\/]*\.[hm]" -print | egrep -v '.bak$' | \
15) while read file_from; do
16) file_to=`echo $file_from | sed "s/\(.*\)[[:<:]]${class_from}[[:>:]]\([^\/]*\)/\1${class_to}\2/"`
17) echo mv "${file_from}" "${file_to}"
18) mv "${file_from}" "${file_to}"
19) done
20) done < ${RENAME_CLASSES}
其中rename_classes.txt文件内容的形式是:
AClass BClass
TestClass TTestClass
源码解析
虽然算上空行和注释总共也只有20行代码,不过乍一看还是很懵的,各种/@:.\等特殊的字符,有些没有思绪。不过这都是因为我对shell脚本的认知其实还属于小白阶段,所以对一些语法和特性并不理解,导致想修改脚本满足自己更多个性化的需求不知道从何入手。查阅了很多资料,文档以及请教了一些对Shell脚本非常有经验的同事,终于读懂了这个脚本,现在就逐行分析一下源码,算是Mark一下自己这几天的收获。
第1行,第2行
这两行比较简单,将当前的目录路径和当前目录下的rename_classes.txt
文件地址赋值给两个变量
第5行
sed命令用于对文件以行为单位进行内容操作,如增删改,查找替换。sed命令的语法如下:
sed [选项] '[动作]' 文件名
选项:
-n:一般sed命令会把所有数据都输出到屏幕,如果加入此选择则只会把经过sed命令处理的行输出到屏幕。
-e:允许对输入数据应用多条sed命令编辑。
-i:用sed的修改结果直接修改读取数据的文件,而不是由屏幕输出。
动作:
a:追加,在当前行后添加一行或多行
c:行替换,用c后面的字符串替换原数据行
i:插入,在当前行前插入一行或多行
d:删除,删除指定的行
p:打印,输出指定的行
s:字串替换,用一个字符串替换另外一个字符串。格式为“行范围s/旧字串/新字串/g”,这里/g指的是在整行内完整的匹配,否则默认的只是匹配第一次查找到的旧字符串
根据上面的语法解释,sed -e 后面接着的应该是多条命令,以;分割。
等同于下面三行命令:
s@^@s/[[:<:]]@;
s@[[:space:]]\{1,\}@[[:>:]]/@;
s@$@/g;@
那么@是什么呢?
原来,在执行替换操作的时候,如果要替换的内容中包含/,这个时候需要对/进行了转义成\/
,不过这样表达式的可读性必然会降低,因此在sed中还可以使用|,@,^,!
作为命令的分隔符。
[[:space:]]
、[[:<:]]
、[[:>:]]
又是什么意思呢?
<br /> POSIX Bracket Expressions
从这个文档里可以查到,[:space:]表示空格或者制表符的字符集合,外面那一层[]表示匹配[]中的字符查找,是正则表达式中的规则, ^和$也是正则表达式中的规则,分别表示行首和行尾。{1,}是表示匹配次数的限制,至少匹配到一次,{m,n}表示匹配到m到n次。[:<:]和[:>:]查阅很多资料后依然查询不到,后来在自己的猜测和验证下,这两个字符的含义分别是从xx字符开始和以xx字符结束,其中开始或者结束的标志都是挨着xx字符的不是大写/小写字母、数字和_
这样上面三行sed指令就可以翻译为:
将行首替换为s/[:<:]
将至少匹配到一次的空格字符替换为[:>:]
将行尾替换为/g;
所以
AClass BClass
TestClass TTestClass
执行完上面的三行命令后就变成了:
s/[:<:]AClass[:>:]/BClass/g;
s/[:<:]TestClass[:>:]/TTestClass/g;
执行到这里看出来了:这几行命令实际上最后生成的是一个新的sed命令,作用是将配置文件中的旧类名替换为新类名。个人认为这里的命令行嵌套是此脚本的精髓之处。
第6行,第7行,第8行
find命令可以实现对于文件的查找。
find命令的基本组成:
find pathname -options [-print -exec -ok]
参数
pathname: find命令所查找的目录路径。例如用.来表示当前目录,用/来表示系统根目录。
-print: find命令将匹配的文件输出到标准输出。
-exec: find命令对匹配的文件执行该参数所给出的shell命令。相应命令的形式为'command' {} \;,注意{ }和\;之间的空格。
-ok: 和-exec的作用相同,只不过以一种更为安全的模式来执行该参数所给出的shell命令,在执行每一个命令之前,都会给出提示,让用户来确定是否执行。
find命令选项
-name:按照文件名查找文件。
-perm:按照文件权限来查找文件。
-prune:使用这一选项可以使find命令不在当前指定的目录中查找,如果同时使用-depth选项,那么-prune将被find命令忽略。
-user: 按照文件属主来查找文件。
-group:按照文件所属的组来查找文件。
-mtime -n +n:按照文件的更改时间来查找文件, - n表示文件更改时间距现在n天以内,+n表示文件更改时间距现在n天以前。Find命令还有-atime和-ctime选项,但它们都和-mtime选项。
-nogroup:查找无有效所属组的文件,即该文件所属的组在/etc/groups中不存在。
-nouser:查找无有效属主的文件,即该文件的属主在/etc/passwd中不存在。
-newer file1 ! file2:查找更改时间比文件file1新但比文件file2旧的文件
-type 查找某一类型的文件
b - 块设备文件。
d - 目录。
c - 字符设备文件。
p - 管道文件。
l - 符号链接文件。
f - 普通文件
sed -i表示对操作文件的原地修改,sed -i .xxx是可以在原地修改前对操作的文件进行备份,备份后的文件名是原文件名.xxx。
所以,这几行命令的意思是:找到当前目录下后缀名为pbxproj,h,m,xib的文件,对于每个找到的文件备份为后缀名为.bak的备份文件,然后再执行sed_cm的命令,这样所有上述格式文件中的旧类名就已经被替换成了新类名
第11行,第20行
读取文件分为两步:
1.将文件的内容通过重定向(<)的方式传给while
2.while中调用read将文件内容一行一行的读出来,并付值给read后跟随的变量。变量中就保存了当前行中的内容。
第12行,第13行
这里的|是管道命令的操作符,"|"只能处理经由前面一个指令传出的正确输出信息,对错误信息信息没有直接处理能力。然后,传递给下一个命令,作为标准的输入.
管理命令的输出说明:
指令1 | 指令2 | 指令3
【指令1】正确输出,作为【指令2】的输入,然后【指令2】的输出作为【指令3】的输入 ,【指令3】输出就会直接显示在屏幕上面了。
通过管道之后【指令1】和【指令2】的正确输出不显示在屏幕上面。
所以这两行的意思就非常明显了,读取rename_classes.txt文件中的旧类名最为class_from,新类名作为class_to
第14行
在当前目录下查找class_from.h或者.m的文件,不包括.bak的备份文件,也不包括class_from的文件夹目录
第15行,第19行
while语句
语法:
while 命令/条件
do
语句
done
机制:如果while后的命令执行成功,或条件真,则执行do和done之间的语句,执行完成后,再次判断while后的命令和条件;如果while后的命令执行失败,或条件为假,循环结束
第16行
\(\)
用于匹配文本中的某个子串。
在sed中,使用\(\)
对匹配的内容进行分组,使用\N的方式进行引用。示例
echo "Three One Two" | sed 's|\(\w\+\) \(\w\+\) \(\w\+\)|\2 \3 \1|'
One Two Three
我们输出了Three,One,Two三个单词,在sed的替换规则中,使用空格分隔了三小段正则表达式\(\w\+\)来匹配每一个单词,后面使用\1,,\2,\3分别引用它们的值。
所以这里的意思是将XX旧类名YY.h替换成XX新类名YY.h
第17行,第18行
打印出来改名前的文件路径和改名后的文件路径,将旧的文件名替换为新的文件名
代码改进
读懂了源码之后,就可以修改满足自己的需求了。 目前这个脚本无法满足需要的主要是两个点:
1.备份文件其实没必要生成
2.支持对于旧类名的前缀匹配替换
修改方式:
1.第8行-exec sed -i.bak "${sed_cmd}" {} +
改为-exec sed -i "" "${sed_cmd}" {} +
,
-i extension
Edit files in-place, saving backups with the specified extension.
If a zero-length extension is given, no backup will be saved. It
is not recommended to give a zero-length extension when in-place
editing files, as you risk corruption or partial content in situ-
ations where disk space is exhausted, etc.
要点:
* 用 -i 命令将替换结果写入文件
* -i 之后的""表示不生成备份文件,解决报错问题
2.将原来脚本中的[:>:]
都去掉就实现了前缀匹配