An explicit, zero-dependency CLI framework for Python
Project description
excli
An explicit, zero-dependency CLI framework for Python.
excli makes you declare everything -- every command, flag, argument, and environment variable must have help text or the framework errors at registration time. Types are str and bool only; there is no magic type inference. Environment variables are first-class, with prefix enforcement to keep your config namespace clean.
Installation
uv add excli
Or:
pip install excli
Requires Python 3.11+.
Quickstart
# greet.py
import excli
app = excli.App(name="greet", version="0.1.0", help="a friendly greeter")
@app.command("hello", help="say hello", args=[excli.Arg(name="name", help="who to greet")])
@excli.flag("loud", short="l", type=bool, help="shout the greeting")
def hello(name, loud):
msg = f"Hello, {name}!"
if loud:
msg = msg.upper()
print(msg)
app.run()
$ python greet.py hello World
Hello, World!
$ python greet.py hello --loud World
HELLO, WORLD!
$ python greet.py hello --help
greet hello -- say hello
Arguments:
name who to greet
Flags:
--loud, --no-loud, -l shout the greeting [default: false]
Commands and Groups
Register top-level commands with @app.command:
app = excli.App(name="myapp", version="1.0.0", help="manage deployments")
@app.command("status", help="show current status")
def status():
print("all systems go")
Create groups for two-level nesting with app.group:
db = app.group("db", help="manage databases")
@db.command("migrate", help="run database migrations")
@excli.flag("dry-run", type=bool, help="preview without applying")
def migrate(dry_run):
if dry_run:
print("would run migrations")
else:
print("running migrations")
@db.command("seed", help="populate with sample data")
@excli.flag("count", type=str, help="number of records", default="100")
def seed(count):
print(f"seeding {count} records")
$ myapp db migrate --dry-run
would run migrations
$ myapp db seed --count 500
seeding 500 records
Flags
Declare flags with the @excli.flag decorator. Every flag must have help text.
String flags
@app.command("build", help="build the project")
@excli.flag("output", short="o", type=str, help="output directory", default="dist")
def build(output):
print(f"building to {output}")
String flags accept values via --output dist or --output=dist. A string flag without a default is required.
Bool flags
@app.command("deploy", help="deploy the app")
@excli.flag("force", short="f", type=bool, help="skip confirmation")
def deploy(force):
if force:
print("deploying without confirmation")
Bool flags default to False. Pass --force to set True, or --no-force to explicitly set False. The --no- negation form is available by default for all bool flags; disable it with negatable=False.
Short aliases
Any flag can have a one-character short alias:
@excli.flag("verbose", short="v", type=bool, help="verbose output")
This allows both --verbose and -v.
Required vs optional
strflags with nodefaultare required -- the parser errors if missing.strflags with adefaultare optional.boolflags always default toFalse.
Arguments
Positional arguments are declared with excli.Arg. There are two equivalent forms.
Using the args= parameter:
@app.command("copy", help="copy files", args=[
excli.Arg(name="src", help="source path"),
excli.Arg(name="dst", help="destination path"),
])
def copy(src, dst):
print(f"copying {src} to {dst}")
Using the @excli.arg decorator:
@app.command("show", help="show a file")
@excli.arg("path", help="file to show")
def show(path):
print(f"showing {path}")
Arguments are matched in order. Use required=False for optional arguments. The -- separator stops flag parsing, so everything after it becomes positional:
$ myapp cmd -- --not-a-flag
Environment Variables
Flags can be backed by environment variables with the env parameter:
app = excli.App(name="myapp", version="1.0.0", help="my app", env_prefix="MYAPP")
@app.command("deploy", help="deploy the app")
@excli.flag("region", type=str, help="cloud region", env="MYAPP_REGION", default="us-east-1")
def deploy(region):
print(f"deploying to {region}")
Prefix enforcement
When env_prefix is set on the App, all env vars must start with that prefix. This is validated at registration time:
# This raises ValueError: env var 'REGION' must start with 'MYAPP_'
@excli.flag("region", type=str, help="region", env="REGION", default="x")
External env vars
Use prefixed=False for env vars outside your app's namespace:
@excli.flag("token", type=str, help="auth token", env="GITHUB_TOKEN", prefixed=False, default="")
Priority
Values resolve in this order: CLI argument > environment variable > default. If none of the three provides a value, the parser errors.
Bool env vars
Bool flags from env vars accept 1, true, yes (case-insensitive) for True and 0, false, no for False. Any other value is an error.
Tags
Tags are reusable bundles of flags that can be applied to multiple commands:
auth_tag = excli.Tag(
name="auth",
flags=[
excli.Flag(name="token", type=str, help="auth token", env="MYAPP_TOKEN", default=""),
excli.Flag(name="insecure", type=bool, help="skip TLS verification"),
],
)
@app.command("deploy", help="deploy the app", tags=[auth_tag])
def deploy(token, insecure):
print(f"token={'set' if token else 'unset'}, insecure={insecure}")
@app.command("status", help="check status", tags=[auth_tag])
def status(token, insecure):
print(f"checking status")
Both commands now have --token and --insecure flags. Tag flags appear in help output and are parsed like any other flag.
Help Output
Help is auto-generated at three levels. Pass --help or -h at any level, or invoke the app with no arguments.
App level (myapp --help):
myapp v1.0.0 -- manage deployments
Commands:
deploy deploy the application
Groups:
db manage databases
Use 'myapp <command> --help' for more information.
Group level (myapp db --help):
myapp db -- manage databases
Commands:
migrate run database migrations
seed populate with sample data
Use 'myapp db <command> --help' for more information.
Command level (myapp deploy --help):
myapp deploy -- deploy the application
Arguments:
target deployment target
Flags:
--region, -r <str> cloud region [env: MYAPP_REGION] [default: us-east-1]
--force, --no-force, -f skip confirmation prompt [default: false]
Version: --version or -v prints myapp 1.0.0.
Testing
app.test(argv) runs the CLI in-process and returns a Result with captured output:
result = app.test(["deploy", "--force", "production"])
assert result.exit_code == 0
assert "deploying" in result.stdout
assert result.stderr == ""
The Result dataclass has three fields: stdout, stderr, and exit_code.
Explicit by Design
excli is opinionated about explicitness:
- Help is mandatory. Every command, flag, and argument must have help text. Missing help raises
ValueErrorat registration time, not at runtime. - Only str and bool. No int, float, or list types. Parse them yourself in the handler -- it is one line of code and makes the conversion visible.
- Handler signatures are validated. Every declared flag and arg must have a matching parameter in the handler function, and vice versa. Extra or missing parameters raise
ValueError. - Env var prefixes are enforced. If you set
env_prefix="MYAPP", every env-backed flag must use that prefix (or explicitly opt out withprefixed=False). - No hidden defaults. Required flags fail loudly. Bool flags default to
False. Everything else must be declared.
If you want automatic type coercion, subcommand hierarchies deeper than two levels, or rich terminal formatting, consider argparse, click, or typer.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file excli-0.1.0.tar.gz.
File metadata
- Download URL: excli-0.1.0.tar.gz
- Upload date:
- Size: 21.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
867d85e5d30d97b9591c384f949cb4e15168c62046082ffdf9f6fa205b45f3f2
|
|
| MD5 |
4c2c74a40c2180d5847b5cdf722dbfcb
|
|
| BLAKE2b-256 |
ae8519e0c854e6fe04593eec14dfe6cb6957e171aceb96b9f9f0dbdd8333a9c8
|
Provenance
The following attestation bundles were made for excli-0.1.0.tar.gz:
Publisher:
publish.yml on smm-h/excli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
excli-0.1.0.tar.gz -
Subject digest:
867d85e5d30d97b9591c384f949cb4e15168c62046082ffdf9f6fa205b45f3f2 - Sigstore transparency entry: 1520511460
- Sigstore integration time:
-
Permalink:
smm-h/excli@e25e5d85491bcc676521f814a3d218f3d1e458c6 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/smm-h
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e25e5d85491bcc676521f814a3d218f3d1e458c6 -
Trigger Event:
release
-
Statement type:
File details
Details for the file excli-0.1.0-py3-none-any.whl.
File metadata
- Download URL: excli-0.1.0-py3-none-any.whl
- Upload date:
- Size: 10.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dec63353956612ee1973712ce022c0ae51813ad357d500741d953ebc7c20eaf5
|
|
| MD5 |
54a2f1c58f1e12917b0a5b9df0038255
|
|
| BLAKE2b-256 |
8d8cf5159bedc8234cbcb6bce2e661ea683f7e65db21ad7ef23734b4fbf227fc
|
Provenance
The following attestation bundles were made for excli-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on smm-h/excli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
excli-0.1.0-py3-none-any.whl -
Subject digest:
dec63353956612ee1973712ce022c0ae51813ad357d500741d953ebc7c20eaf5 - Sigstore transparency entry: 1520511475
- Sigstore integration time:
-
Permalink:
smm-h/excli@e25e5d85491bcc676521f814a3d218f3d1e458c6 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/smm-h
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e25e5d85491bcc676521f814a3d218f3d1e458c6 -
Trigger Event:
release
-
Statement type: