欢迎转载,但请保留作者链接:http://www.jianshu.com/p/8adb1d92defc
阅读前提:你应该使用过Linux,了解脚本语言的基础概念“弱类型”以及“变量无须定义即可使用”。
几句话
在Linux中,绝大部分操作是这样一个流程:用户-->Shell-->kernel-->硬件。
既能对用户屏蔽复杂操作,又能对kernel起到一定保护,Shell就是处在这样一个位置上的“壳(Shell)”。
Shell中能够使用到的命令有内建命令与外部命令之分,内建命令即为Shell自身提供的命令,相当于调用当前的Shell进程执行一个函数;而外部命令则效率会更差一些,因为它不但会触发磁盘IO,还需要fork出一个单独的进程,执行完再退出,而它的优点也是显然易见的——你有海量功能强大的外部工具可以调用,所以能够用十分少的代码完成非常多的功能。
而学习Shell最重要的原因,就是将繁琐的工作全都自动化处理。
背景
在MTK平台上,每次更新Modem文件都伴随着一个十分机械化的过程:打patch,编译,最后是将编译生成的文件按一定格式重命名再放置到Android工程的相应目录下并修改相应的构建参数。
这里我们仅讨论重命名的情况,MTK的说明如下图:
重命名说明.png
可以看到,我们要将/database,/bin两个目录下的7个文件改名为右栏的格式。
比如catcher_filter.bin -> catcher_filter_1_lwg_n.bin
脚本代码
Talk is cheap,show me the code.
我们先上完整代码,其后进行详细讲解:
#!/bin/bash
# Created by Duanze 2015/12/28
# For modem update, to make your hands free :)
function confirmOp(){
read -p $1"[Y/n]:" N
if [ "y"x != ${N}x -a "Y"x != ${N}x -a ""x != ${N}x ]; then
echo "exit program"
exit
fi
}
#if false;then
if [ ! -f Android.mk ]; then
echo " There is no 'Android.mk' !"
exit
fi
#fi
if [ ! $1 ]; then
echo " parameter 1 is null"
exit
fi
if [ ! $2 ]; then
echo " parameter 2 is null"
exit
fi
sourceDir=$1
targetDir=$2
if [ ! -d $1 ];then
echo "dir $1 don't exist"
exit
fi
if [ -d $2 ]; then
read -p " dir $2 exists, program will remove it at frist, are you sure continue?[Y/n]:" N
if [ "y"x != ${N}x -a "Y"x != ${N}x -a ""x != ${N}x ]; then
echo " exit program"
exit
fi
fi
# remove target dir if it exists
rm -rf $2
mkdir $2
cp Android.mk ${targetDir}Android.mk
sourceDirDB=${sourceDir}dhl/database/
sourceDirBin=${sourceDir}bin/
fileArr[0]='catcher_filter.bin'
baseName[0]='catcher_filter_1_lwg_n.bin'
fileArr[1]=$(basename ${sourceDirDB}BPLGUInfo*)
baseName[1]=${fileArr[1]%_P??}_1_lwg_n
fileArr[2]=$(basename ${sourceDirBin}*_PCB01_*.elf)
baseName[2]=$(basename ${sourceDirBin}*_PCB01_*.elf .elf)_1_lwg_n.elf
fileArr[3]=$(basename ${sourceDirBin}DbgInfo*.*.*.*.*)
baseName[3]=${fileArr[3]}_1_lwg_n
fileArr[4]=$(basename ${sourceDirBin}*_PCB01_*.bin)
baseName[4]=modem_1_lwg_n.img
fileArr[5]=$(basename ${sourceDirBin}*DSP*.bin)
baseName[5]=dsp_1_lwg_n.bin
fileArr[6]='~HQ6753_65C_B2B_L1(LWG_DSDS).mak'
baseName[6]=modem_1_lwg_n.mak
echo "-----------------------------------"
for ((i=0;i<2;i++))
do
cp ${sourceDirDB}${fileArr[$i]} ${targetDir}${baseName[$i]}
done
for ((i=2;i<7;i++))
do
cp ${sourceDirBin}${fileArr[$i]} ${targetDir}${baseName[$i]}
done
for ((i=0;i<7;i++))
do
echo "$i ${fileArr[$i]}"
echo "--> ${baseName[$i]}"
done
echo "----cp modem files successfully----"
其运行效果如下:
运行示例.png代码讲解
#!/bin/bash
# Created by Duanze 2015/12/28
# For modem update, to make your hands free :)
一个Shell脚本使用#!
开头,而后面的/bin/bash
则是指明了解释器的具体位置。作为Linux的默认Shell,使用bash
能大大提高脚本的泛用性。
#
号为行注释符。
从运行示例中可以看出
bash modem_tool.sh ./ test/
我们用bash
命令运行脚本,modem_tool.sh
为脚本的文件名,./
,test/
则为运行脚本时给出的两个参数。第一个参数为/database,/bin两个子目录的父级目录——但这不是重点可以忽略,第二个参数为将相应文件重命名后的放置目录。
#if false;then
if [ ! -f Android.mk ]; then
echo " There is no 'Android.mk' !"
exit
fi
#fi
除了之前列出的7个文件之外,modem目录中还需要Android.mk
文件,而我们这里就是使用一个if
语句来判断当前目录下是否存在Android.mk
,如果不存在则输出提示然后退出程序——应该说Shell看上去还是比较丑陋的,if
语句块的结束居然是用反过来的fi
来标志。(如果这种程度你觉得可以忍受的话,case
判断中的esac
应该可以把你击沉。)
[ ! -f Android.mk ]
,在Shell中这叫做“测试”,最后返回true or false
,Shell中与其他编程语言很不一样的一点为0为真值,非0为假值。!
是常见的取反操作,-f
则为文件测试符之一,表示“当文件存在且为普通文件时返回真,否则为假”。
注意外层的
#if false;then
...
#fi
Shell中自身不支持段落注释,故出现了许多种变通的“注释方法”,以上即是一种,只要把#
号去掉,这个判断语句中的代码由于条件为假永远不会执行,也就间接达到了段落注释的效果。
if [ ! $1 ]; then
echo " parameter 1 is null"
exit
fi
我们的脚本需要两个参数,这里就是检测参数是否存在的代码。$1
为“位置参数”,是一种特殊的只读变量,其值只有在脚本运行后才能确定。它表示脚本所接收到的第一个参数,而脚本的多个参数以空格分隔。亦即在命令bash modem_tool.sh ./ test/
中,$1
的值为./
当前路径,$2
的值为test/
目标路径。
sourceDir=$1
targetDir=$2
这里将源路径和目标路径用变量记录下来。注意Shell中做变量赋值时,=
号两边不能有空格。
if [ -d $2 ]; then
read -p " dir $2 exists, program will remove it at frist, are you sure continue?[Y/n]:" N
if [ "y"x != ${N}x -a "Y"x != ${N}x -a ""x != ${N}x ]; then
echo " exit program"
exit
fi
fi
这里我们用到了另一个文件测试符-d
:“当文件存在且是个目录时返回真,否则为假”。使用read
命令输出一行提示的同时等待用户键盘输入,将输入的内容存储至变量 N
。
下一个判断语句中,我们使用了逻辑与-a
来连接三个判断条件,其意为“字符N不为y且不为Y且不为空”。使用!=
来表示不等于应该很好理解,而左右两边多出来的x
大概会让你感到费解。这主要是因为在read
命令中用户可能直接敲下回车从而使得N
为空值,这时若不加x
就会造成"y" !=
这样的报错语句。
另外,由于加了x
,所以我们使用了更标准的变量取值方式${N}
,如果不加花括号的话,就会变成取变量Nx
的值$Nx
。
你可能会觉得这部分的语句块是个很常用的功能,可以封装出一个函数来像是这样:
function confirmOp(){
read -p $1"[Y/n]:" N
if [ "y"x != ${N}x -a "Y"x != ${N}x -a ""x != ${N}x ]; then
echo "exit program"
exit
fi
}
函数中的$1
指的是调用函数时的第一个参数了,调用函数的语句像是这样:
confrimOp "haha" # the value of $1 is "haha"
具体到这个脚本中,你可能会想要这样使用:
confrimOp " dir $2 exists, program will remove it at frist, are you sure continue?"
然而实际运行却达不到你想要的效果,正如前面提到的那样,“多个参数以空格分隔”,函数中取到的$1
将会是dir
。
我的建议是如果你写的脚本不是很复杂,那么不写函数会让可读性更好。
之后几句用过Linux应该都知道,而这几句需要注意一下:
cp Android.mk ${targetDir}Android.mk
sourceDirDB=${sourceDir}dhl/database/
sourceDirBin=${sourceDir}bin/
这种路径使用方式意味着我们使用脚本时传入的两个参数应该带有/
才行。
之后的几句我们使用了数组,记录下“原始文件名“fileArr[X]
以及需要重命名的“目标文件名”baseName[X]
:
fileArr[1]=$(basename ${sourceDirDB}BPLGUInfo*)
baseName[1]=${fileArr[1]%_P??}_1_lwg_n
fileArr[2]=$(basename ${sourceDirBin}*_PCB01_*.elf)
baseName[2]=$(basename ${sourceDirBin}*_PCB01_*.elf .elf)_1_lwg_n.elf
在这其中我们使用了basename
命令来获取文件名,并使用命令替换$()
来让该命令的标准输出作为值赋给变量。
还记得最一开始的“重命名说明”吗?许多原始文件的文件名并非固定,只会满足一定的规律性格式,所以我们在这里用到了通配符*
,*
能匹配除.
和/
外的任意长度的字符串。我们通过${sourceDirDB}BPLGUInfo*
匹配到原始文件,通过basename
命令去除文件名/
左侧的路径字串,然后是使用命令替换$()
将文件名赋值给fileArr[1]
。
basename
还能够去除文件后缀,像是basename ${sourceDirBin}*_PCB01_*.elf .elf
就返回文件名去除后缀.elf
后的字串。
?
也是一个通配符,它能匹配任一单个字符。baseName[1]=${fileArr[1]%_P??}_1_lwg_n
之中的${fileArr[1]%_P??}
我们用到了它,这则是因为笔者公司里的服务器使用的Android
编译脚本比较娇贵,对于表示modem版本号的_P??
会报错,所以这里多了一步将之去除的操作,请结合观看上面的运行示例.png,第1号文件从原始名到目标名的输出。
${fileArr[1]%_P??}
的意思是:
${string%substring} 从变量$string的结尾, 删除最短匹配$substring的子串
更详细的资料可以看这里。
最后,我们使用了for
循环来完成cp
操作以及输出将什么文件重命名为了什么以供肉眼检查:
for ((i=2;i<7;i++))
do
cp ${sourceDirBin}${fileArr[$i]} ${targetDir}${baseName[$i]}
done
for ((i=0;i<7;i++))
do
echo "$i ${fileArr[$i]}"
echo "--> ${baseName[$i]}"
done
for
语句还是挺丑的,但好在总算跟C语言有点相近了,只不过作为强迫症,双重括号实在是……-_-#
好了,光速上手到此为止,Shell除了有点丑之外,既简单又好用的特性还是很值得一学的。
文章的最后,祝各位读者新年愉快!
参考资料
王军《Linux系统命令及Shell脚本实践指南》
网友评论