Reputation: 85
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
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:
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:
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.python docker_tool.py create foo foo:latest
will produce the Namespace
object Namespace(action='create', name='foo', image='foo:latest')
, as before.python docker_tool.py foo
will now produce the Namespace
object
Namespace(action='run', name='foo')
.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