光速上手Shell——简单批量文件操作为例
发布日期:2021-11-09 22:50:57 浏览次数:40 分类:技术文章

本文共 5973 字,大约阅读时间需要 19 分钟。

阅读前提:你应该使用过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;thenif [ ! -f Android.mk ]; then    echo " There is no 'Android.mk' !"    exitfi#fiif [ ! $1 ]; then    echo " parameter 1 is null"    exitfiif [ ! $2 ]; then    echo " parameter 2 is null"    exitfisourceDir=$1targetDir=$2if [ ! -d $1 ];then    echo "dir $1 don't exist"    exitfiif [ -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    fifi# remove target dir if it existsrm -rf $2mkdir $2cp Android.mk ${targetDir}Android.mksourceDirDB=${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_nfileArr[2]=$(basename ${sourceDirBin}*_PCB01_*.elf)baseName[2]=$(basename ${sourceDirBin}*_PCB01_*.elf .elf)_1_lwg_n.elffileArr[3]=$(basename ${sourceDirBin}DbgInfo*.*.*.*.*)baseName[3]=${fileArr[3]}_1_lwg_nfileArr[4]=$(basename ${sourceDirBin}*_PCB01_*.bin)baseName[4]=modem_1_lwg_n.imgfileArr[5]=$(basename ${sourceDirBin}*DSP*.bin)baseName[5]=dsp_1_lwg_n.binfileArr[6]='~HQ6753_65C_B2B_L1(LWG_DSDS).mak'baseName[6]=modem_1_lwg_n.makecho "-----------------------------------"for ((i=0;i<2;i++))do    cp ${sourceDirDB}${fileArr[$i]} ${targetDir}${baseName[$i]}donefor ((i=2;i<7;i++))do    cp ${sourceDirBin}${fileArr[$i]} ${targetDir}${baseName[$i]}donefor ((i=0;i<7;i++))do    echo "$i ${fileArr[$i]}"    echo "--> ${baseName[$i]}"doneecho "----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;thenif [ ! -f Android.mk ]; then    echo " There is no 'Android.mk' !"    exitfi#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"    exitfi

我们的脚本需要两个参数,这里就是检测参数是否存在的代码。$1为“位置参数”,是一种特殊的只读变量,其值只有在脚本运行后才能确定。它表示脚本所接收到的第一个参数,而脚本的多个参数以空格分隔。亦即在命令bash modem_tool.sh ./ test/中,$1的值为./当前路径,$2的值为test/目标路径。

sourceDir=$1targetDir=$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    fifi

这里我们用到了另一个文件测试符-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.mksourceDirDB=${sourceDir}dhl/database/sourceDirBin=${sourceDir}bin/

这种路径使用方式意味着我们使用脚本时传入的两个参数应该带有/才行。

之后的几句我们使用了数组,记录下“原始文件名“fileArr[X]以及需要重命名的“目标文件名”baseName[X]

fileArr[1]=$(basename ${sourceDirDB}BPLGUInfo*)baseName[1]=${fileArr[1]%_P??}_1_lwg_nfileArr[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]}donefor ((i=0;i<7;i++))do    echo "$i ${fileArr[$i]}"    echo "--> ${baseName[$i]}"done

for语句还是挺丑的,但好在总算跟C语言有点相近了,只不过作为强迫症,双重括号实在是……-_-#

好了,光速上手到此为止,Shell除了有点丑之外,既简单又好用的特性还是很值得一学的。

文章的最后,祝各位读者新年愉快!

参考资料

王军《Linux系统命令及Shell脚本实践指南》

转载地址:https://blog.csdn.net/dbnight/article/details/50688324 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:「打造自己的Library」SharedPreferences篇
下一篇:「深入Java」类型信息:RTTI和反射

发表评论

最新留言

第一次来,支持一个
[***.219.124.196]2024年04月18日 18时39分36秒