Skip to main content

atomm-clap

Almost Too Much Magic Command Line Argument Parser — a Python library for building deep, domain-specific CLIs whose command definitions read like a list of example invocations.


The idea

Most CLI parsers build a tree of flags and subcommands and then map that tree onto function calls. atomm-clap works the other way around: you write the command paths you want the user to type, and the library figures out the routing.

infra.server.by.nickname[SERVER].edit.config(handler, SERVER)
infra.server.by.nickname[SERVER].restart(handler, SERVER)
infra.deployment[DEPLOYMENT].add.probe.type_[TYPE].to.microscope[SCOPE](handler, ...)

This makes atomm-clap a good fit for infrastructure tools, device managers, and any CLI where the commands have a rich, noun-verb domain vocabulary — the kind of thing kubectl and the Docker CLI do, where commands read like sentences. atomm-clap could, for example, easily implement

kubectl list <entity> in namespace <namespace> with label <label>

instead of

kubectl get pods -n <namespace> -l <label-key>=<label-value>

Installation

pip install atomm-clap

Or from source:

git clone https://github.com/johannes-bauer/atomm-clap
cd atomm-clap
pip install -e .

Quickstart

#!/usr/bin/env python3
import sys
from atomc import CLI, parse_tokens, Argument

NAME = Argument('name')

cli = CLI('greeter')
cli.hello(lambda: print("Hello, world!"))
cli.hello.to[NAME](lambda n: print(f"Hello, {n}!"), NAME)

parse_tokens(cli, sys.argv)
$ greeter hello
Hello, world!

$ greeter hello to Alice
Hello, Alice!

$ greeter --help
Usage:
   greeter hello
   greeter hello to [name]

Building a CLI

Paths

A CLI is a CLI object. Attribute access on it creates subcommand nodes. Calling a node registers a handler for that path:

cli = CLI('mytool')
cli.server.list(list_servers)          # mytool server list
cli.server.restart(restart_server)     # mytool server restart

How matched values are delivered to handlers is covered below.

Arguments

Argument defines a positional capture node. It matches any token and records the value. Use square-bracket syntax to place it in the path:

SERVER = Argument('server')
PORT   = Argument('port', arg_type=int)

arg_type restricts matching to tokens that can be converted and delivers the converted value to the handler:

COUNT = Argument('count', arg_type=int)
cli.run[COUNT].times(handler, COUNT)   # only integers match; handler receives an int

Handlers

Handlers are plain callables — functions or lambdas. There are two ways to pass matched Argument values to a handler.

Explicit: list Argument objects after the callable. The function parameter names can be anything:

cli.status(lambda: print("OK"))

SERVER = Argument('server')
PORT   = Argument('port', arg_type=int)

def show_server(srv):
    print(f"Showing {srv}")

cli.server[SERVER].show(show_server, SERVER)               # mytool server <name> show
cli.server[SERVER].set.port[PORT](set_port, SERVER, PORT)  # mytool server <name> set port <n>

Inferred: pass the handler with call= and name each parameter to match the corresponding Argument name. atomm-clap reads the parameter names with inspect and maps them automatically:

def show_server(server):    # 'server' matches Argument('server')
    print(f"Showing {server}")

cli.server[SERVER].show(call=show_server)

Both forms handle multiple arguments the same way:

ENV     = Argument('env')
VERSION = Argument('version')

def do_deploy(env, version): ...

cli.deploy[ENV].version[VERSION](do_deploy, ENV, VERSION)   # explicit
cli.deploy[ENV].version[VERSION](call=do_deploy)             # inferred

Prefer call= when parameter names naturally match Argument names — it keeps path definitions concise and avoids repeating Argument objects.

Accessing matched values directly

As a lower-level alternative, a handler can accept the raw parser state and call ARG.get_value(parser_state) to retrieve values:

def handler(parser_state):
    server = SERVER.get_value(parser_state)

This is rarely needed — the explicit and call= forms cover the common cases.


Shell completion

Every CLI automatically gains a hidden completion subcommand. Source the output in your shell profile to enable tab completion.

bash

# Creates the function 'mytool'.  Pass anything instead of 'mytool' to choose a different name.
eval "$(python path/to/my_tool/cli.py completion bash mytool)" 

Or add to ~/.bashrc:

# Creates the function 'mytool'.  Pass anything instead of 'mytool' to choose a different name.
source <(python path/to/my_tool/cli.py completion bash mytool)

zsh

# Creates the function 'mytool'.  Pass anything instead of 'mytool' to choose a different name.
eval "$(python path/to/my_tool/cli.py completion zsh mytool)"

fish

# Creates the function 'mytool'.  Pass anything instead of 'mytool' to choose a different name.
python path/to/my_tool/cli.py completion fish mytool | source

Or save to the completions directory:

