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
-nor--non_regexoption will result in thebarcommand using a non-regular expression breakpoint search instead. This option will not take any additional parameters. -
Filter by module: Using the
-mor--moduleoption 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
-cor--conditionoption, thebarcommand will evaluate the given condition after stepping out of the current function. IfTrue, execution will stop. IfFalse, 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-CBOOL.
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:
-
You’re creating the
OptionParserinstance and supplying it a usage param and a prog param. Theusagewill get displayed if you screw up and give the parser an argument it doesn’t know how to handle. Theprogoption 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-hor--helpoption to get all the supported options for a custom command. If theprogarg is not in there, the-hcommand will not work correctly. It’s one of life’s little mysteries.¯\_(ツ)_/¯ -
This line (followed by the next four lines of non-commented code) add the
--non_regexor-nparameter to theparser. -
The
actionparam informs what action should be done when this param is supplied."store_true"informs the parser to store the Python BooleanTruewhen this option is supplied. -
The
defaultparam informs that the initial value will beFalse. If this option is not given, this will be the value. -
The
destparameter will determine the name, non_regex, that you’re giving to the property when theOptionParserparses your input. For example, consider the following code which parses a Python string of options and arguments incommand:
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.
-
helpwill give you help documentation. You can get all the parameters and their info with the--helpoption. For example, when this is correctly set up in thebarcommand, all you have to do is typebar -hto see a list of all the options and what they do. - Once you’ve created the
OptionParserand added the-noption, you’re returning the instance of theOptionParser.
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:
-
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. -
As you learned in a previous chapter, the
commandparameter passed into your custom LLDB scripts is a Pythonstr, which contains all input that is passed into your argument. You’ll pass this variable into theshlex.splitmethod to obtain a Pythonlistof Pythonstrs. In addition, there’s thatposix=Falsewhich helps combat any input which contains special characters like a dash; otherwise,OptionParserwill 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! -
Using the newly created
generateOptionParserfunction, you create a parser to handle the command’s input. -
Parsing input can be error-prone. Python’s usual approach to error handling is throwing exceptions. It’s no surprise that
optparsethrows 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. -
The
OptionParserclass has aparse_argsmethod. You’re passing in yourcommand_argsvariable 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 thenon_regexoption right now). The other half of the tuple hands you all of theargswhich consists of any other input parsed by the parser. -
You’re taking the first captured argument (the breakpoint query) and assigning it to a variable called
clean_command. Remember thatposix=Falsementioned in bullet 2? That logic will maintain the quotes around your captured argument which preserves your exact syntax. If you didn’t have thatposix=False, you could just useargs[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. -
You’re putting your first option to use! You’re checking the truthiness of
options.non_regex. IfTrue, you’ll execute theBreakpointCreateByNamemethod inSBTargetto implement a non-regular expression breakpoint. If thenon_regexisFalse(by default it is when you supplied the default parameter inside thegenerateOptionParserfunction), then your script will use a regex search. Again, all you need to do is add the-nto your input for thebarcommand to make thenon_regexTrue.
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")
-
You’re adding a new option
-mor--moduleto the OptionParser instance. -
In the previous option, the
actionwas"store_true"; this time it is"store". This means this option expects a parameter. -
This parameter’s default value is
None. -
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:
- You’re declaring a class named
BarOptionswhich inherits from typeobject. Think ofobjectas Python’s equivalent forNSObject. 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 fromobject. - 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. - You’re also declaring a class method called addOptions (think
+[in Objective-C orclass funcin Swift), which uniquely assigns the options that are bound to theSBBreakpoint’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:
- You’re creating a
SBCommandReturnObjectto handle the code being passed in from theconditionparameter. - This will create and execute the custom expression that’s being passed in. Notice you’re declaring the instance variable
objand casting it to typeidfrom the return register. This lets you conveniently reference the return value asobjinstead of a hardware-specific register. The expression you provide will be cast into an Objective-CBOOL, which will either return aYESorNOoutput. - You’ll evaluate the return value, and if it contains an error, print the error out. You’re explicitly returning
FalseorTruewithin this function because you’ll use this return value to determine if execution should stop or not when evaluating this expression. Remember, theSBBreakpointcallback functionbreakpointHandlerwill stop execution if the function returnsTrue. Execution will not stop if notTrue(i.e.False,Noneor no return) is returned. - This will assign the output to a variable named
retvalif there is one to grab. - 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 theSBCommandReturnObjectand compare the output to what you expect. If the expression is evaluated toYES, then pause execution. - If the execution returns
NO, then just keep on executing by returningFalse.
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!
-
The
bp_locis of typeSBBreakpointLocation. This class lets you reference the initialSBBreakpointby theGetBreakpointmethod. From there, you can reference the ID, which will be a number. Therefore, you need to cast this number as a Pythonstrand assign that to the variablekey. -
This will grab the options from the class property
optdictand assign it to the variableoptions. -
Check if the
optionsvariable contains a non-Nonereference. If there’s a valid reference, execute the logic. -
This will unwrap the
conditionpassed into the command line option. Again, you have to do a little extra work thanks to thatposix=Falsementioned earlier, but it allows you to use backslash and dash characters in our options & arguments. -
Finally, you’re calling the function
evaluateConditionyou 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!