Chapters

Hide chapters

Advanced Apple Debugging & Reverse Engineering

Third Edition · iOS 12 · Swift 4.2 · Xcode 10

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section III: Low Level

Section 3: 7 chapters
Show chapters Hide chapters

Section IV: Custom LLDB Commands

Section 4: 8 chapters
Show chapters Hide chapters

24. Script Bridging with Options & Arguments
Written by Derek Selander

When you’re creating a custom debugging command, you’ll often want to slightly tweak functionality based upon options or arguments supplied to your command. A custom LLDB command that can do a job only one way is a boring one-trick pony.

In this chapter, you’ll explore how to pass optional parameters (a.k.a. options) as well as arguments (parameters which are expected) to your custom command to alter functionality or logic in your custom LLDB scripts.

You’ll continue working with the bar (“break-after-regex”) command you created in the previous chapter. In this chapter, you’ll finish up the bar command by adding logic to handle options in your script.

By the end of this chapter, the bar command will have logic to handle the following optional parameters:

  • Non-regular expression search: Using the -n or --non_regex option will result in the bar command using a non-regular expression breakpoint search instead. This option will not take any additional parameters.
  • Filter by module: Using the -m or --module option will only search for breakpoints in that particular module. This option will expect an additional parameter which specifies the name of the module.
  • Stop on condition: Using the -c or --condition option, the bar command will evaluate the given condition after stepping out of the current function. If True, execution will stop. If False, execution will continue. This option will expect an additional parameter which is a string of code that will be executed and evaluated as an Objective-C BOOL.

This will be a dense but fun chapter. Make sure you’ve got a good supply of caffeine!

Setting up

If you’ve gone through the previous chapter and your bar command is working, then you can continue using that script and ignore this part. Otherwise, head on over to the starter folder in this chapter’s resources, and copy the BreakAfterRegex.py file into your ~/lldb folder. Make sure your ~/.lldbinit file has the following line which you should have from the previous chapter:

command script import ~/lldb/BreakAfterRegex.py

If you’ve any doubts if this command loaded successfully into LLDB, simply fire up a new LLDB instance in Terminal:

lldb

Then check for the help docstring of the bar command:

(lldb) help bar

If you get an error, it’s not successfully loaded; but if you got the docstring, you’re golden.

The RWDevCon project

For this chapter, you’ll use an app called RWDevcon. It’s a live app, available in the App Store (https://itunes.apple.com/us/app/rwdevcon-the-tutorial-conference/id958625272).

This app is the companion app for the RWDevcon conference, https://www.rwdevcon.com/, where it’s an annual tradition to see how many times you can touch Ray Wenderlich’s shoulders before he gets annoyed. Try it! My personal best is 37!

For this project, I’ve forked from commit 84167c68 which can be found in the starter folder. However, you can get a more up-to-date version here: https://github.com/raywenderlich/RWDevCon-App.

Navigate to the starter folder then open, build, then run this application. Take a look around to get acquainted with the project.

There’s no need to explore any of the source code. With the aid of the bar command, you’ll be able to explore different items of interest with smart breakpoint queries.

But before we can do that, let’s talk about how to make this bar command much more powerful.

The optparse Python module

The lovely thing about LLDB Python scripts is you have all the power of Python — and its modules — at your disposal.

There are three notable modules that ship with Python 2.7 that are worth looking into when parsing options and arguments: getopt, optparse, and argparse.

getopt is kind of low level and optparse is on its way out since it’s been deprecated after Python 2.7. Unfortunately argparse is mostly designed to work with Python’s sys.argv — which is not available to your Python LLDB command scripts. This means optparse will be your go-to option. Facebook’s Chisel, Apple’s own custom LLDB scripts, and I all use this module. So, it’s kinda the de-facto standard for parsing arguments. ;]

The optparse module will let you define an instance of type OptionParser, a class responsible for parsing all your arguments. For this class to work, you need to declare what arguments and options your command supports. This makes sense because optional parameters may or may not take additional values for that particular option.

Take a brief look at an example. Consider the following:

some_command woot -b 34 -a "hello world"

The command is named some_command. But what are the arguments and options being passed into this command?

If you didn’t give any context to the parser, then this statement is ambiguous. The parser doesn’t know whether or not the -b or -a option should take in parameters for the option. For example, the parser could think this command is passed three arguments: ['woot', '34', 'hello world'], and two options -b, -a with no parameters. However, if the parser expected -b and -a to take parameters, the parser would give you the argument of ['woot'], '34' for the -b option and 'hello world' for -a.

