美文网首页程序员Android知识Shell
光速上手Shell——简单批量文件操作为例

光速上手Shell——简单批量文件操作为例

作者: 郭非文 | 来源:发表于2016-01-01 00:22 被阅读898次

    欢迎转载,但请保留作者链接: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脚本实践指南》

    相关文章

      网友评论

        本文标题:光速上手Shell——简单批量文件操作为例

        本文链接:https://www.haomeiwen.com/subject/emiihttx.html