# Creates the function 'mytool'.  Pass anything instead of 'mytool' to choose a different name.
python path/to/my_tool.py completion fish mytool > ~/.config/fish/completions/mytool.fish

The completion scripts invoke the tool itself with a special sentinel token, so completion always reflects the live CLI definition — no separate completion file to maintain.


Help text

Every node responds to --help / -h. Descriptions can be provided as a string or inferred from a function's docstring:

cli.server(description="Manage servers")

def do_restart(server):
    """Restart the named server and wait for it to come back online."""
    ...

cli.server[SERVER].restart(do_restart, SERVER)
$ mytool server --help
Manage servers

Usage:
   mytool server <server> restart
   
$ mytool server "Some Server" restart --help
Restart the named server and wait for it to come back online.

Usage:
   mytool server <server> restart

Example: bridge.py

examples/bridge.py is a self-contained starship bridge simulator that demonstrates the patterns atomm-clap was built for: deep paths, named-entity selection, two lookup routes to the same action, and optional mid-path segments.

bridge status
bridge alert set red
bridge system sensors repair
bridge system shields set power 80
bridge shields raise
bridge engine engage warp 6
bridge weapon by type torpedo fire at target "Klingon Bird-of-Prey"
bridge weapon by type phaser status
bridge crew ranked "chief engineer" assign to system weapons
bridge crew named Reyes report
bridge scan for ships
bridge scan sector "12-A" for resources
bridge course plot to system "Alpha Centauri"
bridge log entry add "Encountered anomaly at bearing 270"
bridge log show

The CLI definition (excerpt) shows how the paths look in code:

# Two lookup routes ("named" and "ranked") leading to the same actions
bridge.crew.named[CREW_NAME].report(cmd_crew_report_by_name, CREW_NAME)
bridge.crew.named[CREW_NAME].assign.to.system[SYSTEM](cmd_crew_assign_by_name, CREW_NAME, SYSTEM)
bridge.crew.ranked[CREW_RANK].report(cmd_crew_report_by_rank, CREW_RANK)
bridge.crew.ranked[CREW_RANK].assign.to.system[SYSTEM](cmd_crew_assign_by_rank, CREW_RANK, SYSTEM)

# "sector <X>" is optional — both forms route to the same handler
bridge.scan.for_.ships(cmd_scan_ships, SECTOR)
bridge.scan.for_.resources(cmd_scan_resources, SECTOR)
bridge.scan.sector[SECTOR].for_.ships(cmd_scan_ships, SECTOR)
bridge.scan.sector[SECTOR].for_.resources(cmd_scan_resources, SECTOR)

# Eight tokens deep
bridge.weapon.by.type_[WEAPON_TYPE].fire.at.target[TARGET](cmd_weapon_fire, WEAPON_TYPE, TARGET)

Run it with:

cd examples
PYTHONPATH=../src python3 bridge.py --help

Hints

Optional --flags and --options

atomm-clap treats the whole command line as one sentence in a DSL—the only distinction it makes between tokens is between Subcommands and Arguments. --flags- and --option-like patterns can be implemented within that framework:

Implement --flags:

### CLI DEFINITION
def tell_me_about_frogs(everything):
    if not everything:
        assert everything is None
        print("Oh, where do I even start!")
    else:
        assert everything == '--everything'
        print("Really, everything?")
        
tell = CLI('tell')
tell.me.about.frogs(tell_me_about_frogs, tell.me['--everything'])
tell.me['--everything'].about = tell.me.about

The += operator merges one node's successors and executable into another, creating an optional segment in the path.

### SHELL USAGE 
$ tell me about frogs
Oh, where do I even start!

$ tell me --everything about frogs
Really, everything?

Implement --option OPTIONAL_ARGUMENT:

### CLI DEFINITION
counting = CLI('counting')

SEVEN = Argument('seven')

def did_it(seven):
    if not seven:
        print('Skroob: Six? What happened to eight and seven?')
    elif seven not in ['seven', '7']:
        print(f"{seven}?!?")
    else:
        print("Computer: ... five, four, three, two, one.  Have a nice day.\nEverybody: Thank you!")

counting.down.ten.nine.six(did_it, SEVEN)
counting.down.ten.nine['--eight'][SEVEN] += counting.down.ten.nine
### SHELL USAGE
$ counting down ten nine six
Skroob: Six? What happened to eight and seven?

$ counting down ten nine --eight whatever six
whatever?!?

$ counting down ten nine --eight seven six
Computer: ... five, four, three, two, one.  Have a nice day.
Everybody: Thank you!

Python reserved words

Some Python keywords appear naturally in CLI paths. Append _ to use them; atomm-clap strips it from the matched token:

Write Matches
cli.list_ or cli.for_ list, for
cli.type_ type
cli.raise_ raise
cli.scan.for_.ships(scan_ships)   # mytool scan for ships
cli.alert.raise_(sound_alarm)     # mytool alert raise