Let’s dive into optparse some more, and see how we can use it to handle cases like this.

Adding options without params

With the knowledge you need to educate your parser with what arguments are expected, it’s time to add your first option which will alter the functionality of the bar command to apply the SBBreakpoint without using a regular expression, but instead use a normal expression.

This argument will be backed by a Python boolean value, so no parameters are needed for this option. The existence (or lack thereof) of this option is all the information you need to determine the boolean value. If the argument exists, then it’ll be True. Otherwise, False.

It’s worth noting some script authors will engineer an option that will encourage a boolean option which explicitly requires a parameter for the Boolean value and default to either True or False if the option is not supplied.

For example, the following command takes an option, -f with no parameters:

some_command -f

This would then turn into:

some_command -f1

That’s not really my style. But you might want to consider this design decision if you’re building scripts for a wider audience, since it gives the user more explicit intentions.

Ok, enough chit-chat. Let’s get to implementing this parser thing.

Open up BreakAfterRegex.py and add the following import statements at the top of the file:

import optparse
import shlex

The optparse is the module you just covered that contains the OptionParser class to parse any extra input given to your command.

The shlex module has a nice little Python function that conveniently splits up the arguments supplied to your command on your behalf while keeping string arguments intact.

For example, consider the following Python code:

import shlex
command = '"hello world" "2nd parameter" 34'
shlex.split(command)

This will produce the following output:

['hello world', '2nd parameter', '34']

This returns a Python list of parsed Python strs.

But before you go using this split method, you’ll need to create the parser itself. Head to the very bottom of BreakAfterRegex.py and create the following method:

def generateOptionParser():
  '''Gets the return register as a string for lldb
    based upon the hardware
  '''
  usage = "usage: %prog [options] breakpoint_query\n" +\
          "Use 'bar -h' for option desc"
  # 1
  parser = optparse.OptionParser(usage=usage, prog='bar') 
  # 2
  parser.add_option("-n", "--non_regex",
                    # 3 
                    action="store_true",
                    # 4
                    default=False,
                    # 5
                    dest="non_regex",
                    # 6
                    help="Use a non-regex breakpoint instead")
  # 7
  return parser

Let’s break this down, parameter by parameter:

  1. You’re creating the OptionParser instance and supplying it a usage param and a prog param. The usage will get displayed if you screw up and give the parser an argument it doesn’t know how to handle. The prog option is used to address the name of the program. I always incorporate it because it resolves a weird little issue which lets you run the -h or --help option to get all the supported options for a custom command. If the prog arg is not in there, the -h command will not work correctly. It’s one of life’s little mysteries. ¯\_(ツ)_/¯

  2. This line (followed by the next four lines of non-commented code) add the --non_regex or -n parameter to the parser.

  3. The action param informs what action should be done when this param is supplied. "store_true" informs the parser to store the Python Boolean True when this option is supplied.

  4. The default param informs that the initial value will be False. If this option is not given, this will be the value.

  5. The dest parameter will determine the name, non_regex, that you’re giving to the property when the OptionParser parses your input. For example, consider the following code which parses a Python string of options and arguments in command:

command_args = shlex.split(command)
(options, args) = parser.parse_args(command_args)
options.non_regex

As you’ll see shortly, the parse_args method produces a Python tuple containing a list of options (called options) and a list of arguments (called args). The options variable will now contain the non_regex property.

  1. help will give you help documentation. You can get all the parameters and their info with the --help option. For example, when this is correctly set up in the bar command, all you have to do is type bar -h to see a list of all the options and what they do.
  2. Once you’ve created the OptionParser and added the -n option, you’re returning the instance of the OptionParser.

You’ve just created a method that will generate this OptionParser instance you need to start parsing those arguments. Now it’s time to use this thing.

Jump back to the beginning of the breakAfterRegex function. Remove the following two lines:

target = debugger.GetSelectedTarget()
breakpoint = target.BreakpointCreateByRegex(command)

Then, in their place, add the following code:

'''Creates a regular expression breakpoint and adds it.
Once the breakpoint is hit, control will step out of the 
current function and print the return value. Useful for 
stopping on getter/accessor/initialization methods
'''

# 1
command = command.replace('\\', '\\\\')
# 2
command_args = shlex.split(command, posix=False)

