Metadata-Version: 2.4
Name: larzcli
Version: 0.1.0
Summary: Define a function, get a CLI. Type-hint-driven command-line interfaces with subcommands and CLI-env-default layering. Pure Python, zero dependencies.
Author: larz-scripter
License: MIT
Project-URL: Homepage, https://github.com/larz-scripter/larzcli
Project-URL: Repository, https://github.com/larz-scripter/larzcli
Project-URL: Documentation, https://github.com/larz-scripter/larzcli#readme
Project-URL: Issues, https://github.com/larz-scripter/larzcli/issues
Keywords: cli,command-line,argparse,arguments,argument-parser,type-hints,subcommands,click-alternative,zero-dependency,pure-python
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Environment :: Console
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# larzcli

**Define a function, get a CLI. Pure Python, zero dependencies.**

Write a normal type-annotated function and larzcli builds the command-line
interface from its signature — arguments, types, defaults, boolean flags, and
`--help`. The CLI and the code can't drift apart, and there's no parser to
configure.

```python
from larzcli import command, run

@command
def greet(name: str, count: int = 1, shout: bool = False):
    """Greet someone."""
    for _ in range(count):
        print(("HI " if shout else "Hi ") + name)

if __name__ == "__main__":
    run(greet)
```

```console
$ greet Ada --count 3 --shout
HI Ada
HI Ada
HI Ada
```

## What makes it different

Function-first CLIs aren't new — larzcli's edge is **layered configuration built
in**. Options resolve as **command line → environment variable → default**, so
the same command is 12-factor-configurable with zero extra code:

```python
from larzcli import App

app = App("tool", env_prefix="TOOL_")

@app.command
def serve(host: str = "127.0.0.1", port: int = 8080):
    """Run the server."""
    ...

app.run()
```

```console
$ tool serve --port 9000          # explicit
$ TOOL_PORT=9000 tool serve        # from the environment
$ tool serve                       # default 8080
```

The `--help` even documents which env var backs each option. It pairs naturally
with [larzconf](https://github.com/larz-scripter/larzconf) for the same layering
philosophy across your whole app.

## Why else

- **Zero dependencies.** No `click`, no `typer`, no `argparse` boilerplate.
- **Types drive everything.** `int`/`float`/`str`/`bool`/`list` annotations become
  parsing and validation; `bool` becomes a `--flag`/`--no-flag`; params without a
  default become required positionals.
- **Subcommands** with one decorator (`@app.command`), auto-generated help.
- **Testable.** `command.parse(argv, env=...)` returns the resolved kwargs and
  `command.invoke(...)` calls the function — no `sys.exit`, no globals, so unit
  testing a CLI is trivial.

## Install

```bash
pip install larzcli
```

## Usage

```python
from larzcli import command, App, run

# single command
@command
def build(target: str, jobs: int = 4, verbose: bool = False):
    """Build a target."""
    ...

run(build)                       # build app --jobs 8 --verbose

# multi-command app
app = App("git-ish", help="A tiny VCS.")

@app.command
def add(path: str):
    """Stage a file."""
    ...

@app.command(name="commit")
def do_commit(message: str, amend: bool = False):
    """Record changes."""
    ...

app.run()                        # git-ish add file.py ; git-ish commit --message "wip"
```

### Testing a CLI

```python
build.parse(["main", "--jobs", "8"])         # {"target": "main", "jobs": 8, ...}
build.invoke(["main", "--verbose"])          # actually calls build(...)
app.invoke(["add", "file.py"], env={...})
```

## Tests

```bash
python -m unittest discover -s tests -v      # 21 tests, zero deps
```

## The Larz stack

Pure-Python, zero-dependency building blocks: **[larz](https://github.com/larz-scripter/larz)** · **[larzchain](https://github.com/larz-scripter/larzchain)** · **[larzmoney](https://github.com/larz-scripter/larzmoney)** · **[larzcrypt](https://github.com/larz-scripter/larzcrypt)** · **[larzdb](https://github.com/larz-scripter/larzdb)** · **[larzagent](https://github.com/larz-scripter/larzagent)** · **[larzchart](https://github.com/larz-scripter/larzchart)** · **[larzmark](https://github.com/larz-scripter/larzmark)** · **[larztask](https://github.com/larz-scripter/larztask)** · **[larzvault](https://github.com/larz-scripter/larzvault)** · **[larzvm](https://github.com/larz-scripter/larzvm)** · **[larzcache](https://github.com/larz-scripter/larzcache)** · **[larzvalidate](https://github.com/larz-scripter/larzvalidate)** · **[larzid](https://github.com/larz-scripter/larzid)** · **[larzrpc](https://github.com/larz-scripter/larzrpc)** · **[larzstate](https://github.com/larz-scripter/larzstate)** · **[larzhttp](https://github.com/larz-scripter/larzhttp)** · **[larzconf](https://github.com/larz-scripter/larzconf)** · **[larzcron](https://github.com/larz-scripter/larzcron)** · **[larzlimit](https://github.com/larz-scripter/larzlimit)** · **[larzlog](https://github.com/larz-scripter/larzlog)** · **larzcli**

## License

MIT © larz-scripter