Reserved attribute names

A handful of names are instance attributes of the node class and cannot be used as path tokens via attribute access: name, description, symbol, executable, hidden. The most commonly encountered one is name.

Use a synonym (named, nickname, callsign, id) or subscript syntax as a workaround:

# This silently returns the string 'by', not a subcommand node:
cli.server.by._atomc_name[SERVER]  # ← WRONG

# Use a synonym instead:
cli.server.by.nickname[SERVER]  # ← OK
cli.server.named[SERVER]  # ← OK

# Or subscript syntax (less readable):
cli.server.by['name'][SERVER]  # ← OK

Decorators

Decorating a function with a CLI node is equivalent to calling it with call=, so argument auto-inference always applies:

ENV     = Argument('env')
VERSION = Argument('version')

@cli.deploy[ENV].version[VERSION]
def do_deploy(env, version):    # parameter names matched to Argument names automatically
    ...

# Exactly equivalent to:
cli.deploy[ENV].version[VERSION](call=do_deploy)

That said, atomm-clap is designed for CLI definitions that read as a compact, scannable list of command paths — the decorator form scatters that list across the module alongside the handler implementations. A flat sequence of path definitions is easier to audit and find entry points in.

Decorators can also introduce performance issues (see below).

Performance

One of the most useful features of atomm-clap are its automatic --help text generation and commandline completion. Especially the latter is useful only if it is fast. If, however, the cli script itself loads an entire application, and especially if that leads to heavy libraries like tensorflow or similar to be loaded just to provide one suggestion or print a help message, then that becomes a lot less fun.

It's therefore recommended to separate CLI definition, API definition, and application:

## api.py
## Note: no imports on the top level.

def get_servers(cluster):
    """Prints the servers on the given cluster."""
    from my_application.kubernetes_app import get_servers as _get_servers
    servers = _get_servers(cluster)
    print(', '.join(servers))


def print_gpus():
    """Prints the GPUs that are currently available."""
    import tensorflow as tf # This can take half a second
    gpus = tf.config.list_physical_devices('GPU')
    if gpus:
        print('Have GPUs:')
        print(gpus)
        return True
    else:
        print("No GPUs found")
        return False
## cli.py
import api

from atomc import CLI, parse_tokens, Argument

CLUSTER = Argument('cluster')

my_tool = CLI('my_tool')

my_tool.print.servers.for_[CLUSTER](api.get_servers, CLUSTER)
my_tool.print.gpus(api.get_gpus)

The following will return without loading anything from the actual application or from tensorflow:

$ my_tool print gpus --help
Prints the GPUs that are currently available.

Personally, we think separating concerns like this is a good pattern anyway.

Printing and Logging

atomm-clap makes heavy use of Python's standard logging facility. In normal operation, it sets its own log level to logging.ERROR so its logs don't clutter the user application's output. atomm-clap's log level can be set explicitly (DEBUG, INFO, ...) using the environment variable _ATOMC_OTHER_LOG_LEVELS.

In commandline completion mode, atomm-clap sets the global log level to logging.ERROR, so application output doesn't clutter the completion. However, atomm-clap does not control print() statements, so if there are any print() statements in the code that execute through imports, then the output of those print() statements will be treated as completion items by the shell.

That should not happen, though, if the code is split into CLI definition, API definition, and application, as recommended under Performance, above.

Metadata

Release files for atomm-clap 0.1.9

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for atomm-clap 0.1.9
File Size Uploaded Provenance
atomm_clap-0.1.9.tar.gz 20.3 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for atomm-clap 0.1.9
File Interpreter ABI Platform
atomm_clap-0.1.9-py3-none-any.whl Python 3 none any Details

Total release size: 33.6 kB

Release files / atomm_clap-0.1.9.tar.gz

Download URL atomm_clap-0.1.9.tar.gz
Size 20.3 kB
Tags Source
SHA-256 checksum
How to use checksums
9d46a3b6b7cc5e0f28f88fee7f339960169f785286428e5829f931dd47b9dbfc
BLAKE2b-256 checksum
How to use checksums
3bf228b1301c3e2e477096cc0e16d8f9289b1ff79ca0cd4cd68e56c139f15124
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 19, 2026.

Transparency log

Release files / atomm_clap-0.1.9-py3-none-any.whl

Download URL atomm_clap-0.1.9-py3-none-any.whl
Size 13.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fe864b409f296e679650bf23c77081a3daced7de98262f68166aa907048f4ee8
BLAKE2b-256 checksum
How to use checksums
24efdfeabc75fc5db952cbb29024b551be8405f4188a3fde4f083b7ff34e0556
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 19, 2026.

Transparency log

Security for atomm-clap 0.1.9

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for atomm_clap-0.1.9.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 19, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page