# 3
parser = generateOptionParser()

# 4
try:
  # 5
  (options, args) = parser.parse_args(command_args)
except:
  result.SetError(parser.usage)
  return

target = debugger.GetSelectedTarget()

# 6
clean_command = shlex.split(args[0])[0]

# 7
if options.non_regex:
  breakpoint = target.BreakpointCreateByName(
                      clean_command)
else:
  breakpoint = target.BreakpointCreateByRegex(
                      clean_command)

# The rest remains unchanged

Make sure you have your indentation correct! This should be indented by two spaces, or whatever your single-tab width of choice is, as it’s all part of the function.

Here’s what that code does:

  1. When parsing your input to the OptionParser, it will interpret slashes as escaping characters. For example, "\'" is interpreted as just "'". This means you’ll need to escape any backslash characters in your commands.

  2. As you learned in a previous chapter, the command parameter passed into your custom LLDB scripts is a Python str, which contains all input that is passed into your argument. You’ll pass this variable into the shlex.split method to obtain a Python list of Python strs. In addition, there’s that posix=False which helps combat any input which contains special characters like a dash; otherwise, OptionParser will incorrectly assume that’s an option being passed in. This is important because Objective-C has dashes in instance methods, so you don’t want the dash to be incorrectly interpreted as an option!

  3. Using the newly created generateOptionParser function, you create a parser to handle the command’s input.

  4. Parsing input can be error-prone. Python’s usual approach to error handling is throwing exceptions. It’s no surprise that optparse throws if it finds an error. If you don’t catch exceptions in your scripts, LLDB will go down, which will also tank the process! Therefore, the parsing is contained in a try-except block to prevent LLDB from dying due to bad input.

  5. The OptionParser class has a parse_args method. You’re passing in your command_args variable to this method, and will receive a tuple in return. This tuple consists of two values: options, which consists of all option arguments (i.e. only the non_regex option right now). The other half of the tuple hands you all of the args which consists of any other input parsed by the parser.

  6. You’re taking the first captured argument (the breakpoint query) and assigning it to a variable called clean_command. Remember that posix=False mentioned in bullet 2? That logic will maintain the quotes around your captured argument which preserves your exact syntax. If you didn’t have that posix=False, you could just use args[0], but then you’d forfeit a lot of power in your regex by not being able to use the escape backslash character in your regex query.

  7. You’re putting your first option to use! You’re checking the truthiness of options.non_regex. If True, you’ll execute the BreakpointCreateByName method in SBTarget to implement a non-regular expression breakpoint. If the non_regex is False (by default it is when you supplied the default parameter inside the generateOptionParser function), then your script will use a regex search. Again, all you need to do is add the -n to your input for the bar command to make the non_regex True.

Testing out your first option

Enough code. Time to test this script out.

Instead of using that reload_script command you’ve used in the previous chapters, you’ll try an alternative tactic that you might appreciate to reload the script.

Jump to Xcode and create a new symbolic breakpoint.

Make sure the Breakpoint Navigator tab is selected, then hunt down that lonely + icon in the lower left corner. Then select Symbolic breakpoint…. Alternatively for you cool kids, ⌘ + Ctrl + \

In the Symbol section put getenv.

Add two actions. The first action adds the following command:

br dis 1

In the next action, add your bar command:

bar -n "-[NSUserDefaults(NSUserDefaults) objectForKey:]"

Finally select Automatically continue after evaluating actions.

When all is said and done, your symbolic breakpoint should look like this:

Can you figure out what you’ve just done? You’ve created a Symbolic breakpoint on the getenv C function. If I want to setup breakpoints before “my” code starts executing, or before reverse engineering an app, this is a good go-to to hook any logic for custom commands you want in LLDB.

I’m not a fan of using main, since a lot of executables contain the function main, and the primary executable’s main symbol might be stripped in a production build of an executable. We know that getenv will get hit for sure and will get hit before my code starts running.

What about those actions? The first action says to get rid of that getenv breakpoint. You’re not deleting it; you’re just disabling it. This is ideal since getenv gets called a fair bit and you need to get rid of this breakpoint once you’ve setup your LLDB logic. The use of 1 is mentioned because this breakpoint is the first breakpoint created for this session, which disables this symbolic breakpoint after it has run once.

After that, you’re creating a non regular expression breakpoint on NSUSerDefaults’s objectForKey: method. We expect this method to return an id or nil, so let’s see what this RWDevCon app is reading (or writing) to our NSUserDefaults.

