Skip to Content
Course content

350: Building Subcommands and Shell Completion

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've been building CLI tools, you've probably hit a wall where a single set of flags isn't enough. You want your tool to feel like git or docker—where you have distinct actions like git push or docker run. In Python, we usually reach for argparse subparsers to do this, but there's a pattern I see almost every junior dev fall into that makes the code a nightmare to maintain.

The If-Else Subcommand Trap

Here is a snippet of a tool I recently reviewed. It's a basic cloud management script. It "works," but look at how the logic is handled:

import argparse

def main():
    parser = argparse.ArgumentParser(prog="cloud-tool")
    subparsers = parser.add_subparsers(dest="command")

    # Deploy command
    deploy_parser = subparsers.add_parser("deploy")
    deploy_parser.add_argument("--env", required=True)

    # Status command
    status_parser = subparsers.add_parser("status")
    status_parser.add_argument("--service")

    args = parser.parse_args()

    # The "Trap"
    if args.command == "deploy":
        print(f"Deploying to {args.env}...")
        # ... 50 lines of deployment logic ...
    elif args.command == "status":
        print(f"Checking status of {args.service}...")
        # ... 50 lines of status logic ...
    else:
        parser.print_help()

if __name__ == "__main__":
    main()

The problem here isn't that it doesn't run; it's that the main() function is destined to become a thousand-line monster. Every time you add a new subcommand, your if/elif chain grows. You're mixing the parsing of the arguments with the execution of the logic. It's brittle and hard to test.

Mapping Subcommands to Functions

The professional way to handle this in argparse is using set_defaults. Instead of checking the command name with a string, we tell the parser exactly which function should handle that specific subcommand. This decouples the CLI definition from the business logic.

import argparse

def handle_deploy(args):
    print(f"Deploying to {args.env}...")

def handle_status(args):
    print(f"Checking status of {args.service}...")

def main():
    parser = argparse.ArgumentParser(prog="cloud-tool")
    subparsers = parser.add_subparsers(dest="command", required=True)

    deploy_parser = subparsers.add_parser("deploy")
    deploy_parser.add_argument("--env", required=True)
    deploy_parser.set_defaults(func=handle_deploy) # This is the secret sauce

    status_parser = subparsers.add_parser("status")
    status_parser.add_argument("--service")
    status_parser.set_defaults(func=handle_status)

    args = parser.parse_args()
    
    # No more if/elif chains. Just call the function mapped to the command.
    args.func(args)

if __name__ == "__main__":
    main()

Now, main() doesn't care if you have two commands or two hundred. It just finds the function associated with the user's choice and executes it. I highly recommend moving those handle_ functions into their own modules as your project grows.

Making the Shell Actually Help You

Now, let's talk about the "polish." Have you ever used a CLI tool and felt annoyed that you had to keep checking --help because Tab-completion didn't work? If you're distributing a tool for other engineers, shell completion isn't a "nice-to-have"—it's a requirement.

Python's argparse doesn't do this natively because shell completion is a feature of the shell (Bash, Zsh, Fish), not the language. However, the argcomplete library is the industry standard for bridging this gap. It allows your Python script to "tell" the shell what the valid completions are in real-time.

First, you'll need pip install argcomplete. Then, you add one single line to the very top of your script:

import argparse
import argcomplete # Import it here

def handle_deploy(args):
    # ... logic ...
    pass

def main():
    parser = argparse.ArgumentParser(prog="cloud-tool")
    # ... add subparsers ...
    
    # This line enables the magic
    argcomplete.autocomplete(parser) 
    
    args = parser.parse_args()
    args.func(args)

The Registration Gotcha

Here is where most people get stuck: they add the code, hit Tab, and... nothing happens. That's because the shell needs to be told to look for argcomplete's hooks. You can't just change the Python code; you have to register the script with your shell.

For a global installation, you can run activate-global-python-argcomplete. But for a specific project, the most reliable way is to run this in your terminal (for Bash):

eval "$(register-python-argcomplete cloud-tool)"

I usually put this in a .bashrc or a setup script. Once registered, your shell will actually execute a "hidden" pass of your Python script whenever the user hits Tab to determine which subcommands and flags are valid based on the current state of the parser.




📋 Practical Task

Build a Task Management CLI with Subcommands and Completion

Your goal is to build a CLI tool called taskmaster that manages a simple todo list. You need to implement the following requirements using the set_defaults(func=...) pattern and argcomplete integration.

  • Subcommand: add
    • Must take a positional argument task_name.
    • Must take an optional flag --priority (defaulting to "medium").
  • Subcommand: list
    • Must take an optional flag --completed (a boolean/store_true) to filter the list.
  • Subcommand: remove
    • Must take a positional argument task_id (integer).

Technical Constraints:

  1. Do NOT use a chain of if/elif statements to determine which command to run. Use the function mapping pattern.
  2. Integrate argcomplete so that the subcommands (add, list, remove) can be tab-completed.
  3. The "business logic" functions don't need to actually save to a database; simply printing "Adding task [name] with priority [priority]" is sufficient for this exercise.

Verification: To test the completion, remember to run eval "$(register-python-argcomplete taskmaster)" in your terminal before attempting to use Tab-completion.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.