当前位置 博文首页 > goodcitizen:使用 shell 脚本自动对比两个安装目录并生成差异补

    goodcitizen:使用 shell 脚本自动对比两个安装目录并生成差异补

    作者:goodcitizen 时间:2021-02-02 10:24

    shell 除了处理一下文本,还有什么想象空间呢?今天拿一个补丁包开刀……

    问题的提出

    公司各个业务线的安装包小则几十兆、大则几百兆,使用自建的升级系统向全国百万级用户下发新版本时,流量耗费相当惊人。有时新版本仅仅改了几个 dll ,总变更量不过几十 K 而已,也要发布一个完整版本。为了降低流量费用,我们推出了补丁升级的方式:产品组将修改的 dll 单独挑选出来,加上一个配置文件压缩成包,上传到自建的升级后台;在客户端,识别到补丁包类型后,手动解压并替换各个 dll 完成安装(之前是直接启动下载好的安装包)。这种方式一经推出,受到了业务线的追捧。然而在使用过程中,也发现一些问题,就是在修改完一个源文件后,受影响的往往不止一个 dll,如果仅把其中一两个 dll 替换了,没替换的 dll 很可能就会和新的 dll 产生接口不兼容,从而引发崩溃。而有的安装包包含了几十个、上百个 dll,如果一一对比,非常费时费力。特别是一些 dll 仅仅是编译时间不一样,通过普通的文件对比工具,根本无法判断这个 dll  的源码有没有改过,这让开发人员非常头大。

    问题的解决

    其实这个问题用 c++ 写个程序是可以解决的,但是一想到要遍历目录、构造文件名 map、对比两个目录中的文件名、对比相同文件名的内容、复制文件到目标目录、压缩目标目录…这一系列操作时,我觉得还是算了 —— 都得从头开始写,工作量不小。而 msys2 或 windows 中就有不少现成的命令可以用,例如对比目录可以用 diff -r 命令、对比 win32 可执行文件可以用 dumpbin /disasm 命令反编译然后再用 diff 命令对比、压缩文件夹可以使用 7z 命令等等,完全不用重复造轮子,直接用 shell 将它们粘合起来就完事了!下面就来看看我是怎么用 shell 脚本来写这个小工具吧。

    处理命令行参数

    这个脚本一开始先处理输入的命令行参数:

      1 # return code:
      2 # 0 : success
      3 # 1 : no difference
      4 # 2 : compress failure
      5 # 3 : create file/dir failure (privilege ?)
      6 # 126 : file/dir existent
      7 # 127 : invalid arguments
      8 
      9 function usage ()
     10 {
     11     echo "Usage: diffpacker.sh -o oldversionfolder -n newversionfolder -r relativepath -x exportfolder -v version [-s sp] [-t (verbose)] [-e (exactmode)]"
     12     
     13     exit 127
     14 }
     15 
     16 srcdir=
     17 dstdir=
     18 reldir=
     19 outdir=
     20 version=
     21 sp=0
     22 verbose=0
     23 exactmode=0
     24 setupdir="setup"
     25 # pure windows utilities subdir
     26 win32="win32" 
     27 
     28 if [ "${$*/-t//}" != "$*" ]; then
     29     # dump parameters when verbose on
     30     echo "total $# param(s):"
     31     for var in $*; do
     32         echo "$var"
     33     done
     34 fi
     35 
     36 
     37 while getopts "o:n:r:x:v:s:te" arg 
     38 do
     39     case $arg in
     40         o)
     41             srcdir=$OPTARG
     42             ;;
     43         n)
     44             dstdir=$OPTARG
     45             ;;
     46         r)
     47             reldir=$OPTARG
     48             ;;
     49         x)
     50             outdir=$OPTARG
     51             ;;
     52         v)
     53             version=$OPTARG
     54             ;;
     55         s)
     56             sp=$OPTARG
     57             ;;
     58         t)
     59             verbose=1
     60             ;;
     61         e) 
     62             exactmode=1
     63             ;;
     64         ?)  
     65             echo "unkonw argument: $arg"
     66             usage
     67             exit 127
     68             ;;
     69     esac
     70 done
     71 
     72 # reldir can be empty
     73 if [ -z "$srcdir" -o -z "$dstdir" -o -z "$outdir" -o -z "$version" ]; then
     74     echo "empty parameter found: $srcdir, $dstdir, $outdir, $version"
     75     usage
     76     exit 127
     77 fi
     78 
     79 #replace all \ to / to avoid shell string choked on \ 
     80 srcdir=${srcdir//\\/\/}
     81 dstdir=${dstdir//\\/\/}
     82 reldir=${reldir//\\/\/}
     83 outdir=${outdir//\\/\/}
     84 
     85 echo "srcdir=$srcdir"
     86 echo "dstdir=$dstdir"
     87 echo "reldir=$reldir"
     88 echo "outdir=$outdir"
     89 echo "version=$version"
     90 echo "sp=$sp"
     91 echo "verbose=$verbose"
     92 echo "exactmode=$exactmode"
     93 echo ""
     94 
     95 if [ ! -d "$srcdir" ]; then 
     96     echo "not a directory : $srcdir"
     97     exit 126
     98 fi
     99 
    100 if [ ! -d "$dstdir" ]; then 
    101     echo "not a directory : $dstdir"
    102     exit 126
    103 fi
    104 
    105 #if [ -e "$outdir" ]; then
    106 resp=$(ls -A "$outdir")
    107 if [ "$resp" != "" ]; then
    108     echo "out directory not empty: $outdir, fatal error!"
    109     exit 126
    110 fi 
    111 
    112 if [ "${outdir:$((${#outdir}-1))}" == "/" ]; then 
    113     # remove tailing /
    114     outdir=${outdir%?}
    115 fi
    116 
    117 if [ ! -z "$reldir" ] && [ "${reldir:$((${#reldir}-1))}" == "/" ]; then 
    118     # remove tailing /
    119     reldir=${reldir%?}
    120 fi
    121 
    122 srcasm="src.asm"
    123 dstasm="dst.asm"
    124 dirdiff="dir.diff"
    125 patdiff="diffpattern.txt"
    126 itemcnt=0
    127 jsonhead=\
    128 "{\n"\
    129 "  \"version\": \"$version\",\n"\
    130 "  \"sp\": \"$sp\",\n"\
    131 "  \"actions\": \n"\
    132 "  [\n"
    133 
    134 json=
    135 jsontail=\
    136 "\n  ]\n"\
    137 "}\n"
    138 
    139 echo "exclude patterns: "
    140 while read line
    141 do
    142     echo $line
    143 done < "$patdiff"
    144 
    145 # to avoid user not end file with \n
    146 if [ ! -z "$line" ]; then 
    147     echo "$line"
    148 fi 
    149 echo ""

    简单解说一下:

    • 16-26:声明用到的变量;
    • 28-34:如果命令行中含有 -t (verbose) 选项,则打印命令行各个参数;
    • 37-70:使用 getopts 命令解析命令行,这个脚本接收以下选项:
      • -o (old) 用于对比的旧目录;
      • -n (new) 用于对比的新目录;
      • -r (relative) 补丁包根目录相对于安装目录的位置,有时可能只针对安装目录的某个子目录进行 patch;
      • -x (output) 输出补丁包的目录;
      • -v (version) 补丁包版本号,写入配置文件用;
      • -s (serial pack) 补丁号,写入配置文件用;
      • -t (verbose) 详细输出;
      • -e (exact mode) 配置文件中增加和替换文件项将按每项对应一段 json 的方式精确设置,否则按整个目录递归覆盖设置。
    • 72-77:空路径校验;
    • 79-83:替换路径中的反斜杠为斜杠,因 shell 会将反斜杠识别为转义字符的开始;
    • 85-93:打印识别后的各选项,方便出问题时排错;
    • 95-120:路径校验,包括:
      • 对比目录不得为普通文件;
      • 输出目录不得含有文件(防止将中间对比结果和上一次或其它对比结果放在一起打包);
      • 剔除输出目录与相对目录的结尾斜杠(方便后续处理)。
    • 122-137:中间变量的定义,包含反编译中间文件、目录对比中间文件、忽略文件模式的中间文件以及生成配置文件的 json 头和尾;
    • 139-149:在对比目录时,用户可以提供一个要忽略的文件模式(pattern)列表,例如不对比 [Dd]ebug、[Ss]ymbol、*.pdb 这些编译中间目录或文件,可以使用正则表达式,每行一个。这里打印这些 pattern 用于排错。

    对比目录

    经过前期的铺垫,进入第一个重头戏:

     1 if [ -f "$patdiff" ]; then
     2     diff -qr "$srcdir" "$dstdir" -X "$patdiff" > "$dirdiff"
     3 else
     4     diff -qr "$srcdir" "$dstdir" > "$dirdiff"
     5 fi 
     6 
     7 while read line
     8 do
     9     if [ $verbose != 0 ]; then 
    10         echo $line
    11     fi 
    12 
    13     tmp=$(echo $line | sed -n 's/Files \(.*\) and \(.*\) differ$/\1\\n\2/p')