Izaan
Izaan

Reputation: 85

How to make argparse accept either subcommands or a direct argument?

I have a CLI tool for managing Docker containers. Here's my current parser setup:

def create_parser():
    parser = argparse.ArgumentParser(description="Docker container manager")
    subparsers = parser.add_subparsers(dest="action", help="Actions")

    # Create container
    create_parser = subparsers.add_parser("create", help="Create a new container")
    create_parser.add_argument("name", help="Container name")
    create_parser.add_argument("image", help="Docker image")

    # Stop container
    stop_parser = subparsers.add_parser("stop", help="Stop a container")
    stop_parser.add_argument("name", help="Container name")

    # Remove container
    remove_parser = subparsers.add_parser("remove", help="Remove a container")
    remove_parser.add_argument("name", help="Container name")

    return parser

This works fine for commands like:

python docker_tool.py create mycontainer nginx:latest
python docker_tool.py stop mycontainer
python docker_tool.py remove mycontainer

But I'd also like to allow users to directly start a container by just providing the container name:

python docker_tool.py mycontainer  # Should start the container

How can I modify my parser to accept either a subcommand (create/stop/remove) OR just a container name as the first argument? I want to keep the existing subcommand functionality while adding this shorthand for starting containers.

Upvotes: 1

Views: 91

Answers (1)

simon
simon

Reputation: 5441

I don't think there is a simple solution, but I guess there is a "hacky" one (to which the caveats that have already been mentioned in the comments to your question still apply, of course): Similar to my earlier comment, I would try to handle the case of only one given argument outside the parser itself. However, in the "pythonic spirit" that it is easier to ask for forgiveness than permission, I would:

  1. Try to parse the given arguments with the parser.
  2. Only if this fails, try to handle the single-argument case.
  3. If the latter fails as well, exit.

This can be achieved with the following setup:

import argparse
import sys

def create_parser():  # Original create_parser() function, minus space+comments
    parser = argparse.ArgumentParser(description="Docker container manager")
    subparsers = parser.add_subparsers(dest="action", help="Actions")
    create_parser = subparsers.add_parser("create", help="Create a new container")
    create_parser.add_argument("name", help="Container name")
    create_parser.add_argument("image", help="Docker image")
    stop_parser = subparsers.add_parser("stop", help="Stop a container")
    stop_parser.add_argument("name", help="Container name")
    remove_parser = subparsers.add_parser("remove", help="Remove a container")
    remove_parser.add_argument("name", help="Container name")
    return parser

def adjust_and_parse(parser, args=None, namespace=None) -> argparse.Namespace():
    parser.exit_on_error = False  # Enable us to enter the except block
    parser.epilog = "Only providing a container name will run the given container."
    try:
        return parser.parse_args(args=args, namespace=namespace)
    except argparse.ArgumentError as ae:
        # If 1 argument is given, create namespace and return it, otherwise fail
        if len(args := sys.argv[1:] if args is None else args) == 1:
            namespace = argparse.Namespace() if namespace is None else namespace
            namespace.action = "run"
            namespace.name = args[0]
            return namespace
        parser.error(ae)  # Manually "exit on error"
        
if __name__ == "__main__":
    ns = adjust_and_parse(create_parser())
    print(ns)

Here, the args and namespace arguments of adjust_and_parse() are only present to mimic the capability of parse_args() to receive these arguments, as well. They can be left out if not needed.

This will result in the following exemplary behavior:

  • Calling python docker_tool.py --help will produce the output
    usage: docker_tool.py [-h] {create,stop,remove} ...
    
    Docker container manager
    
    positional arguments:
      {create,stop,remove}  Actions
        create              Create a new container
        stop                Stop a container
        remove              Remove a container
    
    options:
      -h, --help            show this help message and exit
    
    Only providing a container name will run the given container.
    
    Notice the additional epilog that describes the new behavior.
  • Calling python docker_tool.py create foo foo:latest will produce the Namespace object Namespace(action='create', name='foo', image='foo:latest'), as before.
  • Calling python docker_tool.py foo will now produce the Namespace object Namespace(action='run', name='foo').
  • Calling python docker_tool.py foo foo:latest will exit as before, with
    usage: docker_tool.py [-h] {create,stop,remove} ...
    docker_tool.py: error: argument action: invalid choice: 'foo' (choose from 'create', 'stop', 'remove')
    An exception has occurred, use %tb to see the full traceback.
    

Upvotes: 0

Related Questions