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)

spade is the next-generation networking command line tool.

spade is the next-generation networking command line tool. Say goodbye to the likes of dig, ping and traceroute with more accessible, more informative and prettier output.

Vivaan Verma 5 Jan 28, 2022
Tarstats - A simple Python commandline application that collects statistics about tarfiles

A simple Python commandline application that collects statistics about tarfiles.

Kristian Koehntopp 13 Feb 20, 2022
uploadgram uses your Telegram account to upload files up to 2GiB, from the Terminal.

uploadgram uploadgram uses your Telegram account to upload files up to 2GiB, from the Terminal. Heavily inspired by the telegram-upload Installing: pi

Shrimadhav U K 97 Jan 06, 2023
xonsh is a Python-powered, cross-platform, Unix-gazing shell

xonsh is a Python-powered, cross-platform, Unix-gazing shell language and command prompt.

xonsh 6.7k Dec 31, 2022
A CLI tool to disable and enable security standards controls in AWS Security Hub

Security Hub Controls CLI A CLI tool to disable and enable security standards controls in AWS Security Hub. It is designed to work together with AWS S

AWS Samples 4 Nov 14, 2022
googler is a power tool to Google (web, news, videos and site search) from the command-line.

googler is a power tool to Google (web, news, videos and site search) from the command-line.

Terminator X 5.9k Jan 04, 2023
A dec-bin converter uses 2's complement.

2's Complement Dec-Bin Converter A dec-bin converter uses 2's complement. Visit my Medium Post. What is 2's complement? Two's complement is the most c

Khaw Chi Hun (Jacky) 9 Mar 01, 2022
A Hikari command handler for people who love ducks.

duckari A Hikari command handler made with love by ducks. Currently Duckari is work in progress. Documentation is WIP. The wiki is no longer used as d

2 Oct 09, 2022
Code for the Open Data Day 2022 publicbodies.org Nepal data scraping activities.

Open Data Day Publicbodies.org Nepal We've gathered on Saturday, 5th March 2022 with Open Knowledge Nepal in order to try and automate the collection

Augusto Herrmann 2 Mar 12, 2022
CLI Utility to encode and recursively recreate directories with ffmpeg.

FFenmass CLI Utility to encode and recursively recreate directories with ffmpeg. Report Bug · Request Feature Table of Contents Getting Started Prereq

George Av. 8 May 06, 2022
Container images for portable development environments

Docker Dev Spin up a container to develop from anywhere! To run, just: docker run -ti aghost7/nodejs-dev:boron tmux new Alternatively, if on Linux: p

Jonathan Boudreau 163 Dec 22, 2022
A Python-based command prompt concept which includes windows command emulation.

PythonCMD A Python-based command prompt concept which includes windows command emulation. Current features: echo: Input your message and it will be cl

1 Feb 05, 2022
A Julia library for solving Wordle puzzles.

Wordle.jl A Julia library for solving Wordle puzzles. Usage julia import Wordle: play julia play("panic") 4 julia play("panic", verbose = true) I

John Myles White 3 Jan 23, 2022
moviepy-cli: Command line interface for MoviePy.

Moviepy-cli is designed to apply several video editing in a single command with Moviepy as an alternative to Video-cli.

Kentaro Wada 23 Jun 29, 2022
Ideas on how to quickly learn to build command-line tools

CLI-Bootcamp Ideas on how to quickly learn to build command-line tools Part 1-Bash Week1: Using Linux Lesson 1: Using Linux Shell Lab Lesson 2: How sh

Noah Gift 10 Apr 18, 2022
Gamma ion pump QPC ethernet Python library & CLI utility

Unofficial Gamma ion pump ethernet control CLI utility and library This is a mini Python 3 library and utility that exposes some of the functions of t

2 Jul 18, 2022
A Terminal Client for MySQL with AutoCompletion and Syntax Highlighting.

mycli A command line client for MySQL that can do auto-completion and syntax highlighting. HomePage: http://mycli.net Documentation: http://mycli.net/

dbcli 10.7k Jan 07, 2023
A Python package for a basic CLI and GUI user interface

Organizer CLI Organizer CLI is a python command line tool that goes through a given directory and organizes all un-folder bound files into folders by

Caltech Library 12 Mar 25, 2022
📦 A command line utility to put text in a box.

boxie A command line utility to put text in a box. Installation pip install boxie If you are on Linux you may need to use sudo to access this globally

Eliaz Bobadilla 10 Jun 30, 2022
A Yahtzee-solving python package and command line tool.

yahtzee A Yahtzee-solving python package and command line tool. The algorithm is mathematically guaranteed to have the best strategy. That is, it maxi

David Merrell 0 Aug 19, 2022