Build and run the application.

If you haven’t taken a deep dive into the app, you’ll likely get a lot of nil values. This means that this method is definitely getting read by some code in this app.

Tap on any one of the workshops to bring up the detail view controller.

Before you continue, clear the LLDB window (⌘ + K).

From there, tap Add to my Schedule while keeping an eye on the console output.

You can see there’s an object that gets added to the NSUserDefaults that matches the When time.

Adding options with params

You’ve learned how to add an option that expects no arguments. You’ll now add another option that expects a parameter. This next option will be the --module option to specify which module you want to constrain your regular expression query to.

This is very similar to breakpoint set’s -s (aka --shlib option) option where it expects the name of the module immediately after the option. You explored this back in Chapter 4, “Stopping in Code.”

In the BreakAfterRegex.py script jump back down to the generateOptionParser function and add the following code right before return parser:

# 1
parser.add_option("-m", "--module",
                  # 2
                  action="store",
                  # 3
                  default=None,
                  # 4
                  dest="module",
                  help="Filter a breakpoint by only searching within a specified Module")
  1. You’re adding a new option -m or --module to the OptionParser instance.

  2. In the previous option, the action was "store_true"; this time it is "store". This means this option expects a parameter.

  3. This parameter’s default value is None.

  4. The name of this property will be module.

Jump back to the breakAfterRegex function and scan for the following lines:

if options.non_regex:
  breakpoint = target.BreakpointCreateByName(clean_command)
else:
  breakpoint = target.BreakpointCreateByRegex(clean_command)

Add options.module as the second parameter to both of these functions.

if options.non_regex:
  breakpoint = target.BreakpointCreateByName(clean_command, options.module)
else:
  breakpoint = target.BreakpointCreateByRegex(clean_command, options.module)

So how does this work? Let’s print out the method signature right now for BreakpointCreateByRegex. Type the following in LLDB:

(lldb) script help (lldb.SBTarget.BreakpointCreateByRegex)

This will dump the small amount of documentation for this function. Although there is no help documentation for this method, it does give you a list of its method signatures.

The following signature is worth discussing:

BreakpointCreateByRegex(SBTarget self, str symbol_name_regex, str module_name=None) -> SBBreakpoint

Take note of the final parameter: module_name=None. The fact it’s an optional parameter means if you don’t supply a parameter, the module_name will take the value as None. This means when the OptionParser instance parses the options, you can supply options.module into the BreakpointCreateByRegex method regardless, since the default value of options.module will be None, which is the same as not applying an extra argument.

Time to test this out. Save your work in your script. Jump over to Xcode and modify that getenv Symbolic breakpoint. Replace the second action with the following line of code:

bar @objc.*.init -m RWDevCon

Make sure that 'C' in 'Con' is capitalized!

This will create a regex breakpoint on all Objective-C objects that are subclassed by a Swift object and stick a breakpoint on their initializer. You are filtering this breakpoint query to only search for breakpoints inside the RWDevCon module.

Run the application and check out all the Objective-C objects that are subclassed by Swift objects.

Take a quick look at the output. You’ll get a lot of __ObjC.NSEntityDescription hits. That must mean there’s some CoreData logic that’s written in Swift, right?

Right!

Clear the screen (you should know that shortcut by now) and tap on a table cell that contains a workshop (i.e. no lunch or party dates) and see what pops up on the detail view controller.

You’ll get a list of all the Objective-C objects that are subclassed by Swift. Search for the class named Person.

Copy the address into your clipboard.

Before you paste in your address, let’s dump all the methods implemented by this Person class. Since it’s an Objective-C subclass, it’s fair game to all those introspection commands you’ve made earlier.

In LLDB type the following:

(lldb) methods Person

This will dump all the methods the Person class implements that the Objective-C runtime knows about. Note that I said Objective-C runtime. There still could be Swift methods that this class implements that the Objective-C runtime doesn’t know about even if the class inerhits from NSObject!

You can of course execute any of these methods on this valid Person instance.

Let’s up the ante. You’ll now create an option in the bar command that will allow you to add a condition, evaluated after the function the breakpoint is in finishes executing. If true, execution will stop; if false, execution will keep on going.

You’ll apply this condition to fullName and only stop when you hit the name “Ray Wenderlich”. Sneaky!

