Metadata-Version: 2.4
Name: excli
Version: 0.1.0
Summary: An explicit, zero-dependency CLI framework for Python
Project-URL: Homepage, https://github.com/smm-h/excli
Project-URL: Repository, https://github.com/smm-h/excli
Author-email: "S. M. Hosseini" <m.hosseini@veliu.com>
License-Expression: MIT
License-File: LICENSE
Keywords: argparse,cli,command-line,excli,framework,rlsbl
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Description-Content-Type: text/markdown

# 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

```python
# 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`:

```python
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`:

```python
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

```python
@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

```python
@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:

```python
@excli.flag("verbose", short="v", type=bool, help="verbose output")
```

This allows both `--verbose` and `-v`.

### Required vs optional

- `str` flags with no `default` are required -- the parser errors if missing.
- `str` flags with a `default` are optional.
- `bool` flags always default to `False`.

## Arguments

Positional arguments are declared with `excli.Arg`. There are two equivalent forms.

Using the `args=` parameter:

```python
@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:

```python
@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:

```python
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:

```python
# 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:

```python
@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:

```python
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:

```python
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 `ValueError` at 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 with `prefixed=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](https://docs.python.org/3/library/argparse.html), [click](https://click.palletsprojects.com/), or [typer](https://typer.tiangolo.com/).
