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

2021-03-22  本文已影响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
上一篇下一篇

猜你喜欢

热点阅读