Passing parameters into the breakpoint callback function

Time to create the parser option for -c, or --condition!

Jump back to BreakAfterRegex.py and find generateOptionParser. Add the following line of code right before the return parser line of code:

parser.add_option("-c", "--condition",
                  action="store",
                  default=None,
                  dest="condition",
                  help="Only stop if the expression matches True. Can reference return value through 'obj'. Obj-C only.")

You should know what this is doing now, but here’s a quick recap. You’re creating the --condition option which defaults to None and expects a parameter. The help text has something interesting in there. You’re indicating you can reference the return value through the variable name obj. This means when you’re evaluating code, you’ll take the return register and assign obj to it.

Time to use this new option. But hold on… Think about this for a second. How are you going to pass the option parameters into the SBBreakpoint callback function?

Remember, this callback function is being called by a “private” C++ API and is limited to a specific method signature. Consider the following declaration where you set the breakpoint handler:

breakpoint.SetScriptCallbackFunction("BreakAfterRegex.breakpointHandler")

When the SBBreakpoint callback hits, this function will get called:

def breakpointHandler(frame, bp_loc, dict):
  # method contents here

You only have the SBFrame, SBBreakpointLocation, and an internal Python dict to work with to pass around information. How can this function read the parameters which are parsed by your OptionParser instance and be given into another function? This function signature is locked-in to only supply these parameters.

Several ideas come to mind to get around this problem. You can search for alternatives in SBBreakpoint or similar classes to see if there’s an API that lets you pass in other params.

Alternatively, you can try and subclass a SBBreakpoint to add additional functionality to pass around the condition option parameter, or you can try using a global variable to pass around the parsed options. If you’re really desperate, you can try and dynamically creating a method at runtime using the exec Python function.

Unfortunately, SBBreakpoint has no APIs to handle working with classes and callbacks, global variables are a bad idea in general and you could also run into threading problems for stale logic if multiple breakpoint callbacks are referencing a global set of options.

Subclassing won’t work, since this Python LLDB class is dynamically generated behind the scenes by C++ code, and you’ll get a new instance each time when trying to access the passed around SBBreakpoint. Besides, 99% of the time, using exec is just a bad, bad idea.

What’s a developer to do?

This means you’ll have to default to using global variables and deal with the global variable state. Consider the following situation. You assign the options to a global variable and create SBBreakpoint 1. You do the exact same thing for SBBreakpoint 2.

However, SBBreakpoint 1 gets triggered and the callback function is called, which references the global options. Since SBBreakpoint 2 was created, it has since modified these options to the incorrect expectation.

Fortunately, there’s a slightly better alternative to using global variables, and you’ll come up with a sneaky solution to resolve the global state of the options.

Instead of a global variable, you’ll create a Python class, which will have a class property to hold the options being passed around.

Now to address that global state: instead of a property to hold the options, you’ll use a Python dict to hold the options.

The nice thing about breakpoints is regardless of how many you create or delete, each breakpoint will have a unique ID per run session. This means you can use the breakpoint’s ID as a unique key to reference a particular set of options for each breakpoint.

You can then set the breakpoints ID as the key and the options for that breakpoint as the value. Cool, right?

Jump to the top of BreakAfterRegex.py and add the following logic right underneath the import statements:

# 1
class BarOptions(object):

  # 2
  optdict = {}

  # 3
  @staticmethod
  def addOptions(options, breakpoint):
    key = str(breakpoint.GetID())
    BarOptions.optdict[key] = options

