美文网首页
iOS高级强化--014:Shell实战解析

iOS高级强化--014:Shell实战解析

作者: 帅驼驼 | 来源:发表于2021-03-22 13:52 被阅读0次
    Xcode执⾏脚本的三种⽅式
    方式一

    新建Empty工程,命名mode1

    创建Target,选择Aggregate,命名RunScript

    点击RunScript,选择Build Phases,点击+,选择New Run Script Phase

    名称允许重命名,这里修改为CustomScript。文本框内可输入脚本代码

    支持创建多个Run Script Phase

    方式二

    新建External Build System工程,命名mode2

    方式一有所不同,这里可以配置Build ToolArgumentsDirectory

    例如:执行一个上传bugly的命令

    java -jar buglySymboliOS.jar -i /Users/zang/Zang/Spark/buglySymboliOS3.0.0/lsj.dSYM -u -id 3a353e096f -key 42a9b82a-79a0-4120-beb4-8fba4d8exxxx -package com.xxxxx.fxxx -version 4.0.123
    

    选择info,进行如下配置

    • Build Tool:配置命令
    • Arguments:配置参数
    • Directory:配置工作目录

    使用External Build System工程,在编译阶段,还可以看到日志的输出

    方式三

    使用xcconfig文件,定义变量

    xcode_run_cmd.sh文件,使用了xcconfig中的变量

    方式三可以将脚本中的关键代码和命令,在项目中使用xcconfig文件进行控制。配合方式一.sh文件一起使用,相对更为灵活

    实战解析
    案例1

    完成一个简单的Shell脚本,可执行Shell命令,将运行结果或错误信息输出到终端

    创建xcode_run_cmd.sh文件,写入以下代码:

    声明RunCMDToTTY函数

    RunCMDToTTY() {
    
       if [[ -n "$1" ]]; then
           CMD="$1"
       fi
    
       if [[ -n "$2" ]]; then
           TTY="$2"
       fi
    
       if [[ ! -n "$TTY" ]]; then
           TTY=`eval "tty"`
       fi
       
       if [[ ! -n "$TTY" ]]; then
           EchoError "=========================================="
           EchoError "ERROR: Not Config tty to output."
           exit -1
       fi
       
       if [[ -n "$CMD" ]]; then
           RunCommand "$CMD"
       else
           EchoError "=========================================="
           EchoError "ERROR:Failed to run CMD. THE CMD must not null"
       fi
    }
    
    • 判断参数1非空,将参数1赋值给CMD变量
    • 判断参数2非空,将参数2赋值给TTY变量
    • 判断TTY变量为空,通过eval "tty"命令获取终端标识
    • 获取终端标识后,如果TTY变量为空,输出错误提示
    • 判断CMD变量非空,调用RunCommand函数,传入CMD变量。否则输出错误提示

    声明RunCommand函数

    declare VERBOSE_SCRIPT_LOGGING=""
    
    RunCommand() {
    
     if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
         echo "♦ $@" 1>$TTY
         echo "-------------------------" 1>$TTY
     fi
    
     echo `$@ &>$TTY`
    
     return $?
    }
    
    • 定义VERBOSE_SCRIPT_LOGGING全局变量,控制是否输出所有参数,用于调试脚本
    • 如果VERBOSE_SCRIPT_LOGGING变量非空,输出所有参数和分割线
    • 通过echo + 反引号执行命令并输出
    • 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误

    声明EchoError函数

    EchoError() {
       if [[ -n "$TTY" ]]; then
           echo "$@" 1>&2>$TTY
       else
           echo "$@" 1>&2
       fi
    }
    
    • 1>&2:将标准输出重定向到标准错误输出,就是以标准错误格式打印所有参数
    • 如果TTY参数非空,通过终端标识输出到指定终端窗口

    测试xcode_run_cmd.sh脚本

    只传入命令,将结果输出在当前终端窗口

    ./xcode_run_cmd.sh 'ls -a'
    -------------------------
    .          .DS_Store           shell
    ..         Common Symbol       xcode_run_cmd.sh
    

    传入命令和终端标识,将结果输出到指定终端标识窗口

    新开一个终端窗口,使用tty获取终端标识

    在原始窗口输入./xcode_run_cmd.sh 'ls -a' '/dev/ttys003'命令

    配合Xcode使用

    搭建一个项目,将xcode_run_cmd.sh脚本拷贝到项目根目录

    创建xcconfig文件,并配置到Tatget上,写入以下代码:

    MACH_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/${PRODUCT_NAME}
    CMD = objdump --macho --syms ${MACH_PATH}
    TTY=/dev/ttys003
    
    • MACHO_PATH:定义变量,存储Mach-O文件的路径
    • 定义CMDTTY变量,以供xcode_run_cmd.sh脚本使用

    点击Target,选择Build Phases,在Run Script中输入:/bin/sh "$SRCROOT/xcode_run_cmd.sh"

    项目编译后,自动将Mach-O中的符号展示到终端,无需手动操作

    附上完整xcode_run_cmd.sh脚本

    #!/bin/sh
    
    declare VERBOSE_SCRIPT_LOGGING=""
    
    RunCommand() {
    
     if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
         echo "♦ $@" 1>$TTY
         echo "-------------------------" 1>$TTY
     fi
    
     echo `$@ &>$TTY`
    
     return $?
    }
    
    EchoError() {
       if [[ -n "$TTY" ]]; then
           echo "$@" 1>&2>$TTY
       else
           echo "$@" 1>&2
       fi
    }
    
    RunCMDToTTY() {
    
       if [[ -n "$1" ]]; then
           CMD="$1"
       fi
    
       if [[ -n "$2" ]]; then
           TTY="$2"
       fi
    
       if [[ ! -n "$TTY" ]]; then
           TTY=`eval "tty"`
       fi
       
       if [[ ! -n "$TTY" ]]; then
           EchoError "=========================================="
           EchoError "ERROR: Not Config tty to output."
           exit -1
       fi
       
       if [[ -n "$CMD" ]]; then
           RunCommand "$CMD"
       else
           EchoError "=========================================="
           EchoError "ERROR:Failed to run CMD. THE CMD must not null"
       fi
    }
    
    RunCMDToTTY "$@"
    
    案例2

    完成一个相对复杂的Shell脚本。指定目录,指定文件格式,在文件内容中搜索关键字,最终列出包含关键字的文件列表

    演示脚本功能:

    使用sh find_api.sh --help命令

    find_api.sh --directory <dir> 在指定目录指定文件内搜索指定关键字。
    
       -d|--directory <dir> - 指定查找目录,默认当前所在目录
       -k|--keyword <word> - 查找关键字
       -s|--source  - 指定查找源码文件
       -f|--framework - 指定查找framework文件
       -l|--lib - 指定查找libs文件
       --help  - prints help screen
    
    • 可指定目录
    • 可指定文件格式
    • 支持长参数,例如:--keyword
    • 支持短参数,例如:-k
    • 可指定多个搜索关键字

    原理:在源码文件中,可以直接使用grep搜索内容,但在目标文件、静态库、动态库中,需要搜索符号表中的信息

    演示执行效果:

    .xcframework中,指定源码文件、frameworklibs三种文件格式,找到包含mainAF关键字的文件列表

    【步骤一】

    定义变量

    #!/bin/sh
    
    declare DIRECTORY="."
    declare SOURCE=""
    declare FRAMEWORK=""
    declare LIB=""
    declare KEYWORD=""
    
    • DIRECTORY:指定搜索的目录
    • SOURCE:是否搜索源码文件
    • FRAMEWORK:是否搜索framework
    • LIB:是否搜索静态库、动态库
    • KEYWORD:搜索关键字

    参数解析

    while [[ $# -gt 0 ]]; do
       case "$1" in
       -d|--directory)
           shift
           DIRECTORY="$1"
           shift
           ;;
       -k|--keyword)
           shift
           if [[ -n $1 ]]; then
               KEYWORD="${KEYWORD}$1\n"
           fi
           shift
           ;;
       -s|--source)
           SOURCE="1"
           shift
           ;;
       -f|--framework)
           FRAMEWORK="1"
           shift
           ;;
       -l|--lib)
           LIB="1"
           shift
           ;;
       -h|--help)
           show_usage
           exit 0
           ;;
       *)
           echo "Unknown option: $1"
           exit 1
       esac
    done
    
    • $#:传入的参数的个数
    • -gt:大于
    • shift:使参数向右发生位移,每次调用shift时,它将所有位置上的参数-1
    • exit:退出当前Shell进程,并返回一个退出状态。退出状态为0表示成功,退出状态为非0表示失败

    代码逻辑:

    • 定义五个变量
    • 当参数个数大于0循环遍历参数
    • 每次获取$1进行参数匹配
    • 命中-d--directory,使用shift让参数位置-1,然后再次获取$1将指定目录赋值给变量,再使用shift让参数位置-1
    • 命中-k--keyword,同理,获取$1将指定关键字赋值给变量
    • 命中-s--source,将搜索源码文件的标识设置为1,使用shift让参数位置-1
    • 命中-f--framework,同理,将搜索framework的标识设置为1
    • 命中-l--lib,同理,将搜索静态库、动态库的标识设置为1
    • 命中-h--help,调用show_usage函数,退出当前Shell进程,并返回0表示成功
    • 以上均未命中,将参数输出,退出当前Shell进程,并返回1表示失败
    • 其中-k--keyword支持多个参数,这里使用换行符,将多个关键字拼接到一起

    【第二步】

    声明show_usage函数

    function show_usage() {
    
       local help=$(cat <<EOF
    
           find_api.sh --directory <dir> 在指定目录指定文件内搜索指定关键字。
    
               -d|--directory <dir> - 指定查找目录,默认当前所在目录
               -k|--keyword <word> - 查找关键字
               -s|--source - 指定查找源码文件
               -f|--framework - 指定查找framework文件
               -l|--lib - 指定查找libs文件
               -h|--help - prints help screen
    EOF)
       echo "$help"
    }
    
    • EOF只是一个标识而已,可以替换成任意的合法字符
    • EOF作为结尾的标识一定要顶格写,前面不能有任何字符
    • EOF作为结尾的标识后面也不能有任何的字符(包括空格)
    • EOF作为起始的标识前后的空格会被省略掉
    • 使用$()包装成命令,作用与反引号一样,此处不加也行

    代码逻辑:

    • 声明show_usage函数,参数命中-h--help时,输出帮助信息
    • 定义help本地变量
    • 使用$()包装成命令,赋值给help变量
    • 使用cat <<,将带有结束标志的文档内容传递到命令的标准输入
    • 开始的EOF作为起始标识
    • 最后的EOF作为结尾标识
    • 使用echohelp变量输出

    【第三步】

    明确find_api.sh的两个核心原理:

    • 在目录中找到指定格式的文件
    • 在文件中搜索指定关键字

    在目录中找到指定格式的文件

    find命令:从指定的起始目录开始,递归地搜索其各个子目录,查找满足寻找条件的文件并对之采取相关的操作

    .xcframework文件中,递归搜索.framework文件

    find ./mm.xcframework -name "*.framework"
    -------------------------
    ./mm.xcframework/SYTimer.xcframework/ios-arm64_i386_x86_64-simulator/SYTimer.framework
    ./mm.xcframework/SYTimer.xcframework/ios-arm64_armv7/SYTimer.framework
    
    • -name:查找文件名匹配字符串的所有文件,可用通配符*?[]

    .xcframework文件中,递归搜索.a文件和.o文件

    find ./mm.xcframework \( -name "*.a" -o -name "*.o" \)
    -------------------------
    ./mm.xcframework/test.o
    ./mm.xcframework/libAFNetworking.a
    
    • find命令提供的寻找条件可以是一个用逻辑运算符notandor组成的复合条件
    • or:逻辑或,在命令中用-o表示。该运算符表示只要所给的条件中有一个满足时,寻找条件就算满足
    • and:逻辑与,在命令中用-a表示,是系统缺省的选项,表示只有当所给的条件都满足时,寻找条件才算满足
    • not:逻辑非,在命令中用!表示。该运算符表示查找不满足所给条件的文件
    • 当使用很多的逻辑选项时,可以用括号把这些选项括起来。为了避免Shell本身对括号引起误解,在括号前需要加转义字符\来去除括号的意义

    -exec 命令名称 {}:对符合条件的文件执行所给的命令,而不询问用户是否需要执行该命令

    find ./mm.xcframework \( -name "*.a" -o -name "*.o" \) -exec echo {} \;
    -------------------------
    ./mm.xcframework/test.o
    ./mm.xcframework/libAFNetworking.a
    
    • {}:表示命令的参数即为所找到的文件
    • 命令的末尾必须加上终结符,终结符有;+两种。其中;会对每一个find到的文件去执行一次cmd命令。而+find到的文件一次性执行完cmd命令

    在文件中搜索指定关键字

    如果在.h.m.swift文件格式中搜索,可以直接使用grep命令

    grep命令:在文件中搜索关键字,命令会返回一个包含关键字的文本行

    test.m文件中,搜索@"SomeNewFunction_weak_import"

    grep "@\"SomeNewFunction_weak_import\"" ./mm.xcframework/test.m
    -------------------------
       NSLog(@"SomeNewFunction_weak_import");
    

    使用-A 1参数输出结果之后一行,使用-B 1参数输出结果之前一行

    grep "@\"SomeNewFunction_weak_import\"" -A 1 -B 1 ./mm.xcframework/test.m
    -------------------------
    void SomeNewFunction_weak_import(void) {
       NSLog(@"SomeNewFunction_weak_import");
    }
    

    使用-i参数,忽略字符大小写的差别

    grep "@\"somenewfunction_weak_import\"" -A 1 -B 1 -i ./mm.xcframework/test.m
    -------------------------
    void SomeNewFunction_weak_import(void) {
       NSLog(@"SomeNewFunction_weak_import");
    }
    

    使用-E参数,使用正则表达式搜索关键字

    grep -E "LG_Cat-1|LG_Cat-2" -A 1 -B 1 -i ./mm.xcframework/test.m
    -------------------------
       // 外部
       NSLog(@"LG_Cat-1");
       int a[4] = {1,2,3,4};
    --
    --
       int a[4] = {1,2,3,4};
       NSLog(@"LG_Cat-2");
       int m = 10;
    

    如果在目标文件、静态库、动态库中搜索,需要使用nm命令

    nm命令:被用于显示二进制目标文件的符号表

    SYTimer动态库的符号表中搜索关键字

    nm -pa ./mm.xcframework/SYTimer.framework/SYTimer | grep -E "nextFireTime"
    -------------------------
    000057b4 t -[SYTimerBase(Private) nextFireTime]
    0000000000006f08 t -[SYTimerBase(Private) nextFireTime]
    

    【第四步】

    拼接KEYWORD

    无论使用grep命令还是nm命令,在搜索关键字时都会使用正则的匹配格式,所以需要将KEYWORD按照key1|key2|key3的格式拼接

    【步骤一】中,已经将多个关键字按照回车符进行拼接,这里声明Find_Api函数,在主函数中进行二次处理

    read命令:从键盘读取变量的值,通常用在Shell脚本中与用户进行交互的场合。该命令可以一次读取多个变量的值,变量和输入的值都需要使用空格隔开。在read命令后面,如果没有指定变量名,读取的数据将被自动赋值给特定的变量REPLY

    • -a:后跟一个变量,该变量会被认为是个数组,然后给其赋值,默认是以空格为分割符
    • -r:屏蔽\,如果没有该选项,则\作为一个转义字符,有的话\就是个正常的字符

    read通过输入重定向,把file的第一行所有的内容赋值给变量line,循环体内的命令一般包含对变量line的处理;然后循环处理file的第二行、第三行。。。一直到file的最后一行

    read命令也有退出状态,当它从文件file中读到内容时,退出状态为0,循环继续进行。当read从文件中读完最后一行后,下次便没有内容可读了,此时read的退出状态为非0,所以循环才会退出

    while read linefor循环的区别:

    while read line是一次性将文件信息读入并按行赋值给变量linewhile中使用重定向机制,文件中的所有信息都被读入并重定向给了整个while语句中的line变量

    坑点一:

    使用echo输出KEYWORD,通过管道,将内容作为while read命令的标准输入

    function Find_Api() {
    
       if [[ ! -n "${KEYWORD}" ]]; then
           echo "请输入查找的关键字!"
           exit 1
       fi
    
       local key_word=""
    
       echo ${KEYWORD} | while read name; do
           if [[ ! -n "${name}" ]]; then
               continue
           fi
    
           if [[ -n "${key_word}" ]]; then
               key_word="${key_word}|${name}"
           else
               key_word="${name}"
           fi
    
           echo "${key_word}------内部"
       done
    
       echo "${key_word}------外部"
    }
    

    测试脚本的输出结果:

    sh find_api.sh -k "cat" -k "kc" -k "hk" -k "kd"
    -------------------------
    cat------内部
    cat|kc------内部
    cat|kc|hk------内部
    cat|kc|hk|kd------内部
    ------外部
    
    • while循环中的打印没有任何问题,但是在循环之外打印,key_word的值莫名其妙的置空了

    上述问题,因为使用管道而产生

    • 在大多数Shell中(包括bash),管道的每一侧都在子Shell中运行,因此,Shell内部状态的任何更改(例如,设置变量)都仅限于管道的该段。您可以从子Shell上获得的唯一信息是它的输出(到标准输出和其他文件描述符)及其退出代码(0255之间的数字)
    • 当打开一个子Shell时,父Shell里面中的系统环境变量会被复制到子Shell中(用export定义的变量才是系统环境变量)
    • 一个Shell中的系统环境变量只对该Shell或者它的子Shell有效,该Shell结束时变量消失,并不能返回到父Shell
    • 不用export定义的变量只对该Shell有效,对子Shell也是无效的
    • 直接执行一个脚本文件是在一个子Shell中运行的,而在脚本前加source,则是在当前Shell环境中直接运行(不是子Shell

    坑点二:

    解决子Shell问题,需要避免管道的使用

    修改方案,使用<<<,表示将右侧的字符串传递到左侧命令的标准输入

    function Find_Api() {
    
       if [[ ! -n "${KEYWORD}" ]]; then
           echo "请输入查找的关键字!"
           exit 1
       fi
    
       local key_word=""
    
       while read name; do
    
           if [[ ! -n "${name}" ]]; then
               continue
           fi
    
           if [[ -n "${key_word}" ]]; then
               key_word="${key_word}|${name}"
           else
               key_word="${name}"
           fi
           
           echo ${key_word}
    
       done <<< "${KEYWORD}"
    }
    

    测试脚本的输出结果:

    sh find_api.sh -k "cat" -k "kc" -k "hk" -k "kd"
    -------------------------
    catnkcnhknkdn
    
    • 文本中间的\n,没有被识别为换行,而是被当做\n输出了,所以while read逐行读取没有生效

    解决办法:

    定义tmp本地变量,使用echoKEYWORD进行输出,将结果赋值给tmp

    function Find_Api() {
    
       if [[ ! -n "${KEYWORD}" ]]; then
           echo "请输入查找的关键字!"
           exit 1
       fi
    
       local tmp=$(echo ${KEYWORD})
    
       local key_word=""
    
       while read name; do
    
           if [[ ! -n "${name}" ]]; then
               continue
           fi
    
           if [[ -n "${key_word}" ]]; then
               key_word="${key_word}|${name}"
           else
               key_word="${name}"
           fi
    
       done <<< "${tmp}"
    
       echo ${key_word}
    }
    

    测试脚本的输出结果:

    sh find_api.sh -k "cat" -k "kc" -k "hk" -k "kd"
    -------------------------
    cat|kc|hk|kd
    
    • 结果符合预期,问题完美解决

    【第五步】

    拼接将要搜索的文件格式

    declare SOURCE_EXTENSION='*.h *.m *.mm *.c *.hpp *.cpp *.swift *.xcconfig'
    declare FRAMEWORK_EXTENSION='*.framework *.o *.tbd'
    declare LIB_EXTENSION='*.a *.dylib'
    
    • SOURCE_EXTENSION:指定源码文件包含的文件格式
    • FRAMEWORK_EXTENSION:指定framework包含的文件格式
    • LIB_EXTENSION:指定libs包含的文件格式
       local find_name=""
    
       if [[ -n "${SOURCE}" ]]; then
           find_name="${SOURCE_EXTENSION}"
       fi
           
       if [[ -n "${FRAMEWORK}" ]]; then
           find_name="${find_name} ${FRAMEWORK_EXTENSION}"
       fi
           
       if [[ -n "${LIB}" ]]; then
           find_name="${find_name} ${LIB_EXTENSION}"
       fi
           
       if [[ ! -n "${find_name}" ]]; then
           find_name="${SOURCE_EXTENSION} ${FRAMEWORK_EXTENSION} ${LIB_EXTENSION}"
       fi
    
       echo "${find_name}------"
    
    • 定义find_name本地变量
    • 如果指定SOURCE,将SOURCE_EXTENSION赋值给find_name
    • 如果指定FRAMEWORK,追加FRAMEWORK_EXTENSION内容
    • 如果指定LIB,追加LIB_EXTENSION内容
    • 如果最终指定的find_name为空,默认搜索全部格式

    测试脚本的输出结果:

    sh find_api.sh -k "cat" -k "kc" -k "hk" -k "kd"
    -------------------------
    *.h *.m *.mm *.c *.hpp *.cpp *.swift *.xcconfig *.framework *.o *.tbd *.a *.dylib
    

    将结果按照find命令的参数格式拼接:-name p1 -o -name p2 -o -name p3...

    坑点一:

       local need_name=""
    
       for name in ${find_name}
       do
           if [[ ! -n "${need_name}" ]]; then
               need_name="-name \${name}"
           else
               need_name="${need_name} -o -name ${name}"
           fi
              
       done
    
       echo ${need_name}
    

    测试脚本的输出结果:

    sh find_api.sh -k "cat" -k "kc" -k "hk" -k "kd"
    -------------------------
    -name *.h -o -name *.m -o -name *.mm -o -name *.c -o -name *.hpp -o -name *.cpp -o -name *.swift -o -name *.xcconfig -o -name ws.framework -o -name *.o -o -name *.tbd -o -name *.a -o -name *.dylib
    
    • 因为目录中存在ws.framework文件,和find_api.sh平级。导致原本的*.framework输出变成了ws.framework

    使用set -x命令,显示该指令及所下的参数

    set -x
    find . -name *.framework
    -------------------------
    + find . -name ws.framework
    
    • 不加引号的*,首先会被bash进行扩展,所以find . -name *.framework在执行find命令前,bash先把*.framework替换成了ws.framework,然后find命令看到的参数实际上是ws.framework
    set -x 
    find . -name "*.framework"
    -------------------------
    + find . -name '*.framework'
    
    • 加了引号,bash就不去做替换了,那么find命令看到的参数就是*.framework

    坑点二:

    修改代码,在拼接name变量时,前后加上\"

           if [[ ! -n "${need_name}" ]]; then
               need_name="-name \"${name}\""
           else
               need_name="${need_name} -o -name \"${name}\""
           fi
    

    测试脚本的输出结果:

    sh find_api.sh -k "cat" -k "kc" -k "hk" -k "kd"
    -------------------------
    -name "*.h" -o -name "*.m" -o -name "*.mm" -o -name "*.c" -o -name "*.hpp" -o -name "*.cpp" -o -name "*.swift" -o -name "*.xcconfig" -o -name "ws.framework" -o -name "*.o" -o -name "*.tbd" -o -name "*.a" -o -name "*.dylib"
    
    • 问题并没有解决,输出内容变成了"ws.framework"

    这里还存在另一个问题,循环时使用的for name in ${find_name},它会将find_name的内容以空格分割,此时*.framework已经被扩展为ws.framework

    解决办法:

    使用read -a命令,指定find_name为数组

       local need_name=""
    
       read -a find_name <<< "$find_name"
    
       for name in "${find_name[@]}"
       do
           if [[ ! -n "${need_name}" ]]; then
               need_name="-name \"${name}\""
           else
               need_name="${need_name} -o -name \"${name}\""
           fi
              
       done
    
       echo ${need_name}
    

    测试脚本的输出结果:

    sh find_api.sh -k "cat" -k "kc" -k "hk" -k "kd"
    -------------------------
    -name "*.h" -o -name "*.m" -o -name "*.mm" -o -name "*.c" -o -name "*.hpp" -o -name "*.cpp" -o -name "*.swift" -o -name "*.xcconfig" -o -name "*.framework" -o -name "*.o" -o -name "*.tbd" -o -name "*.a" -o -name "*.dylib"
    

    【第六步】

    使用find命令,获取文件列表

    find $DIRECTORY  \( $need_name \)
    

    测试脚本的输出结果:

    sh find_api.sh -d ./mm.xcframework -k "main"
    -------------------------
    
    
    • 没有输出任何结果

    使用set -x命令,显示该指令及所下的参数

    set -x 
    find $DIRECTORY  \( $need_name \)
    -------------------------
    + find ./mm.xcframework '(' -name '"*.h"' -o -name '"*.m"' -o -name '"*.mm"' -o -name '"*.c"' -o -name '"*.hpp"' -o -name '"*.cpp"' -o -name '"*.swift"' -o -name '"*.xcconfig"' -o -name '"*.framework"' -o -name '"*.o"' -o -name '"*.tbd"' -o -name '"*.a"' -o -name '"*.dylib"' ')'
    
    • 找到问题所在,文件格式被引号包裹两层。例如*.h,被包裹成'"*.h"'

    使用eval命令,用于重新运算求出参数的内容

    set -x 
    eval "find $DIRECTORY  \( $need_name \)"
    -------------------------
    + eval 'find ./mm.xcframework  \( -name "*.h" -o -name "*.m" -o -name "*.mm" -o -name "*.c" -o -name "*.hpp" -o -name "*.cpp" -o -name "*.swift" -o -name "*.xcconfig" -o -name "*.framework" -o -name "*.o" -o -name "*.tbd" -o -name "*.a" -o -name "*.dylib" \)'
    ++ find ./mm.xcframework '(' -name '*.h' -o -name '*.m' -o -name '*.mm' -o -name '*.c' -o -name '*.hpp' -o -name '*.cpp' -o -name '*.swift' -o -name '*.xcconfig' -o -name '*.framework' -o -name '*.o' -o -name '*.tbd' -o -name '*.a' -o -name '*.dylib' ')'
    ./mm.xcframework/SYTimer.xcframework/ios-arm64_i386_x86_64-simulator/SYTimer.framework
    ./mm.xcframework/SYTimer.xcframework/ios-arm64_i386_x86_64-simulator/SYTimer.framework/Headers/SYTimer.h
    ./mm.xcframework/SYTimer.xcframework/ios-arm64_i386_x86_64-simulator/SYTimer.framework/Headers/SYShareTimer.h
    ./mm.xcframework/SYTimer.xcframework/ios-arm64_i386_x86_64-simulator/SYTimer.framework/Headers/SYRunLoop.h
    
    ...
    
    • 扫描第一次将外面的双引号去掉
    • 扫描第二次作为find命令的参数,执行成功,输出文件列表

    【第七步】

    遍历文件列表,搜索关键字

       for file in $(eval "find $DIRECTORY  \( $need_name \)")
       do
           echo "${file}"
       done
    

    测试脚本的输出结果:

    sh find_api.sh -d ./mm.xcframework -k "main"
    -------------------------
    ./mm.xcframework/SYTimer.xcframework/ios-arm64_armv7/SYTimer.framework/Headers/SYThreadTimers.h
    ./mm.xcframework/Target
    Support
    Files/Pods-LoginApp/Pods-LoginApp.debug.xcconfig
    
    ...
    
    • 对于中间包含空格的目录,原本是Target Support Files,但遍历时,for循环按空格切分,导致目录被拆分为多条

    使用while read命令,代替for循环

       local files=$(eval "find $DIRECTORY  \( $need_name \)")
    
       while read file; do
           echo "${file}"
       done <<< "${files}"
    -------------------------
    ./mm.xcframework/SYTimer.xcframework/ios-arm64_armv7/SYTimer.framework/Headers/SYThreadTimers.h
    ./mm.xcframework/Target Support Files/Pods-LoginApp/Pods-LoginApp.debug.xcconfig
    ./mm.xcframework/test.o
    
    ...
    

    问题完美解决,Target Support Files作为整体路径被输出

    搜索关键字,有以下三种情况:

    • 对于源码文件,可以直接使用grep命令
    • 对于.o.a文件,需要使用nm命令查找符号表
    • 对于.framework文件,也是目录格式,需要先进入x.framework目录,对x进行符号表查找
           if [[ -d "${file}" ]]; then
               local name=`basename "$file"`
               pushd "${file}" > /dev/null
               if nm -pa "${name/.framework}" | grep -E --color=auto "$key_word"; then
                   echo "在文件 \033[37;32;4m${file}\033[39;49;0m 中找到了(\033[37;31;4m${key_word}\033[39;49;0m)关键字!\n\n\n"
               fi
               popd > /dev/null
           else
    
               local is_source=""
    
               for source in ${SOURCE_EXTENSION}
               do
                   if [[ "*.${file##*.}" = "${source}" ]]; then
                       is_source="1"
                       break
                   fi
               done
    
               if [[ -n ${is_source} ]]; then
                   if grep -E --color=auto "${key_word}" "${file}"; then
                       echo  "在文件 \033[37;32;4m${file}\033[39;49;0m 中找到了(\033[37;31;4m${key_word}\033[39;49;0m)关键字!\n\n\n"
                   fi
               else
                   if nm -pa "${file}" | grep -E --color=auto "$key_word"; then
                       echo "在文件 \033[37;32;4m${file}\033[39;49;0m 中找到了(\033[37;31;4m${key_word}\033[39;49;0m)关键字!\n\n\n"
                   fi
               fi
           fi
    

    代码逻辑:

    • 如果是目录,通过basename命令获取文件名
    • 使用pushd命令,将目录添加到目录堆栈顶部
    • 使用参数扩展去掉文件名的.framework后缀,通过nm命令查找符号表
    • 使用popd命令,从目录堆栈中删除目录
    • 如果是文件,判断文件格式是否属于源码文件
    • 如果是源码文件,通过grep命令搜索关键字
    • 如果非源码文件,通过nm命令查找符号表

    测试脚本的输出结果:

    sh find_api.sh -d ./mm.xcframework -k "main"
    -------------------------
    0000000000005527 t +[SYRunLoop main]
    0000000000006f4c t +[SYTimer mainRunLoopTimerWithRunLoopMode:block:]
    000000000000f5d0 b __ZL13s_mainRunLoop
                    U __dispatch_main_q
                    U _pthread_main_np
    在文件 ./mm.xcframework/SYTimer.xcframework/ios-arm64_i386_x86_64-simulator/SYTimer.framework 中找到了(main)关键字!
    
    
    
    + (instancetype)main;
    在文件 ./mm.xcframework/SYTimer.xcframework/ios-arm64_i386_x86_64-simulator/SYTimer.framework/Headers/SYRunLoop.h 中找到了(main)关键字!
    
    
    
    /// Initializes a new SYTimer object using the block as the main body of execution for the timer. This timer will scheduled on main run loop.
    + (instancetype)mainRunLoopTimerWithRunLoopMode:(CFRunLoopMode)runLoopMode
    在文件 ./mm.xcframework/SYTimer.xcframework/ios-arm64_i386_x86_64-simulator/SYTimer.framework/Headers/SYTimerBase.h 中找到了(main)关键字!
    
    ...
    

    附上完整find_api.sh脚本

    #!/bin/sh
    
    declare SOURCE=""
    declare FRAMEWORK=""
    declare LIB=""
    declare DIRECTORY="."
    declare KEYWORD=""
    
    declare SOURCE_EXTENSION='*.h *.m *.mm *.c *.hpp *.cpp *.swift *.xcconfig'
    declare FRAMEWORK_EXTENSION='*.framework *.o *.tbd'
    declare LIB_EXTENSION='*.a *.dylib'
    
    function Find_Api() {
    
       if [[ ! -n "${KEYWORD}" ]]; then
           echo "请输入查找的关键字!"
           exit 1
       fi
    
       local tmp=$(echo ${KEYWORD})
    
       local key_word=""
    
       while read name; do
    
           if [[ ! -n "${name}" ]]; then
               continue
           fi
    
           if [[ -n "${key_word}" ]]; then
               key_word="${key_word}|${name}"
           else
               key_word="${name}"
           fi
    
       done <<< "${tmp}"
    
       local find_name=""
    
       if [[ -n "${SOURCE}" ]]; then
           find_name="${SOURCE_EXTENSION}"
       fi
           
       if [[ -n "${FRAMEWORK}" ]]; then
           find_name="${find_name} ${FRAMEWORK_EXTENSION}"
       fi
           
       if [[ -n "${LIB}" ]]; then
           find_name="${find_name} ${LIB_EXTENSION}"
       fi
           
       if [[ ! -n "${find_name}" ]]; then
           find_name="${SOURCE_EXTENSION} ${FRAMEWORK_EXTENSION} ${LIB_EXTENSION}"
       fi
    
       local need_name=""
    
       read -r -a find_name <<< "$find_name"
    
       for name in "${find_name[@]}"
       do
           if [[ ! -n "${need_name}" ]]; then
               need_name="-name \"${name}\""
           else
               need_name="${need_name} -o -name \"${name}\""
           fi
              
       done
    
       local file_count=0
       local find_count=0
       local files=$(eval "find $DIRECTORY  \( $need_name \)")
    
       while read file; do
    
           file_count=$(( file_count + 1 ))
    
           if [[ -d "${file}" ]]; then
               local name=`basename "$file"`
               pushd "${file}" > /dev/null
               if nm -pa "${name/.framework}" | grep -E --color=auto "$key_word"; then
                   find_count=$(( find_count + 1 ))
                   echo "在文件 \033[37;32;4m${file}\033[39;49;0m 中找到了(\033[37;31;4m${key_word}\033[39;49;0m)关键字!\n\n\n"
               fi
               popd > /dev/null
           else
    
               local is_source=""
    
               for source in ${SOURCE_EXTENSION}
               do
                   if [[ "*.${file##*.}" = "${source}" ]]; then
                       is_source="1"
                       break
                   fi
               done
    
               if [[ -n ${is_source} ]]; then
                   if grep -E --color=auto "${key_word}" "${file}"; then
                       find_count=$(( find_count + 1 ))
                       echo  "在文件 \033[37;32;4m${file}\033[39;49;0m 中找到了(\033[37;31;4m${key_word}\033[39;49;0m)关键字!\n\n\n"
                   fi
               else
                   if nm -pa "${file}" | grep -E --color=auto "$key_word"; then
                       find_count=$(( find_count + 1 ))
                       echo "在文件 \033[37;32;4m${file}\033[39;49;0m 中找到了(\033[37;31;4m${key_word}\033[39;49;0m)关键字!\n\n\n"
                   fi
               fi
           fi
    
       done <<< "${files}"
    
       echo "共扫描 \033[37;32;4m${file_count}\033[39;49;0m 个文件,发现 \033[37;32;4m${find_count}\033[39;49;0m 个文件包含(\033[37;31;4m${key_word}\033[39;49;0m)关键字!"
    }
    
    function show_usage() {
    
       local help=$(cat <<EOF
    
           find_api.sh --directory <dir> 在指定目录指定文件内搜索指定关键字。
    
               -d|--directory <dir> - 指定查找目录,默认当前所在目录
               -k|--keyword <word> - 查找关键字
               -s|--source - 指定查找源码文件
               -f|--framework - 指定查找framework文件
               -l|--lib - 指定查找libs文件
               -h|--help - prints help screen
    EOF)
       echo "$help"
    }
    
    while [[ $# -gt 0 ]]; do
       case "$1" in
       -d|--directory)
           shift
           DIRECTORY="$1"
           shift
           ;;
       -k|--keyword)
           shift
    
           if [[ -n $1 ]]; then
               KEYWORD="${KEYWORD}$1\n"
           fi
    
           shift
           ;;
       -s|--source)
           SOURCE="1"
           shift
           ;;
       -f|--framework)
           FRAMEWORK="1"
           shift
           ;;
       -l|--lib)
           LIB="1"
           shift
           ;;
       -h|--help)
           show_usage
           exit 0
           ;;
       *)
           echo "Unknown option: $1"
           exit 1
       esac
    done
    
    Find_Api
    

    相关文章

      网友评论

          本文标题:iOS高级强化--014:Shell实战解析

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