Many moderne command-line programs comes with a subcommand style of organization: Git has git status, git commit, git checkout, and each subcommand has its own arguments, options, and documentation.
This article presents a Python skeleton for making well-organized and easily extensible subcommand-style, command-line programs. It uses the standard module argparse.
General code organization
We start with a regular organisation of a python project:
myproject/ ├── mypackage/ └── setup.py
Where the code actual code is in directory mypackage and a classic setup.py script like this:
from setuptools import setup, find_packages
setup(
## Usual info.
packages = ["mypackage"],
)
We place command-line code in a separate directory called cmdline and we make it a package:
myproject/
├── mypackage/
├── setup.py
└── cmdline
├── __init__.py
└── __main__.py
The content of __init__.py and __main__.py is section The magic trick scripts below.
Each subcommand will be a python file placed in the cmdline directory, which will be automatically discovered and added by the __init__.py script. Each python file must implement these two functions:
def add_argument(parser):
## Add command-line arguments to the parser argument, which is an
## instance of argparse.ArgumentParser
pass
def execute(args, parser):
"oneline description of this subcommand"
## Command-line code goes here.
## Parsed arguments are args and the parser object is also provided if
## needed. This function should ideally return an integer that is the
## process return status.
return 0
For a subcommand within a subcommand, just make a directory instead of a python file, and copy (or symlink) the __init__.py file into that directory.
In this file, you will add import mypackage and use is as an external package.
The main command
Now modify setup.py like this:
from setuptools import setup, find_packages
setup(
## Usual info.
packages = ["mypackage"],
entry_points = {
"console_scripts": [
"myprog = cmdline.__main__:main",
],
},
)
Running something like python setup.py develop --user will automatically create a script called myprog in $HOME/.local/bin, which should be in your $PATH, and you’re on !
You can easily add, rename, and delete subcommands with file operations, and command-line code is also cleanly separated from the core code.
The magic trick scripts
The file __init__.py has the following content:
## pylint: disable=missing-module-docstring,missing-function-docstring
## Automatically import modules of this package as sub-commands.
##
## Modules should have functions "add_arguments" and "execute". Docstring of
## the latter is command-line help.
##
## Please, do not mix this package with modules that are not intended to be
## sub-commands; do not put other files either.
import importlib
import os
import sys
def _find_modules(directory):
cmds = dict()
for dir_entry in os.scandir(directory):
if dir_entry.name in ["__init__.py", "__main__.py", "__pycache__"]:
continue
name, _ = os.path.splitext(dir_entry.name)
modname = __name__ + "." + name
module = importlib.import_module(modname)
if (hasattr(module, "add_arguments") and
hasattr(module, "execute")):
cmds[name] = module
else:
print("Incompatible module:", modname, file=sys.stderr)
return cmds
CMDS = _find_modules(__path__[0])
CMD_NAME = __name__
def add_arguments(parser):
subparsers = parser.add_subparsers(dest=CMD_NAME)
for name, module in CMDS.items():
doc = module.execute.__doc__
subparser = subparsers.add_parser(name, help=doc)
module.add_arguments(subparser)
def execute(args, parser):
name = getattr(args, CMD_NAME)
try:
module = CMDS[name]
except KeyError:
parser.print_help(file=sys.stderr)
return 1
return module.execute(args, parser)
And the __main__.py has the content:
import argparse
import sys
from . import add_arguments, execute
def main():
parser = argparse.ArgumentParser()
add_arguments(parser)
args = parser.parse_args()
return execute(args, parser)