Search code examples
pythoncommand-line-interfacecommand-line-argumentsargparse

Best architecture for a Python command-line tool with multiple subcommands


I am developing a command-line toolset for a project. The final tool shall support many subcommands, like so

foo command1 [--option1 [value]?]*

So there can be subcommands like

foo create --option1 value --

foo make file1 --option2 --option3

The tool uses the argparse library for handling command-line arguments and help functionality etc.

A few additional requirements and constraints:

  • Some options and functionality is identical for all subcommands (e.g. parsing a YAML configuration file etc.)

  • Some subcommands are quick and simple to code, because they e.g. just call an external bash script.

  • Some subcommands will be complex and hence long code.

  • Help for the basic tool should be available as well as for an individual subcommand:

    foo help Available commands are: make, create, add, xyz

    foo help make Details for the make subcommand

  • error codes should be uniform across the subcommands (like the same error code for "file not found")

For debugging purposes and for making progress with self-contained functionality for minimal viable versions, I would like to develop some subcommands as self-containted scripts and modules, like

make.py

that can be imported into the main foo.py script and later on invoked as both

make.py --option1 value etc.

and

foo.py make --option1 value

Now, my problem is: What is the best way to modularize such a complex CLI tool with minimal redundancy (e.g. the arguments definition and parsing should only be coded in one component)?

Option 1: Put everything into one big script, but that will become difficult to manage.

Option 2: Develop the functionality for a subcommand in individual modules / files (like make.py, add.py); but such must remain invocable (via if __name__ == '__main__' ...).

The functions from the subcommand modules could then be imported into the main script, and the parser and arguments from the subcommand added as a subparser.

Option 3: The main script could simply reformat the call to a subcommand to subprocess, like so

subprocess.run('./make.py {arguments}', shell=True, check=True, text=True)

Solution

  • Thanks for all of your suggestions!

    I think the most elegant approach is using Typer and following this recipe:

    https://typer.tiangolo.com/tutorial/subcommands/add-typer/