Simple subcommand CLIs with argparse

Overview

multicommand

Simple subcommand CLIs with argparse.

PyPI Version Downloads

multicommand uses only the standard library and is ~150 lines of code (modulo comments and whitespace)

Installation

pip install multicommand

Overview

Multicommand enables you to easily write CLIs with deeply nested commands using vanilla argparse. You provide it with a package, it searches that package for parsers (ArgumentParser objects), and connects, names, and converts those parsers into subcommands based on the package structure.

        Package                       ->                    CLI


commands/unary/negate.py                            mycli unary negate ...
commands/binary/add.py                              mycli binary add ...
commands/binary/divide.py             ->            mycli binary divide ...
commands/binary/multiply.py                         mycli binary multiply ...
commands/binary/subtract.py                         mycli binary subtract ...

All it needs is for each module to define a module-level parser variable which points to an instance of argparse.ArgumentParser.

Motivation

I like argparse. It's flexible, full-featured and it's part of the standard library, so if you have python you probably have argparse. I also like the "subcommand" pattern, i.e. one root command that acts as an entrypoint and subcommands to group related functionality. Of course, argparse can handle adding subcommands to parsers, but it's always felt a bit cumbersome, especially when there are many subcommands with lots of nesting.

If you've ever worked with technologies like Next.js or oclif (or even if you haven't) there's a duality between files and "objects". For Next.js each file under pages/ maps to a webpage, in oclif each module under commands/ maps to a CLI command. And that's the basic premise for multicommand: A light-weight package that lets you write one parser per file, pretty much in isolation, and it handles the wiring, exploiting the duality between command structure and file system structure.

Getting Started

See the simple example, or for the impatient:

Create a directory to work in, for example:

mkdir ~/multicommand-sample && cd ~/multicommand-sample

Install multicommand:

python3 -m venv ./venv
source ./venv/bin/activate

python3 -m pip install multicommand

Create the subpackage to house our parsers:

mkdir -p mypkg/parsers/topic/cmd/subcmd

Create the *.py files required for the directories to be packages

touch mypkg/__init__.py
touch mypkg/parsers/__init__.py
touch mypkg/parsers/topic/__init__.py
touch mypkg/parsers/topic/cmd/__init__.py
touch mypkg/parsers/topic/cmd/subcmd/{__init__.py,greet.py}

Add a parser to greet.py:

# file: mypkg/parsers/topic/cmd/subcmd/greet.py
import argparse


def handler(args):
    greeting = f'Hello, {args.name}!'
    print(greeting.upper() if args.shout else greeting)


