第10章:正则表达式命令

在上一章中,你了解了命令别名命令,以及如何通过ldbinit file持久化命令。不幸的是,命令别名有一些限制。

如果你想执行一个静态的命令,这样创建的别名会很好用,但通常你想把输入的信息输入到一个命令中,以便得到一些有用的输出。

命令别名的不足之处在于它基本上是用实际的命令来代替别名。如果你想在一个命令的中间提供输入,比如一个获取某个对象实例的类的命令,提供对象作为输入怎么办?

幸运的是,有一个优雅的解决方案,可以使用命令重合码向自定义LLDB命令提供输入。

command regex

LLDB 命令 正则表达式命令 的作用与 command alias 很相似,只是你可以为输入提供一个正则表达式,它将被解析并应用于命令的动作部分。

正则表达式命令的输入语法类似于以下内容。

s/<regex>/<subst>/

这是一个普通的正则表达式。它以's/'开头,指定了一个流式编辑器的输入来使用替代命令。部分是指定应该被替换的内容。部分说的是用什么来替换它。

注意:这种语法来自sed终端命令。了解这一点很重要,因为如果你要使用高级模式进行实验,你可以查看sed的手册页面,看看在替代格式化语法中可以实现什么。

是时候看看一个具体的例子了。打开Signals Xcode项目。建立并运行,然后在调试器中暂停应用程序。一旦LLDB控制台启动并准备接受输入,在LLDB中输入以下命令。

(lldb) command regex rlook 's/(.+)/image lookup -rn %1/'

你输入的这个命令将使你的图像重码搜索更容易。你已经创建了一个名为rlook的新命令。这个新的命令在rlook之后的所有内容都用image lookup -rn作为前缀。它是通过一个带有单一匹配器(括号)的词组来实现的,该词组匹配一个或多个字符,并将整个词组替换为image lookup -rn %1。%1指定了匹配器的内容。

因此,举例来说,如果你输入这个

rlook FOO

LLDB实际上会执行以下内容。

image lookup -rn FOO

现在,你不必再输入长得吓人的图像查找-rn,而只需输入rlook即可!这就是LLDB。

但是等等,还有更好的。只要与字符rl没有冲突,你就可以简单地用它来代替。你可以指定任何命令,无论是内置的还是你自己的,通过使用任何不与其他命令共享的前缀。

这意味着你可以用更方便的键入方式轻松搜索像viewDidLoad这样的方法。现在就试试吧。

(lldb) rl viewDidLoad

这将产生当前可执行文件中所有模块的所有viewDidLoad实现。试着把它限制在Signals应用中的代码。

(lldb) rl viewDidLoad Signals

现在你对这个命令满意了,在你的~/.lldbinit文件中添加以下一行代码。

command regex rlook 's/(.+)/image lookup -rn %1/'

注意:实现重码命令的最好方法是在程序运行时使用LLDB。这可以让你迭代命令重码(如果你不满意,可以重新声明)并测试它,而不必重新启动LLDB。一旦你对该命令满意,就把它添加到~/.lldbinit文件中,这样每次LLDB启动时它都会可用。现在,rlook命令将从这里开始对你可用,不再需要痛苦地输入完整的图像查找-rn命令。耶!

执行复杂的逻辑

是时候把regex命令提高一个层次了。你实际上可以用这个命令为一个别名执行多个命令。当LLDB还在暂停时,实现这个新的命令。

(lldb) command regex -- tv 's/(.+)/expression -l objc -O -- @import QuartzCore; [%1 setHidden:!(BOOL)[%1 isHidden]]; (void)[CATransaction flush];/'

这条复杂但有用的命令,将创建一个名为tv(toggle view)的命令,在调试器暂停时切换UIView(或NSView)的开启或关闭。

这条命令中包含了三行独立的代码。

  1. @import QuartzCore 将QuartzCore框架导入到调试器的地址空间。这是必须的,因为在声明之前,调试器不会明白你在执行什么代码。你即将执行QuartzCore框架的代码,所以为了防止它还没有被导入,你现在就导入了。

  2. [%1 setHidden:!(BOOL)[%1 isHidden]]; 根据之前的状态,将视图切换为隐藏或可见。这取决于之前的状态是什么。注意,isHidden不知道返回类型,所以你需要把它转换为Objective-C的BOOL。

  3. 最后一条命令,[CATransaction flush],将冲掉CATransaction队列。在调试器中操作用户界面,通常意味着屏幕不会反映任何更新,直到调试器恢复执行。然而,这个方法将更新屏幕,导致LLDB不需要继续显示视觉变化。

注意:由于输入参数的限制,指定多行输入是不允许的,所以你必须把所有的命令连接到一行。这很难看,但在制作这些正则表达式命令时是必要的。然而,如果你在实际的Objective-C/Swift源代码中这样做的话,愿苹果之神用超长的应用程序审查时间来惩罚你。

只要LLDB还在暂停状态,执行这个新创建的tv命令。

(lldb) tv [[[UIApp keyWindow] rootViewController] view]

调出模拟器来验证视图是否已经消失了。

现在只需在LLDB控制台按回车键,因为LLDB会重复你最后输入的命令。视图将闪回正常状态。

现在你已经完成了tv命令的执行,把它添加到你的~/.lldbinit文件中。

command regex -- tv 's/(.+)/expression -l objc -O -- @import QuartzCore; [%1 setHidden:!(BOOL)[%1 isHidden]]; (void)[CATransaction flush];/'

链式正则表达式输入

选择那个奇怪的流编辑器输入来使用这个命令是有原因的:这种格式可以让你很容易地为同一个命令指定多个动作。当给定多个命令时,正则将尝试匹配每个输入。如果输入匹配,那个特定的就会应用到命令中。如果某条流的输入不匹配,它就会转到下一条命令,看看重码是否能匹配该输入。

