# Xclif

[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
[![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
[![codecov](https://codecov.io/gh/ThatXliner/xclif/branch/main/graph/badge.svg)](https://codecov.io/gh/ThatXliner/xclif)

[![CI](https://github.com/ThatXliner/xclif/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/ThatXliner/xclif/actions/workflows/ci.yml)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/xclif)](https://pypi.org/project/xclif)
[![PyPI](https://img.shields.io/pypi/v/xclif)](https://pypi.org/project/xclif)
[![PyPI - License](https://img.shields.io/pypi/l/xclif)](#license)
[![Read the Docs](https://img.shields.io/readthedocs/xclif)](https://xclif.readthedocs.io/en/latest/)


> Xliner's CLI Framework

Read the [Manifesto](MANIFESTO.md) to understand *why* Xclif exists and how it compares to Click, Typer, and argparse.

## Installation

```bash
pip install xclif
```

Or with [uv](https://github.com/astral-sh/uv):

```bash
uv add xclif
```

## Quick Start

Your directory structure *is* your command tree:

```
myapp/
├── __init__.py
├── __main__.py
└── routes/
    ├── __init__.py       →  myapp
    ├── greet.py          →  myapp greet
    └── config/
        ├── __init__.py   →  myapp config
        ├── get.py        →  myapp config get
        └── set.py        →  myapp config set
```

```python
# routes/greet.py
from xclif import command

@command()
def _(name: str, template: str = "Hello, {}!") -> None:
    """Greet someone by name."""
    print(template.format(name))
```

```python
# __main__.py
from xclif import Cli
from . import routes

cli = Cli.from_routes(routes)
if __name__ == "__main__":
    cli()
```

No default → positional argument. Has default → `--template` option. Docstring → help text. Drop a file in the right folder and the command exists.

## Features

- **File-based routing** — directory structure is the command tree
- **Decorator + type-hint API** — function signatures define the CLI contract
- **[Rich](https://github.com/Textualize/rich#readme) integration** — beautiful help pages, formatted errors, progress indicators
- **Built-in logging and verbosity** — `--verbose` / `-v` wired up automatically
- **Config management** — `WithConfig[T]` reads from config files or environment variables (CLI flag > env var > config file > default)
- **Autogenerated shell completions** — bash, zsh, fish
- **Minimal overhead** — custom parser built from scratch for fast startup; `xclif compile` pre-builds a static manifest to eliminate route-walking cost
- **[ExtendedIO](#extendedio)** — reference arbitrary URLs, zip files, SSH, git repos, and other resources as inputs
- **Automatic plugin discovery** — third-party subcommands via entry points (like Git or cargo)
- **Easy testing** — `command.execute(["greet", "Alice"])` with explicit arg lists, no mocking needed

## Performance

> Performance is not a primary focus of Xclif. If startup latency is a hard constraint, Python is probably the wrong tool for the job. These numbers are here for fun. That being said, we do recommend switching to a manifest-based setup for large codebases.

### Startup time

Benchmarked on macOS (Apple Silicon, Python 3.12, 30 iterations + 3 warmup, wall-clock subprocess time):

| Scenario | Click | Typer | Xclif (`from_routes`) | Xclif (flat) | Xclif (manifest) |
|---|---|---|---|---|---|
| `greet World` | 28.0 | 39.8 | 41.0 | 27.2 | **26.9** ✅ |
| `greet` + options | 27.9 | 37.8 | 40.9 | **26.3** ✅ | 26.7 |
| `config set` | 27.6 | 38.0 | 40.5 | **26.1** ✅ | 26.9 |
| `config get` | 27.9 | 38.1 | 40.3 | **26.2** ✅ | 26.8 |
| `--help` | **29.2** ✅ | 82.2 | 59.6 | 46.6 | 47.2 |
| `greet --help` | **29.7** ✅ | 83.8 | 59.3 | 46.8 | 47.6 |

**Xclif (flat)** uses the decorator API (`Command.command()` / `Command.group()`) instead of `from_routes`, and is the fastest framework for command execution — edging out Click by ~1–2 ms. The `--help` gap (~20 ms vs Click) is Rich's lazy-import cost.

**Typer** is the slowest overall: it wraps Click with extra overhead, and its Rich-based help rendering adds ~52–53 ms on `--help` scenarios.

**`from_routes`** adds ~13–15 ms for the package walker on top of Xclif (flat) — the trade-off for zero-registration file-based routing.

**`from_manifest`** eliminates that cost. Running `xclif compile myapp.routes` once generates a `_xclif_manifest.py` next to your routes package; loading it with `Cli.from_manifest()` skips the filesystem walk entirely, matching flat API performance.

```python
# __main__.py — manifest variant
from xclif import Cli
from myapp import _xclif_manifest

cli = Cli.from_manifest(_xclif_manifest)
if __name__ == "__main__":
    cli()
```

```bash
# regenerate after adding or removing routes
xclif compile myapp.routes
```

To reproduce (requires [hyperfine](https://github.com/sharkdp/hyperfine) — `brew install hyperfine` on macOS):

```bash
bash benchmarks/bench_frameworks.sh
```

### Parse and dispatch latency

Measured in-process (no subprocess, no import cost) on the same machine, 5 000 iterations:

| Scenario | Click | Typer | Xclif |
|---|---|---|---|
| `greet World` | 72 µs | 496 µs | **2.7 µs** ✅ |
| `greet` + options | 77 µs | 490 µs | **4.0 µs** ✅ |
| `config set` | 82 µs | 578 µs | **3.4 µs** ✅ |
| `config get` | 79 µs | 553 µs | **3.2 µs** ✅ |
| `--help` | **131 µs** ✅ | 1 879 µs | 483 µs |
| `greet --help` | **142 µs** ✅ | 2 281 µs | 553 µs |

Xclif's custom parser is **~25× faster than Click** and **~170× faster than Typer** for command dispatch. The `--help` cases are slower than Click because Rich is doing real formatting work; Typer is dramatically slower there due to its reflective help generation.

```bash
uv run python benchmarks/bench_parsing.py
```

## ExtendedIO

ExtendedIO is Xclif's approach to transparent resource access. Instead of limiting CLI inputs to local file paths, ExtendedIO lets commands accept arbitrary URIs — URLs, zip archives, SSH paths, git repositories — and resolves them behind the scenes. The API uses dependency injection to provide a unified interface for accessing these resources.

*Coming soon.*

## Roadmap

### Milestone 1: `0.1.0` — Usable Core

- [x] Option value parsing: `--name Bryan` and `--name=Bryan`
- [x] Boolean flags: `--verbose` sets to `True`
- [x] Short aliases: `-v` for `--verbose` (auto-generated)
- [x] `--` separator for raw positional passthrough
- [x] Variadic positional args (`*files: str`)
- [x] `str`, `int`, `float`, `bool` as argument/option types
- [x] `--help` / `-h` on every command
- [x] `--version` on root command (auto-detected from package metadata)
- [x] Rich-formatted help text with alignment
- [x] `Cli.from_routes()` stable and tested
- [x] `xclif compile` + `Cli.from_manifest()` for zero-overhead startup
- [x] `list[str]` for repeated options: `--tag foo --tag bar`
- [x] Proper error messages with friendly, formatted output (includes edit-distance suggestions)
- [ ] `pyproject.toml` finalized and published to PyPI

### Milestone 2: `0.2.0` — Developer Experience

- [ ] `WithConfig[T]` — read from config files (TOML/JSON) and env vars (stub exists, not yet functional)
- [ ] `Annotated[str, Arg(description="...")]` for per-parameter metadata
- [ ] Shell completion generation for bash, zsh, fish (stub exists)
- [ ] Distinct user errors vs developer errors with different output styles
- [ ] Documented exit codes
- [ ] Skip `_`-prefixed modules in `from_routes` (private helpers inside the routes package)

### Milestone 3: `0.3.0` — Power Features

- [ ] Mutually exclusive option groups
- [ ] Cascading global options (opt-in per option)
- [ ] Pre/post command hooks (middleware for auth, logging, etc.)
- [ ] Plugin system: third-party type converters, custom implicit options
- [ ] ExtendedIO

### Non-goals

- Database access or application-level business logic
- Supporting Python < 3.12 (we use `type` statements, generics syntax, etc.)
- A GUI or TUI framework — Xclif is strictly for text CLIs
- Automatic retry, rate limiting, or async command execution

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md).

## License

This project is licensed under the [GNU GPL v3+](https://github.com/ThatXliner/xclif/blob/main/LICENSE.txt).

In short, this means you can do anything with it (distribute, modify, sell) but if you were to publish your changes, you must make the source code and build instructions readily available.

If you are a company using this project and want an exception, email [thatxliner@gmail.com](mailto:thatxliner@gmail.com).