parser = argparse.ArgumentParser(
    description='My first CLI with multicommand',
    formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument('name', help='Name to use in greeting')
parser.add_argument('--shout', action='store_true', help='Yell the greeting')
parser.set_defaults(handler=handler)

lastly, add an entrypoint:

touch mypkg/cli.py

with the following content:

# file: mypkg/cli.py
import multicommand
from . import parsers


def main():
    parser = multicommand.create_parser(parsers)
    args = parser.parse_args()
    if hasattr(args, 'handler'):
        args.handler(args)
        return
    parser.print_help()


if __name__ == "__main__":
    exit(main())

Try it out!

$ python3 -m mypkg.cli
usage: cli.py [-h] {topic} ...

optional arguments:
  -h, --help  show this help message and exit

subcommands:

  {topic}

Take a look at our greet command:

$ python3 -m mypkg.cli topic cmd subcmd greet --help
usage: cli.py topic cmd subcmd greet [-h] [--shout] name

My first CLI with multicommand

positional arguments:
  name        Name to use in greeting

optional arguments:
  -h, --help  show this help message and exit
  --shout     Yell the greeting (default: False)

From this we get:

$ python3 -m mypkg.cli topic cmd subcmd greet "World"
Hello, World!

$ python3 -m mypkg.cli topic cmd subcmd greet --shout "World"
HELLO, WORLD!

Bonus

Want to add the command topic cmd ungreet ... to say goodbye?

Add the module:

touch mypkg/parsers/topic/cmd/ungreet.py

with contents:

# file: mypkg/parsers/topic/cmd/ungreet.py
import argparse


def handler(args):
    print(f'Goodbye, {args.name}!')


parser = argparse.ArgumentParser(description='Another subcommand with multicommand')
parser.add_argument('name', help='Name to use in un-greeting')
parser.set_defaults(handler=handler)

The new command is automatically added!:

$ python3 -m mypkg.cli topic cmd --help
usage: cli.py cmd [-h] {subcmd,ungreet} ...

optional arguments:
  -h, --help        show this help message and exit

subcommands:

  {subcmd,ungreet}

Try it out:

$ python3 -m mypkg.cli topic cmd ungreet "World"
Goodbye, World!
You might also like...
This a simple tool to query the awesome ippsec.rocks website from your terminal
This a simple tool to query the awesome ippsec.rocks website from your terminal

ippsec-cli This a simple tool to query the awesome ippsec.rocks website from your terminal Installation and usage cd /opt git clone https://github.com

Simple Tool To Grab Like-Card Coupon
Simple Tool To Grab Like-Card Coupon

Simple Tool To Grab Like-Card Coupon

(BionicLambda Universal SHell) A simple shell made in Python. Docs and possible C port incoming.

blush 😳 (BionicLambda Universal SHell) A simple shell made in Python. Docs and possible C port incoming. Note: The Linux executables were made on Ubu

A simple reverse shell in python

RevShell A simple reverse shell in python Getting started First, start the server python server.py Finally, start the client (victim) python client.py

Un module simple pour demander l'accord de l'utilisateur dans une CLI.
Un module simple pour demander l'accord de l'utilisateur dans une CLI.

Demande de confirmation utilisateur pour CLI Présentation ask_lib est un module pour le langage Python proposant une seule fonction; ask(). Le but pri

💻 Physics2Calculator - A simple and powerful calculator for Physics 2
💻 Physics2Calculator - A simple and powerful calculator for Physics 2

💻 Physics2Calculator A simple and powerful calculator for Physics 2 🔌 Predefined constants pi = 3.14159... k = 8988000000 (coulomb constant) e0 = 8.

Simple script to download OTA packages from Realme's endpoint.

Realme OTA Downloader CLI tool (based on this C# program) to create requests to the Realme's endpoint. Requirements Python 3.9. pycryptodome. Installa

A simple command line dumper written in Python 3.

A simple command line dumper written in Python 3.

A simple cli tool to commit Conventional Commits

convmoji A simple cli tool to commit Conventional Commits. Requirements Install pip install convmoji convmoji --help Examples A conventianal commit co

Releases(1.0.0)
  • 0.1.1(Jun 8, 2021)

  • 0.1.0(May 24, 2021)

    • Make sure to always initialize a root index parser (if one doesn't already exist) so that multicommand.create_parser(...) always returns a useable ArgumentParser (instead of raising an exception).

      This way multicommand.create_parser(...) can be called on a package that has no parsers, and will still behave sensibly.

    • Check that found parsers are actually (sub-classes of) ArgumentParser, skip them if they aren't.

    • Fix bug in _requires_subparsers

    • Improve help for subcommands

    Source code(tar.gz)
    Source code(zip)
  • 0.0.8(Apr 9, 2021)

    • Fix: Fix prog=... for intermediate index parsers. Prior to this these parsers would only show the command name (sys.argv[0]) and the last parser's name, but none of the intermediate parser's names, which meant the usage string was wrong.
    • Update: Add a license (MIT)
    • Update: Change registry data structure from List[Tuple[PurePath, ArgumentParser]] -> OrderedDict[PurePath, Dict[str, ArgumentParser]]
    Source code(tar.gz)
    Source code(zip)
  • 0.0.7(Apr 6, 2021)

  • 0.0.6(Apr 5, 2021)

    • Update: Refactored multicommand.py to simplify the structure of the create_parser(...) function. Moreover, the keys in the parser registry (OrderedDict) are now pathlib.PurePath objects instead of tuples. The motivation for this change was because the registry keys (Tuple[str, ...] objects) were already basically being treated like pathlib.Path objects and it made the registry key manipulation easier to read and understand.
    • Update: pyproject.toml (added keywords)
    Source code(tar.gz)
    Source code(zip)
  • 0.0.5(Apr 4, 2021)

    • Update: load_parsers now uses pkg.__name__ when loading parsers into the parser registry (OrderedDict). Prior to this, the value commands was hardcoded meaning that for multicommand to work the subpackage housing all of the parsers had to be called mypkg.<blah>.commands.
    • Update: Documentation
      • General housekeeping: Fixed typos, fixed broken links, added PyPI badge, etc.
    Source code(tar.gz)
    Source code(zip)
  • 0.0.4(Apr 3, 2021)

    • Fix: Fixed the scenario where "intermediate" commands were not defined. When that happened multicommand wouldn't have intermediate parsers to link terminal parsers to the root parser.

      Specifically, if there was say a single command defined as: commands/mytopic/mycommand/mysubcommand.py (with appropriate __init__.py files) multicommand would crash, because the index parsers required to exist "between" the parser defined in mysubcommand.py and the root parser (created by multicommand) weren't getting created, and the application would crash.

    This release also adds a very basic README and a simple example demonstrating the basic usage of multicommand.

    Source code(tar.gz)
    Source code(zip)
  • 0.0.3(Apr 2, 2021)

    • Update: Forward parser config on subparser creation
    • Update: Add custom prog=... when linking parsers to accurately reflect the expected command invocation
    • Maintenance: Add comments, clean up type hints, update pyproject.toml
    Source code(tar.gz)
    Source code(zip)
  • 0.0.2(Apr 1, 2021)

sync-my-tasks is a CLI tool that copies tasks between apps.

sync-my-tasks Copy tasks between apps Report a Bug · Request a Feature . Ask a Question Table of Contents Table of Contents Getting Started Developmen

William Hutson 2 Dec 14, 2021
Magma is a NeoVim plugin for running code interactively with Jupyter.

Magma Magma is a NeoVim plugin for running code interactively with Jupyter. Requirements NeoVim 0.5+ Python 3.8+ Required Python packages: pynvim (for

Daniel Csillag 372 Dec 26, 2022
A simple script to make the operation of AltServer-Linux more easier with cli

A simple script to make the operation of AltServer-Linux more easier with cli

powen 23 Dec 13, 2022
Password manager for the CLI simps.

CLI Password Manager Password manager for the CLI simps. Free software: MIT license

1 Dec 30, 2021
Logic-Sim - A clone of 'Digital Logic Sim' from Sebastian Lague

Logic Simulator This is a clone of 'Digital Logic Sim' from Sebastian Lague. But

Ethan 1 Feb 01, 2022
This is a repository for collecting global custom management extensions for the Django Framework.

Django Extensions Django Extensions is a collection of custom extensions for the Django Framework. Getting Started The easiest way to figure out what

Django Extensions 6k Jan 03, 2023
A CLI framework based on asyncio

asynccli A CLI framework based on asyncio. Note This is still in active development. Things will change. For now, the basic framework is operational.

Adam Hopkins 6 Nov 13, 2022
Wordle-textual - Play Wordle from the CLI, using Textual

Wordle, playable from the CLI This project seeks to emulate Wordle in your shell

PhenoM4n4n 3 Mar 29, 2022
Dart Version Manager CLI implemented with Python and Typer.

Dart Version Manager Dart Version Manager CLI implemented with Python and Typer Usage: $ dvm [OPTIONS] COMMAND [ARGS]... Options: --install-completion

EducUp 6 Jun 26, 2022
Euporie is a text-based user interface for running and editing Jupyter notebooks

Euporie is a text-based user interface for running and editing Jupyter notebooks

781 Jan 01, 2023
Microsoft Azure CLI - Azure Command-Line Interface

A great cloud needs great tools; we're excited to introduce Azure CLI, our next generation multi-platform command line experience for Azure.

Microsoft Azure 3.4k Dec 30, 2022
Simple command line tool for text to image generation using OpenAI's CLIP and Siren (Implicit neural representation network)

Simple command line tool for text to image generation using OpenAI's CLIP and Siren (Implicit neural representation network)

Phil Wang 4.4k Jan 09, 2023
nbcommands bring the goodness of Unix commands to Jupyter notebooks.

nbcommands nbcommands bring the goodness of Unix commands to Jupyter notebooks. Installation You can simply use pip to install nbcommands: $ pip insta

Vinayak Mehta 181 Dec 23, 2022
Open a file in your locally running Visual Studio Code instance from arbitrary terminal connections.

code-connect Open a file in your locally running Visual Studio Code instance from arbitrary terminal connections. Motivation VS Code supports opening

Christian Volkmann 56 Nov 19, 2022
A CLI messenger for the Signum community.

A CLI messenger for the Signum community. Built for people who like using terminal for their work and want to communicate with other users in the Signum community.

Jush 5 Mar 18, 2022
TermPair lets developers securely share and control terminals in real time🔒

View and control terminals from your browser with end-to-end encryption 🔒

Chad Smith 1.5k Jan 05, 2023
A CLI application that downloads your AC submissions from OJ's like Atcoder,Codeforces,CodeChef and distil it into beautiful Submission HeatMap.

Yoda A CLI that takes away the hassle of managing your submission files on different online-judges by automating the entire process of collecting and organizing your code submissions in one single Di

Nikhar Manchanda 1 Jul 28, 2022
Wik is use to get information about anything on the shell using Wikipedia.

WIK wik is a tool to view wikipedia pages from your terminal. It also let you search for any wikipedia up to date article on one query from your termi

Yash Singh 340 Dec 18, 2022
A new kind of Progress Bar, with real time throughput, eta and very cool animations!

A new kind of Progress Bar, with real time throughput, eta and very cool animations!

Rogério Sampaio de Almeida 4.1k Jan 08, 2023
Command-line interface to PyPI Stats API to get download stats for Python packages

pypistats Python 3.6+ interface to PyPI Stats API to get aggregate download statistics on Python packages on the Python Package Index without having t

Hugo van Kemenade 140 Jan 03, 2023