Figuring out Python Subcommands with ArgParse
1. Overview
While watching The Taggart Institute's mttaggart stream on programming a KASM manager with python, he ran into a question on how subcommands with argparse work. This has haunted me before, and if answered would certainly help me with other projects. Taggart eventually used click, a package for "beautiful command line interfaces", to handle his subcommand and argument parsing needs.
Currently I have a python project I'd like to use subcommands of either "load" or "unload". With which each would have different arguments associated with the subcommand. This example, rant, and hopeful proof-of-concept will help in utilizing subcommands using argparse. Specifically, this is for adversary emulation within a devops environment that is confirmed to have the python docker sdk but no other non-standard module or library assumptions.
2. The Setup
This is just utilizing the argparse module, to create the program's global variable values. The global program arguments act as regular arguments in argparse, can have default values, actions, help menu context, and typing. In my case below, I need to pass some information about the docker registry, repository authentication, and dockerclient regardless if I'm utilizing the load or unload subcommands.
from argparse import ArgumentParser parser = ArgumentParser() parser.add_argument('--registry', help="The location of the registry to login", default="docker.io") parser.add_argument('--repository', help="Where to push your image") parser.add_argument('--username') parser.add_argument('--password') parser.add_argument('--DockerClient', help="The docker socket to use that you have permissiosn for", default="unix:///var/run/docker.sock/")
3. Subparsers, subcommands, and their subarguments
Next we want to start the subparsers pointing to the subcommand
, note that this is what python will use when describing if you print the object. I also use the title
parameter so that it shows up in --help
. We do this with the parser
object from above and running the add_subparsers
method on it. Once the add_subparsers
method has ran, we need to define the new parsers that will be the "subcommands", in this case using the add_parser
method on the returned add_subparsers
object stored in subcommand_parser
.
subcommand_parser = parser.add_subparsers(dest='subcommand', title="subcommand", help="additonal subcommand help here") load_parser = subcommand_parser.add_parser('load') unload_parser = subcommand_parser.add_parser('unload')
After these subcommand parsers are defined, we can continue by adding arguments to the subparsers, like we did with the global arguments.
Next is adding arguments specific to our subcommands with the add_argument
method of the specific subcommand. We can see that unload doesn't have any additional arguments and load has two additional arguments.
# Load First load_parser.add_argument('--dataLoc', help="Provide the file location of the data to load into the container") load_parser.add_argument('--dockerFile', help="Specify a specific dockerfile location. Warning! this assumes you load the data into the container manually with the dockerfile") # Unload doesn't have additonal arguments, just executes different functions
4. Conclusion
- I'm definitely more comfortable with subcommands now and pretty much finished my initial use case in this post.
- If you wanted to add further subcommands, it would be as simple as adding another layer of
add_subparsers
to thesubcommand_parser.add_parser
objects (e.g.load-subcommand_parse = load.parser.add_subparsers('subcommand for load'
). - Also found this stackoverflow post from > 10 years ago with python2.x that helped piece this together with the documentation.
4.1. Results from parsing arguments
print("The default --help results") print(parser.parse_args(['--help'])) print("\n\n\n---\n\n\n") print("The default load --help results") print(parser.parse_args(['load', '--help']))
The default --help results usage: [-h] [--global-thing] [--global-value GLOBAL_VALUE] [--registry REGISTRY] [--repository REPOSITORY] [--username USERNAME] [--password PASSWORD] [--DockerClient DOCKERCLIENT] {load,unload} ... options: -h, --help show this help message and exit --global-thing --global-value GLOBAL_VALUE --registry REGISTRY The location of the registry to login --repository REPOSITORY Where to push your image --username USERNAME --password PASSWORD --DockerClient DOCKERCLIENT The docker socket to use that you have permissiosn for subcommand: {load,unload} additonal subcommand help here --- The default load --help results usage: load [-h] [--dataLoc DATALOC] [--dockerFile DOCKERFILE] options: -h, --help show this help message and exit --dataLoc DATALOC Provide the file location of the data to load into the container --dockerFile DOCKERFILE Specify a specific dockerfile location. Warning! this assumes you load the data into the container manually with the dockerfile
4.2. Tangled code
from argparse import ArgumentParser parser = ArgumentParser() parser.add_argument('--global-thing', action = 'store_true') parser.add_argument('--global-value', default = 42) parser.add_argument('--registry', help="The location of the registry to login", default="docker.io") parser.add_argument('--repository', help="Where to push your image") parser.add_argument('--username') parser.add_argument('--password') parser.add_argument('--DockerClient', help="The docker socket to use that you have permissiosn for", default="unix:///var/run/docker.sock/") subcommand_parser = parser.add_subparsers(dest='subcommand', title="subcommand", help="additonal subcommand help here") load_parser = subcommand_parser.add_parser('load') unload_parser = subcommand_parser.add_parser('unload') # Load First load_parser.add_argument('--dataLoc', help="Provide the file location of the data to load into the container") load_parser.add_argument('--dockerFile', help="Specify a specific dockerfile location. Warning! this assumes you load the data into the container manually with the dockerfile") # Unload doesn't have additonal arguments, just executes different functions print("The default --help results") print(parser.parse_args(['--help'])) print("The default load --help results") print(parser.parse_args(['load', '--help']))