在处理内存和寄存器中的对象时,一般需要使用Objective-C上下文。另外,任何以方括号或'@'字符开头的东西都(可能)是Objective-C的。这是因为Swift很难处理内存,而且它不会让你访问寄存器,Swift表达式通常也不会以大括号或'@'字符开始。

你可以使用这些信息来自动检测你需要为一个给定的输入使用哪种上下文。

让我们来看看你如何建立一个命令,从一个对象中获取类的信息,这符合上述要求。

在Xcode中,在Signals.MasterViewController.viewDidLoad()->上创建一个符号断点(确保保持间距)。

建立并运行,然后等待断点被触发。像往常一样,向调试器走去。
$nimen//$
首先,构建出这个新命令的Objective-C实现,getcls。

(lldb) command regex getcls 's/(([0-9]|\$|\@|\[).*)/cpo [%1 class]/'

哇,这个正则让人眼花缭乱。是时候把它分解了。

首先,有一个内部分组,说以下字符可以用来匹配开头。

任何以上述开头的字符都将产生一个匹配。后面是.*,表示0个或更多的字符将产生匹配。

总的来说,这意味着数字、$、@或[,后面的任何字符都会导致命令的匹配和运行cpo [%1类]。再一次,%1被替换成了重构函数中的第一个匹配器。在这种情况下,它是整个命令。内部匹配器(匹配数字、$或其他)将是%2。

试着向getcls命令抛出几个命令,看看它是如何工作的。

(lldb) getcls @"hello world" 
__NSCFString

(lldb) getcls @[@"hello world"]    
__NSSingleObjectArrayI

(lldb) getcls [UIDevice currentDevice] 
UIDevice

(lldb) cpo [UIDevice currentDevice] 
<UIDevice: 0x60800002b520>

(lldb) getcls 0x60800002b520 
UIDevice

棒极了!

然而,这只能处理在Objective-C上下文中有意义的引用,并且与你的命令相匹配。例如,试试下面的方法。

(lldb) getcls self

你会得到一个错误。

error: Command contents 'self' failed to match any regular expression in the 'getcls' regex command.

这是因为你提供的输入没有匹配的regex。让我们为getcls添加一个可以捕获其他形式的输入的重合项。现在在LLDB中键入以下内容。

(lldb) command regex getcls 's/(([0-9]|\$|\@|\[).*)/cpo [%1 class]/' 's/ (.+)/expression -l swift -O -- type(of: %1)/'

这看起来有点复杂,但也不算太糟。该命令的第一部分与你之前添加的相同。但现在你又在结尾处添加了一个词组。这个是一个全局性的,就像你添加的rlook命令一样。这个get-all只是调用type(of:),并将输入作为参数。

尝试为自己再次执行命令。

(lldb) getcls self

现在你会得到预期的Signals.MasterViewController输出。由于你把Swift上下文作为一个集合体,你可以用有趣的方式使用这个命令。

(lldb) getcls self .title

注意里面的空格,它仍然有效。这是因为你告诉Swift上下文可以接受除换行之外的任何东西。

一旦你用完了这个新的、改进的getcls命令,一定要把它添加到你的~/.lldbinit文件中。

提供多个参数

你将在命令重组中探索的最后一个聚会技巧是为命令重组提供多个参数。然而,在这之前,请重新审视第一个命令。

(lldb) command regex rlook 's/(.+)/image lookup -rn %1/'

看一下(.+)。周围的括号使其成为所谓的捕获组。替换的右边的%1表示%1应该被替换成第一个捕获组。因此,这整个正则表达式意味着整个文本被捕获,而图像查找-rn被加在它的前面。

通过提供更多的捕获组,你可以添加更多的参数来进行解析。

在前一章中,你探讨了UIApplication实例的私有 statusBar 属性。

你可以像这样用Objective-C来查找它。

(lldb) ex -l objc -O -- [[UIApplication shared] statusBar]

但这是一个私有的API,意味着你不能通过Swift来调用它,除非你把Objective-C运行时带入,或者在你自己的头中的一个类别中添加statusBar方法。

为了能够在Swift LLDB上下文中执行这个,你可以执行以下内容。

(lldb) ex -l swift -O -UIApplication.shared.perform(NSSelectorFromString("statusBar"))

这是一个有点多的输入。你可以使用正则命令来为这个命令创建多个捕获组。

在LLDB中,键入以下内容。

(lldb) command regex swiftperfsel 's/(.+)\s+(\w+)/expression -l swift -O

-- %1.perform(NSSelectorFromString("%2"))/'

然后给这个命令来个下马威。

(lldb) swiftperfsel UIApplication.shared statusBar

你会得到Swift格式的输出到这个私有属性。

▿ Optional<Unmanaged<AnyObject>> 
  ▿ some : Unmanaged<AnyObject
    - _value : <UIStatusBar_Modern: 0x7fa86dc05820; frame = (0 0; 375 44); autoresize = W+BM; layer = <CALayer: 0x6000018e5000>>

正如你所看到的,添加多个参数可以迅速提高正则表达式的复杂性。通过将多个捕获组与多个链式组结合起来,你可以做出一个相当通用的命令式正则表达式来处理所有类型的可选和必需的输入。然而,这看起来会非常非常难看,因为所有这些都需要在一行中声明。

幸运的是,LLDB有脚本桥接接口--一个功能齐全的Python实现,用于创建高级的LLDB命令来完成你的调试任务。你将在本书的第四节中深入了解脚本桥接。

现在,只需使用命令别名或命令重码来满足你的调试需求。

从这里开始,该怎么做?

回到你在本章中创建的正则表达式命令,并添加语法和帮助帮助文档。


上一章 目录 下一章