Going over this step-by-step:

  1. You’re declaring a class named BarOptions which inherits from type object. Think of object as Python’s equivalent for NSObject. This class provides base functionality and generally makes your life a little easier. It’s absolutely possible to not have a base class (just like in Swift), but some Python APIs play a little nicer when inheriting from object.
  2. You’re declaring a class variable named optdict. If you were to declare an instance variable, it would have to be inside an init function. Since you’re only working with this class variable, you won’t be setting up any initialization methods for this class.
  3. You’re also declaring a class method called addOptions (think +[ in Objective-C or class func in Swift), which uniquely assigns the options that are bound to the SBBreakpoint’s ID.

Jump down to breakAfterRegex and add the following line of code right before the point where you specify the callback function (i.e. the call to SetScriptCallbackFunction):

BarOptions.addOptions(options, breakpoint)

After you’ve added this new line of code, create a new function to evaluate the condition. Add the new function evaluateCondition to the bottom of BreakAfterRegex.py:

def evaluateCondition(debugger, condition):
  '''Returns True or False based upon the supplied condition.
  You can reference the NSObject through "obj"'''

  # 1
  res = lldb.SBCommandReturnObject()
  interpreter = debugger.GetCommandInterpreter()
  target = debugger.GetSelectedTarget()

  # 2
  expression = 'expression -lobjc -O -- id obj = ((id){}); ((BOOL){})'.format(getRegisterString(target), condition)
  interpreter.HandleCommand(expression, res)

  # 3
  if res.GetError():
    print(condition)
    print('*' * 80 + '\n' + res.GetError() + '\ncondition:' + condition)
    return False
  elif res.HasResult():
    # 4
    retval = res.GetOutput()

    # 5
    if 'YES' in retval:
      return True

  # 6
  return False

Breaking that down:

  1. You’re creating a SBCommandReturnObject to handle the code being passed in from the condition parameter.
  2. This will create and execute the custom expression that’s being passed in. Notice you’re declaring the instance variable obj and casting it to type id from the return register. This lets you conveniently reference the return value as obj instead of a hardware-specific register. The expression you provide will be cast into an Objective-C BOOL, which will either return a YES or NO output.
  3. You’ll evaluate the return value, and if it contains an error, print the error out. You’re explicitly returning False or True within this function because you’ll use this return value to determine if execution should stop or not when evaluating this expression. Remember, the SBBreakpoint callback function breakpointHandler will stop execution if the function returns True. Execution will not stop if not True (i.e. False, None or no return) is returned.
  4. This will assign the output to a variable named retval if there is one to grab.
  5. It really pains me to teach expression parsing this way, since there’s a much cleaner method of evaluating objects using SBValues, which you’ll learn about in the next chapter. For now, you’ll continue using the SBCommandReturnObject and compare the output to what you expect. If the expression is evaluated to YES, then pause execution.
  6. If the execution returns NO, then just keep on executing by returning False.

Final round of code! Find breakpointHandler function. Add the following code beneath the thread.StepOut() call:

# 1
key = str(bp_loc.GetBreakpoint().GetID())
# 2
options = BarOptions.optdict[key]
# 3
if options.condition:
  # 4
  condition = shlex.split(options.condition)[0]
  # 5
  return evaluateCondition(debugger, condition)

Last explanation. Yay!

  1. The bp_loc is of type SBBreakpointLocation. This class lets you reference the initial SBBreakpoint by the GetBreakpoint method. From there, you can reference the ID, which will be a number. Therefore, you need to cast this number as a Python str and assign that to the variable key.

  2. This will grab the options from the class property optdict and assign it to the variable options.

  3. Check if the options variable contains a non-None reference. If there’s a valid reference, execute the logic.

  4. This will unwrap the condition passed into the command line option. Again, you have to do a little extra work thanks to that posix=False mentioned earlier, but it allows you to use backslash and dash characters in our options & arguments.

  5. Finally, you’re calling the function evaluateCondition you created in the previous code snippet. You are returning the function’s return value which will influence if execution should stop or not.

No more Python code (well, for this chapter…muwahahaha)! Save your work and head back to Xcode.

Again, modify the second action in the getenv symbolic breakpoint. This time, change it to the following:

bar NSURL\(.*init

This will breakpoint will now fire on the initialization of NSURLs. That weird syntax is necessary because the majority of NSURL initialization methods are created through categories.

Scan for any HTTPS NSURLs in the console output.

Looks like the app is hitting some Amazon S3 webservice. Use the newly created --condition option of the bar command you’ve just created to stop when an NSURL returns from initialization and contains "amazon" in the absoluteString.

Go back after the getenv symbolic breakpoint and change the second action yet again to the following:

bar NSURL\(.*init -c '(BOOL)[[obj absoluteString] containsString:@"amazon"]'

Build and run and see what happens…

Execution will stop on the exact line containing this NSURL … er… URL, since it stopped in the Swift context. But let’s be real, that instance is a NSURL.

There are many creative situations the bar command can be utilized. Try to come up with some on your own!

Where to go from here?

That was pretty intense, but you’ve learned how to incorporate options into your own Python scripts.

In the very unlikely chance you still have energy after reading this chapter, you should implement some sort of backtrace option for the bar command. There are many times, when debugging, where I wish I’d known the stack trace of an interesting object!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.