iOS高级强化--014:Shell实战解析
Xcode执⾏脚本的三种⽅式
方式一
新建
Empty
工程,命名mode1
创建
Target
,选择Aggregate
,命名RunScript
点击
RunScript
,选择Build Phases
,点击+
,选择New Run Script Phase
名称允许重命名,这里修改为
CustomScript
。文本框内可输入脚本代码
支持创建多个
Run Script Phase
方式二
新建
External Build System
工程,命名mode2
和
方式一
有所不同,这里可以配置Build Tool
、Arguments
和Directory
例如:执行一个上传
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
文件的路径- 定义
CMD
和TTY
变量,以供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
中,指定源码文件、framework
、libs
三种文件格式,找到包含main
或AF
关键字的文件列表
【步骤一】
定义变量
#!/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
作为结尾标识- 使用
echo
将help
变量输出
【第三步】
明确
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
命令提供的寻找条件可以是一个用逻辑运算符not
、and
、or
组成的复合条件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 line
与for
循环的区别:
while read line
是一次性将文件信息读入并按行赋值给变量line
,while
中使用重定向机制,文件中的所有信息都被读入并重定向给了整个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
上获得的唯一信息是它的输出(到标准输出和其他文件描述符)及其退出代码(0
到255
之间的数字)- 当打开一个子
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
本地变量,使用echo
将KEYWORD
进行输出,将结果赋值给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