批量替换类名Shell脚本源码解析

背景

现在做的项目有个批量修改类名的需求,包括文件名、类名、工程文件中的名字。去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.将原来脚本中的[:>:]都去掉就实现了前缀匹配

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容