diff --git a/packages/quickli-docs/docs/concepts/application.md b/packages/quickli-docs/docs/concepts/application.md index dc5480d..95aa7b1 100644 --- a/packages/quickli-docs/docs/concepts/application.md +++ b/packages/quickli-docs/docs/concepts/application.md @@ -1,131 +1,271 @@ --- sidebar_position: 2 -description: Application is the root container of every quiCkLI CLI. -keywords: [quickli, application, entrypoint, command registration, run] +description: Application is the root container and execution manager of every quiCkLI CLI. +keywords: [quickli, application, entrypoint, command registration, run, main, global options, shell completion] --- # Application -`Application` is the root CLI container in `quickli`. It sits at the top of the concept -hierarchy: everything else — commands, arguments, options, and plugins — is registered -against an `Application` instance. +`Application` is the root CLI container and execution manager in `quickli`. It sits at the top of the concept hierarchy: everything else — commands, global options, plugins, and execution options — is registered against or configured within an `Application` instance. ``` Application ← you are here -├── Command -├── Command +├── global_options +├── Command / Subcommand +├── Entrypoint (fallback / single-action) └── Plugin ``` -## What it owns +## What Application Owns -- command registration -- optional root entrypoint registration -- application-level global options -- command dispatch from input tokens -- application and command help rendering +- **Command Registry**: Manages registered **[Command](./command.md)** objects and prevents duplicate command names. +- **Root Entrypoint**: Supports single-action CLIs via `@app.entrypoint`. +- **Global Options**: Defines application-wide **[Option](./option.md)** flags/values available to all commands. +- **Command Dispatch**: Parses `argv` tokens, matches commands/subcommands, and binds arguments and options to handlers. +- **Help Rendering**: Automatically generates structured help text for the application and all registered commands. +- **Shell Completion**: Optionally generates tab-completion scripts for `bash`, `zsh`, and `powershell`. +- **Execution Lifecycles**: Provides `run()` for library/testing use and `main()` for executable CLIs. -## Execution model +## Application Construction Parameters -`Application.run()` dispatches the selected command and returns the handler result (or -generated help text). +When instantiating `Application`, you can customize several core behaviors: -- It reads `sys.argv[1:]` **by default** when called without arguments. -- Pass an explicit list to override: `app.run(["greet", "Ada"])`. -- Set `auto_sys_argv=False` at construction to always use an empty list instead. -- It does **not** print output by default. -- It does **not** choose process exit codes. +```python +from quickli import Application, Option + +app = Application( + name="mytool", + description="A multi-purpose developer CLI.", + global_options=[ + Option("verbose", short_name="v", is_flag=True, help_text="Enable verbose output."), + ], + shell_completion=True, + auto_sys_argv=True, + error_handler=None, +) +``` + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `name` | `str` | `"app"` | The binary/application name used in help text and usage lines. | +| `description` | `str \| None` | `None` | Short description shown at the top of application help output. | +| `global_options` | `list[Option] \| None` | `None` | List of global **[Option](./option.md)** definitions available to all commands. | +| `shell_completion` | `bool` | `False` | When `True`, automatically registers a built-in `shell-completion` command. | +| `auto_sys_argv` | `bool` | `True` | When `True`, `run()` reads `sys.argv[1:]` by default when `argv` is `None`. | +| `error_handler` | `Callable \| None` | `None` | Optional error callback that can inspect or adapt exceptions before `main()` renders them. | -`Application.main(argv=None)` adds the standard executable shell on top of `run()`. +## Command Registration Methods -- It reads `sys.argv[1:]` when `argv` is omitted. -- It prints normal command results. -- It converts runtime failures into structured quickli errors. -- It returns process-friendly exit codes. +`Application` supports four distinct ways to author and register commands: -That split keeps library use explicit while still giving executable applications a simple -default runtime. +### 1. Decorator Registration (`@app.command`) -## Registration API +The most common way to register named commands in multi-command tools. + +```python +from quickli import Application, Argument, Option + +app = Application(name="demo") + +@app.command( + name="greet", + help_text="Greet a user by name.", + arguments=[Argument("name")], + options=[Option("shout", is_flag=True)], +) +def greet_user(name: str, shout: bool = False) -> str: + msg = f"Hello, {name}!" + return msg.upper() if shout else msg +``` -`Application` offers decorator APIs for command registration: +### 2. Imperative Command Registration (`app.register_command`) -- `@app.command(...)` for named commands in a multi-command CLI -- `@app.entrypoint(...)` for a commandless root flow +Useful when commands are constructed dynamically or created in separate modules using the `Command` class. -When both exist, command names take precedence, and the entrypoint acts as fallback. +```python +from quickli import Application, Command, Argument + +app = Application(name="demo") + +build_cmd = Command( + name="build", + help_text="Build the target artefact.", + arguments=[Argument("target")], + handler=lambda target: f"building {target}…", +) + +app.register_command(build_cmd) +``` -## Single-action tool example +### 3. Root Entrypoint Registration (`@app.entrypoint`) -Use `@app.entrypoint` when your tool does exactly one thing and does not need named -subcommands. +Used for single-action tools (like `cat` or `head`) that do not require command names. ```python from quickli import Application, Argument -app = Application(name="greet") +app = Application(name="quickhead") +@app.entrypoint( + arguments=[Argument("file_path")], +) +def main_action(file_path: str) -> str: + return f"Reading top lines of {file_path}" -@app.entrypoint(arguments=[Argument("name")]) -def main(name: str) -> str: - return f"Hello, {name}!" +print(app.run(["notes.txt"])) +``` + +:::note[Command Precedence with Entrypoints] +When an `Application` defines both commands and a root entrypoint, input tokens matching a registered command name dispatch to that command. If no command name matches, the tokens fallback to the root entrypoint. +::: +### 4. Plugin Registration (`app.load_plugin`) -print(app.run(["Alice"])) # Hello, Alice! +Used to attach external command modules via the **[Plugin](./plugin.md)** contract. + +```python +from quickli import Application, Plugin + +class AuditPlugin(Plugin): + @property + def name(self) -> str: + return "audit" + + @property + def description(self) -> str: + return "Security audit commands" + + def register(self, application: Application) -> None: + @application.command(help_text="Run security check.") + def check() -> str: + return "audit passed" + +app = Application(name="demo") +app.load_plugin(AuditPlugin()) ``` -## Multi-command tool example +## Global Options -Use `@app.command` when your tool exposes several distinct actions, such as `build`, -`deploy`, and `clean`. +Global options apply across the entire application and can be passed before or after the command name: ```python -from quickli import Application +app = Application( + name="demo", + global_options=[ + Option("config", short_name="c", help_text="Path to configuration file."), + Option("verbose", short_name="v", is_flag=True, help_text="Verbose logging."), + ], +) + +@app.command(help_text="Perform deployment.") +def deploy(config: str | None = None, verbose: bool = False) -> str: + return f"deploying (config={config}, verbose={verbose})" + +# Both token orders are valid: +print(app.run(["--verbose", "deploy", "-c", "app.toml"])) +print(app.run(["deploy", "-c", "app.toml", "--verbose"])) +``` + +:::info[Global Option Availability] +Global options are automatically injected into handler signatures when the handler accepts parameters matching the global option names. See **[Option](./option.md#global-and-local-options)** for more details. +::: -app = Application(name="mytool") +## Execution Models: `run()` vs `main()` +`Application` explicitly separates library-level command execution from executable entrypoints: -@app.command(help_text="Build the project.") -def build() -> str: - return "building…" +``` + ┌──────────────────────┐ + │ sys.argv / input │ + └──────────┬───────────┘ + │ + ┌────────────────┴────────────────┐ + ▼ ▼ + app.run(argv) app.main(argv, format) +┌──────────────────────┐ ┌───────────────────────────┐ +│ - Pure execution │ │ - Calls run() │ +│ - Returns string/text│ │ - Prints stdout │ +│ - No print or exit │ │ - Handles exit codes │ +│ - Ideal for testing │ │ - Supports JSON output │ +└──────────────────────┘ └───────────────────────────┘ +``` +### `Application.run(argv=None)` -@app.command(help_text="Clean build artefacts.") -def clean() -> str: - return "cleaning…" +Runs the command dispatch loop and returns the string output or help text. +- **Arguments**: `argv` (`list[str] | None`). If `None` and `auto_sys_argv=True`, reads `sys.argv[1:]`. +- **Side effects**: None (does not print to stdout/stderr and does not terminate the process). +- **Return value**: The return value of the matched command handler (coerced to string) or generated help text. -print(app.run(["build"])) # building… +```python +# Pure string execution — ideal for unit tests: +output = app.run(["greet", "Alice"]) +assert output == "Hello, Alice!" ``` -## Tips +### `Application.main(argv=None, output_format="text")` -:::note[Single action vs. multiple commands] +Provides an executable wrapper suitable for CLI binaries (`if __name__ == "__main__":`). -Use `@app.entrypoint` for a single-action tool (like `cat` or `head`) and `@app.command` -for a multi-action tool (like `git` or `kubectl`). You can always add commands later — the -entrypoint acts as a fallback when no command name matches. +- **Output handling**: Prints successful results directly to standard output. +- **Exit codes**: Returns `0` on success, or non-zero error exit codes on failure. +- **Machine-readable output**: Pass `output_format="json"` to emit JSON payloads for automated agents or scripts. +- **Error transformation**: Converts unhandled exceptions into structured `UserCodeError` or `InternalCLIError` instances. -::: +```python +if __name__ == "__main__": + app.main() +``` -:::tip[Wrapping `run()` in an executable] +#### JSON Output Format Example -`Application.run()` reads `sys.argv[1:]` by default and returns a string result. -Printing and exit-code handling belong to your `main()` wrapper so that the application -stays independently testable. +```python +app.main(argv=["greet", "Alice"], output_format="json") +# Outputs JSON payload: +# {"status": "success", "result": "Hello, Alice!"} +``` + +## Built-in Shell Completion + +When constructed with `shell_completion=True`, the `Application` automatically registers a `shell-completion` command: ```python -if __name__ == "__main__": - print(app.run()) +app = Application(name="mycli", shell_completion=True) + +# Generate completion script programmatically: +bash_script = app.generate_completion("bash") +zsh_script = app.generate_completion("zsh") +ps_script = app.generate_completion("powershell") ``` -Pass an explicit list to override the default: `app.run(["greet", "Ada"])`. Set -`auto_sys_argv=False` at construction to disable automatic reading entirely. +Users can generate shell completion scripts directly from the CLI: + +```bash +mycli shell-completion bash > /etc/bash_completion.d/mycli +``` +:::tip[Testing Shell Completion] +The standalone completion generators `generate_bash_completion`, `generate_zsh_completion`, and `generate_powershell_completion` are also available from top-level `quickli`. ::: -## Where to go next +## Custom Error Handlers + +You can pass a custom `error_handler` callback to `Application` to process or format exceptions before `main()` outputs them: + +```python +def log_and_format_error(error: Exception) -> str | None: + print(f"[LOG] CLI error encountered: {error}") + return f"Custom Error Message: {error}" + +app = Application(name="demo", error_handler=log_and_format_error) +``` + +## Where to Go Next + +- Define named actions using **[Command](./command.md)** and **[Subcommand](./command.md#nested-subcommands)**. +- Accept positional arguments using **[Argument](./argument.md)**. +- Accept flags and options using **[Option](./option.md)**. +- Persist settings across runs with **[Config](./config.md)**. +- Modularize your application with **[Plugin](./plugin.md)**. -- Add **[Commands](./command.md)** to give your application named actions. -- Add **[Arguments](./argument.md)** and **[Options](./option.md)** to accept input. -- Use **[Plugins](./plugin.md)** to load reusable command sets without modifying the core. diff --git a/packages/quickli-docs/docs/concepts/argument.md b/packages/quickli-docs/docs/concepts/argument.md index d42864a..bb9d266 100644 --- a/packages/quickli-docs/docs/concepts/argument.md +++ b/packages/quickli-docs/docs/concepts/argument.md @@ -1,13 +1,12 @@ --- sidebar_position: 4 -description: Argument describes a required positional input value for a command. -keywords: [quickli, argument, positional, required, optional, converter, validator] +description: Argument describes a required or optional positional input value for a command. +keywords: [quickli, argument, positional, required, optional, converter, validators, metavar, file_path, number_range] --- # Argument -`Argument` describes a positional input value for a command. Arguments belong to a command -(or subcommand) and are resolved in the order they are declared. +`Argument` describes a positional input value for a **[Command](./command.md)** or **[Subcommand](./command.md#nested-subcommands)**. Arguments are matched in the fixed order they are declared in the command definition. ``` Application @@ -15,106 +14,153 @@ Application └── Argument ← you are here ``` -## Core behavior +## Argument Properties -- Arguments are positional and ordered. -- They may be required or optional. -- An argument becomes optional when it has a default value. -- A converter can transform raw text before handler invocation. -- Validators can check converted values. +When defining an `Argument`, you can configure several properties: -## Basic example +| Property | Type | Default | Description | +|---|---|---|---| +| `name` | `str` | Required | Internal parameter name bound to the handler function signature. | +| `help_text` | `str \| None` | `None` | Description shown in command help output. | +| `required` | `bool` | `True` | Whether the positional argument must be provided. Automatically `False` if `default` is given. | +| `default` | `Any` | `None` | Fallback value used when an optional positional argument is omitted. | +| `converter` | `Callable \| None` | `None` | Transformation function applied to the raw string token before validation and execution. | +| `validators` | `list[Callable] \| None` | `None` | Callable checks executed against the converted value. | +| `metavar` | `str \| None` | `None` | Custom placeholder name displayed in generated usage lines and help listings. | + +## Required vs Optional Arguments + +Positional arguments are **required** by default. They become **optional** when a `default` value is provided: ```python from quickli import Application, Argument app = Application(name="demo") - @app.command( - help_text="Count characters in a word.", - arguments=[Argument("word")], + help_text="Greet a user with an optional fallback name.", + arguments=[ + Argument("greeting", help_text="Greeting word."), + Argument("name", default="World", help_text="Recipient name."), + ], ) -def count(word: str) -> str: - return str(len(word)) - +def greet(greeting: str, name: str = "World") -> str: + return f"{greeting}, {name}!" -print(app.run(["count", "hello"])) # 5 +print(app.run(["greet", "Hello"])) # Hello, World! +print(app.run(["greet", "Hello", "Alice"])) # Hello, Alice! ``` -## Optional argument with a default +:::warning[Positional Argument Ordering] +Arguments are matched sequentially from left to right. Place all required positional arguments *before* any optional positional arguments to ensure deterministic CLI token parsing. +::: + +## Type Converters + +By default, argument tokens are passed to handlers as raw strings. Pass a `converter` callable (such as `int`, `float`, `Path`, or a custom function) to transform the token automatically: ```python +from pathlib import Path from quickli import Application, Argument app = Application(name="demo") - @app.command( - help_text="Greet a user.", - arguments=[Argument("name", default="World")], + help_text="Calculate line count for a given file.", + arguments=[ + Argument("file", converter=Path, help_text="Target file path."), + ], ) -def greet(name: str = "World") -> str: - return f"Hello, {name}!" +def count_lines(file: Path) -> str: + lines = len(file.read_text().splitlines()) + return f"{file.name} has {lines} lines" +``` +## Built-in and Custom Validators -print(app.run(["greet"])) # Hello, World! -print(app.run(["greet", "Alice"])) # Hello, Alice! -``` +`quickli` exports several built-in validator functions that run after type conversion: -## Argument with a converter +- `file_path`: Validates that the path exists and is an existing file. +- `directory_path`: Validates that the path exists and is an existing directory. +- `positive_number`: Validates that a numeric value is strictly greater than 0. +- `number_range(min_val, max_val)`: Validates that a numeric value lies within `[min_val, max_val]`. -Pass a `converter` callable to transform the raw string before the handler receives it. +### Example with Built-in Validators ```python +from pathlib import Path from quickli import Application, Argument +from quickli import file_path, number_range app = Application(name="demo") - @app.command( - help_text="Double a number.", - arguments=[Argument("value", converter=int)], + help_text="Process a target file with a specific thread count.", + arguments=[ + Argument("path", converter=Path, validators=[file_path], metavar="FILE"), + Argument("threads", converter=int, validators=[number_range(1, 16)], metavar="N"), + ], ) -def double(value: int) -> str: - return str(value * 2) - +def process(path: Path, threads: int) -> str: + return f"Processing {path.name} with {threads} threads" -print(app.run(["double", "7"])) # 14 +print(app.run(["process", "README.md", "4"])) ``` -## Typical usage +### Custom Validator Example + +You can also provide custom validator callables that raise an error or return `False` when validation fails: -Use arguments for required command context, such as paths, identifiers, or target names: +```python +def validate_even_number(val: int) -> bool: + if val % 2 != 0: + raise ValueError("Value must be an even integer") + return True -- source file path -- resource name -- numeric input for an operation +@app.command( + arguments=[Argument("amount", converter=int, validators=[validate_even_number])], +) +def split(amount: int) -> str: + return f"Half is {amount // 2}" +``` -If required arguments are missing, command execution fails with a deterministic error. +:::info[Validator Metadata in Help Text] +Built-in validators enrich command help output automatically by describing expected constraints (e.g. `[1..16]`) alongside the argument description. +::: -## Tips +## Custom Metavar Formatting -:::tip[Argument vs. Option] -Use an `Argument` when the value is the *subject* of the command — the thing the command -acts on (a file path, a name, an ID). Use an `Option` when the value *modifies how* the -command behaves (a format, a verbosity level, a boolean flag). +Use `metavar` to customize how the positional argument appears in generated usage strings and help screens: +```python +@app.command( + arguments=[Argument("target_url", metavar="URL")], +) +def fetch(target_url: str) -> str: + return f"fetching {target_url}" ``` -# Argument: the file is what the command acts on -cat myfile.txt -# Option: the format modifies how the output looks -cat --format json myfile.txt +Generated usage line: +`Usage: demo fetch URL` + +## Argument vs Option Guidelines + +:::tip[Choosing Between Argument and Option] +- Use an **`Argument`** for the primary **subject** or target of the operation — the core entity the command acts upon (e.g. a source file path, a user ID, or a project name). +- Use an **`Option`** for settings or flags that **modify how** the operation executes (e.g. output format, verbosity level, dry-run toggle, or timeout). + +```bash +# Argument: 'main.py' is the subject being executed +quickpython run main.py + +# Option: '--debug' and '--output' modify how the execution behaves +quickpython run main.py --debug --output=json ``` ::: -:::tip[Order matters] -Arguments are matched positionally in the order they are declared. Place required -arguments before optional ones to keep the command signature predictable. -::: +## Where to Go Next -## Where to go next +- Learn about named inputs and flags in **[Option](./option.md)**. +- See how arguments attach to commands in **[Command](./command.md)**. +- Use **[Parsers](./parsers.md)** inside converters to parse complex string input like JSON or YAML. -- Use **[Options](./option.md)** for named, order-independent input. -- Go back to **[Command](./command.md)** to see the full command structure. diff --git a/packages/quickli-docs/docs/concepts/command.md b/packages/quickli-docs/docs/concepts/command.md index fd27e86..e9862ab 100644 --- a/packages/quickli-docs/docs/concepts/command.md +++ b/packages/quickli-docs/docs/concepts/command.md @@ -1,13 +1,12 @@ --- sidebar_position: 3 -description: Command represents one executable action in a quiCkLI CLI. -keywords: [quickli, command, subcommand, handler, registration] +description: Command represents an executable operation and subcommand hierarchy in a quiCkLI CLI. +keywords: [quickli, command, subcommand, handler, authoring, registration, signature binding, docstrings] --- # Command -`Command` represents one executable operation in a CLI. Commands live directly inside an -`Application` and are the main way to expose named actions to the user. +`Command` represents an individual executable operation in a CLI application. Commands are registered with an **[Application](./application.md)** and own positional **[Argument](./argument.md)** definitions, named **[Option](./option.md)** definitions, and optional nested **[Subcommand](#nested-subcommands)** trees. ``` Application @@ -19,101 +18,179 @@ Application └── Option ``` -`Subcommand` inherits from `Command` and is used for nested command trees. It behaves -identically to a `Command` but is attached under a parent command rather than directly -under the `Application`. +## Command Properties -## What a command contains +A `Command` object contains the following explicit attributes: -- a public command name -- help text (explicit or derived from a docstring) -- positional argument definitions -- named option definitions -- optional nested subcommand definitions -- one handler callable +| Property | Type | Default | Description | +|---|---|---|---| +| `name` | `str` | Required | Public command invocation name (e.g., `"build"`). Normalized from function names if omitted. | +| `help_text` | `str \| None` | `None` | Human-readable help text. If `None`, falls back to the handler function's docstring. | +| `arguments` | `list[Argument]` | `[]` | Positional argument definitions. | +| `options` | `list[Option]` | `[]` | Named local option definitions. | +| `subcommands` | `list[Subcommand]` | `[]` | Nested subcommand definitions. | +| `handler` | `Callable` | Required | The Python function or callable executed when the command matches. | -## Responsibilities +## Ways to Author and Register Commands -A command parses tokens that belong to it, validates the parsed values against resource -definitions, binds values to the handler signature, and executes the handler. +`quickli` supports three explicit ways to define and register commands: -## Naming behavior +### 1. Decorator Authoring (`@app.command`) -Commands are registered under unique names. Function names are normalized by replacing -underscores with hyphens when a name is not explicitly provided. - -## Basic command example +The decorator pattern is the most concise way to define commands directly alongside handler functions: ```python from quickli import Application, Argument, Option app = Application(name="demo") - @app.command( - help_text="Greet someone.", - arguments=[Argument("name")], - options=[Option("shout", is_flag=True)], + name="deploy", + help_text="Deploy an application target.", + arguments=[Argument("target")], + options=[Option("env", short_name="e", default="production")], ) -def greet(name: str, shout: bool = False) -> str: - msg = f"Hello, {name}!" - return msg.upper() if shout else msg +def deploy_app(target: str, env: str = "production") -> str: + return f"Deploying {target} to {env}" + +print(app.run(["deploy", "web-api", "-e", "staging"])) +``` + +### 2. Standalone Class Authoring (`Command` Instance) + +You can instantiate `Command` objects directly for modular codebases where commands are defined across separate modules or packages: + +```python +from quickli import Application, Command, Argument, Option + +# Module: commands/build.py +def build_handler(target: str, release: bool = False) -> str: + mode = "release" if release else "debug" + return f"Building {target} ({mode})" + +build_command = Command( + name="build", + help_text="Compile the source project.", + arguments=[Argument("target")], + options=[Option("release", is_flag=True)], + handler=build_handler, +) + +# Module: main.py +app = Application(name="demo") +app.register_command(build_command) + +print(app.run(["build", "core", "--release"])) +``` + +### 3. Docstring Help Text Authoring + +If `help_text` is omitted, `quickli` automatically uses the handler function's docstring for help generation: + +```python +from quickli import Application + +app = Application(name="demo") + +@app.command() +def test() -> str: + """Run all automated unit tests in the workspace.""" + return "tests passed" + +print(app.run(["test", "--help"])) +``` + +:::tip[Docstrings Keep Code Self-Documenting] +Using docstrings for `help_text` eliminates duplication between code documentation and CLI help screens. The first paragraph of the docstring is rendered in command listings. +::: + +## Command Name Normalization +When using `@app.command()` without an explicit `name` argument, `quickli` normalizes Python function names by converting underscores (`_`) into hyphens (`-`): -print(app.run(["greet", "Alice"])) # Hello, Alice! -print(app.run(["greet", "Alice", "--shout"])) # HELLO, ALICE! +```python +@app.command() +def list_users() -> str: # Registered as command name: "list-users" + return "user listing" + +print(app.run(["list-users"])) ``` -## Nested subcommands example +:::note[Explicit Names Override Normalization] +If you pass `name="list_users"` explicitly to `@app.command()`, the exact string provided will be used as the CLI command name. +::: -Use `Subcommand` when a command naturally groups further actions, such as `env create` -and `env delete`. +## Parameter Binding & Validation + +When a command executes, `quickli` parses input tokens into positional arguments and named options, then matches them against the handler function's parameter signature: + +1. **Token Parsing**: Positional tokens fill **[Argument](./argument.md)** definitions in declaration order. Named tokens (`--flag` or `--option=value`) fill **[Option](./option.md)** definitions. +2. **Conversion & Validation**: Raw string tokens pass through converters (e.g. `int`, `Path`) and validators (e.g., `file_path`, `number_range`). +3. **Signature Binding**: Parsed values are bound to function parameters matching the argument/option names. Missing required arguments or unknown options raise a `CommandExecutionError`. ```python -from quickli import Application, Argument, Subcommand +from quickli import Application, Argument, Option app = Application(name="demo") +@app.command( + arguments=[Argument("count", converter=int)], + options=[Option("label", default="item")], +) +def repeat(count: int, label: str = "item") -> str: + return ", ".join([label] * count) + +print(app.run(["repeat", "3", "--label", "hello"])) # hello, hello, hello +``` + +:::info[Signature Parameter Matching] +Handler parameter names **must** match the defined `Argument` and `Option` resource names. If a parameter is missing or mismatched, `quickli` raises a registration error during setup. +::: + +## Nested Subcommands + +`Subcommand` inherits all capabilities of `Command` and enables nested command trees (e.g. `git remote add` or `kubectl config view`): + +```python +from quickli import Application, Command, Subcommand, Argument, Option + +app = Application(name="cloud") @app.command( name="env", + help_text="Manage cloud environments.", subcommands=[ Subcommand( name="create", - arguments=[Argument("name")], - handler=lambda name: f"created:{name}", - ) + help_text="Create a new environment.", + arguments=[Argument("env_name")], + options=[Option("region", default="us-east-1")], + handler=lambda env_name, region="us-east-1": f"Created {env_name} in {region}", + ), + Subcommand( + name="delete", + help_text="Delete an environment.", + arguments=[Argument("env_name")], + handler=lambda env_name: f"Deleted {env_name}", + ), ], ) -def env() -> str: - return "env" - +def env_root() -> str: + return "Use 'cloud env --help' to list subcommands." -print(app.run(["env", "create", "dev"])) +print(app.run(["env", "create", "staging", "--region", "eu-central-1"])) +# Returns: Created staging in eu-central-1 ``` -## Tips - -:::tip[Command vs. Subcommand] -Use a top-level `@app.command` for actions that are independent of each other, such as -`build` and `clean`. Use a `Subcommand` when actions logically share a namespace, such as -`env create`, `env list`, and `env delete`. +:::tip[Subcommand Scope] +Subcommands inherit global options defined on the **[Application](./application.md)**, but local options belong specifically to the command or subcommand on which they are declared. ::: -:::tip[Help text from docstrings] -If you do not pass `help_text`, `quickli` automatically uses the function's docstring. -This keeps your handler code self-documenting. - -```python -@app.command() -def build() -> str: - """Build the project and produce a release artefact.""" - return "building…" -``` -::: +## Where to Go Next -## Where to go next +- Learn how positional input works in **[Argument](./argument.md)**. +- Learn about flags, repeatable values, and short aliases in **[Option](./option.md)**. +- Learn how global options and entrypoints are configured on **[Application](./application.md)**. +- Parse structured handler input/output using **[Parsers](./parsers.md)**. +- Package sets of commands into reusable modules using **[Plugin](./plugin.md)**. -- Add **[Arguments](./argument.md)** to accept positional input. -- Add **[Options](./option.md)** to accept named flags and values. -- Go back to **[Application](./application.md)** to understand how commands are dispatched. diff --git a/packages/quickli-docs/docs/concepts/config.md b/packages/quickli-docs/docs/concepts/config.md index 67e9f70..8f56cc9 100644 --- a/packages/quickli-docs/docs/concepts/config.md +++ b/packages/quickli-docs/docs/concepts/config.md @@ -1,104 +1,164 @@ --- sidebar_position: 7 -description: Configuration file support for quiCkLI applications using TOML. -keywords: [quickli, config, toml, configuration, schema, validate] +description: Native TOML configuration file management, validation, and auto-initialization in quiCkLI. +keywords: [quickli, config, toml, schema, ConfigField, ConfigSchema, add_auto_init_config, validate_config, generate_schema_json] --- # Configuration Files -`quickli` provides native TOML configuration file support through the `config` module. -Configuration files sit outside the command hierarchy and are loaded by the application -before commands run. +`quickli` provides native TOML configuration file support through its `config` module. Configuration files exist outside the command token stream, enabling applications to load persistent, schema-validated user settings at startup before commands execute. ``` Application +├── global_options ├── Command -│ ├── Argument -│ └── Option -└── Config ← you are here (loaded at startup, not part of the command tree) +└── Config ← you are here (loaded at startup, persistent on disk) ``` -## Resources +## Core Resources & API Summary -- `ConfigField` — describes one expected key with a type, optional default, and validators. -- `ConfigSchema` — a collection of `ConfigField` objects that define the expected structure. -- `Config` — manages reading and writing a TOML file at a given path. -- `ConfigIssue` — a structured finding returned by `validate_config`. -- `add_auto_init_config` — creates a default file on first run; loads it on subsequent runs. -- `validate_config` — returns all issues in a loaded config without raising. +| Resource / Function | Description | +|---|---| +| `ConfigField` | Describes an expected configuration key, including its type, default, required status, validators, and description. | +| `ConfigSchema` | Container for a list of `ConfigField` definitions that form the application's configuration structure. | +| `Config` | Class managing TOML file reading (`load()`), writing (`save()`), and schema binding at a specified `Path`. | +| `ConfigIssue` | Data object holding validation findings (`severity`, `field`, `message`). | +| `add_auto_init_config` | Helper that auto-creates the TOML file with default values on first run, and loads/validates it on subsequent runs. | +| `validate_config` | Validates a `Config` instance without raising exceptions, returning a list of `ConfigIssue` findings. | +| `generate_schema_json` | Exports a dictionary JSON Schema representation of a `ConfigSchema`. | -## Defining a schema +## Defining a Configuration Schema + +A `ConfigSchema` defines the expected structure of a TOML file: ```python from quickli import ConfigField, ConfigSchema +from quickli import positive_number, number_range schema = ConfigSchema( fields=[ - ConfigField("host", value_type=str, required=False, default="localhost"), - ConfigField("port", value_type=int, required=False, default=8080), + ConfigField("host", value_type=str, default="127.0.0.1", description="Server bind host."), + ConfigField("port", value_type=int, default=8080, validators=[number_range(1024, 65535)], description="Port number."), + ConfigField("timeout", value_type=float, default=30.0, validators=[positive_number], description="Request timeout in seconds."), + ConfigField("debug", value_type=bool, default=False, description="Enable debug logging."), ] ) ``` -## Loading and auto-initialising +### ConfigField Attributes + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `name` | `str` | Required | Field key name in the TOML file. | +| `value_type` | `type` | `str` | Expected Python type (`str`, `int`, `float`, `bool`, `list`, `dict`). | +| `required` | `bool` | `False` | When `True`, validation fails if the field is missing from the TOML file and has no default. | +| `default` | `Any` | `None` | Default value written during auto-initialization or used as fallback. | +| `validators` | `list[Callable]` | `None` | Callable checks executed against the field value. | +| `description` | `str \| None` | `None` | Field documentation string. | + +## Auto-Initialization and Loading (`add_auto_init_config`) + +`add_auto_init_config` is the recommended pattern for CLI applications. It ensures the user always has a valid configuration file on disk: + +1. **First Run**: If the target TOML file does not exist, `add_auto_init_config` automatically creates parent directories, populates the file with default values, and returns the default data dictionary. +2. **Subsequent Runs**: If the file exists, `add_auto_init_config` loads the TOML file, validates it against the `ConfigSchema`, and returns the validated dictionary. ```python from pathlib import Path -from quickli import Config, add_auto_init_config +from quickli import Application, Config, add_auto_init_config + +# Path: ~/.config/myapp/config.toml +config_path = Path.home() / ".config" / "myapp" / "config.toml" +config = Config(path=config_path, schema=schema) -config = Config(path=Path.home() / ".myapp" / "config.toml", schema=schema) -data = add_auto_init_config(config) -# First run: writes defaults to file, returns {"host": "localhost", "port": 8080} -# Later runs: loads and validates the existing file +# Load configuration with auto-init: +config_data = add_auto_init_config(config) + +app = Application(name="myapp") + +@app.command(help_text="Start server using loaded config.") +def start() -> str: + host = config_data.get("host", "127.0.0.1") + port = config_data.get("port", 8080) + return f"Server running on {host}:{port}" ``` -## Validating configuration +:::info[Auto-Init Default Values] +Fields marked with `default` values in the schema are written to disk when the configuration file is created for the first time. +::: + +## Inspecting Validation Findings (`validate_config`) + +If you want to inspect issues without raising exceptions, call `validate_config(config)`: ```python from quickli import validate_config issues = validate_config(config) + for issue in issues: - print(f"[{issue.severity.upper()}] {issue.field}: {issue.message}") + if issue.severity == "error": + print(f"❌ [ERROR] Field '{issue.field}': {issue.message}") + elif issue.severity == "warning": + print(f"⚠️ [WARNING] Field '{issue.field}': {issue.message}") ``` -`validate_config` returns: +`validate_config` categorizes issues into: +- **`severity="error"`**: Missing required fields, type mismatches, or validator failures. +- **`severity="warning"`**: Fields present in the user's TOML file that are not defined in the `ConfigSchema`. + +## Exporting JSON Schema (`generate_schema_json`) + +`quickli` allows exporting a JSON Schema object from a `ConfigSchema` for documentation or integration with external editors: + +```python +from quickli import generate_schema_json, render_json -- **errors** for missing required fields, type mismatches, and validator failures. -- **warnings** for fields present in the file but not defined in the schema. +schema_dict = generate_schema_json(schema) +json_str = render_json(schema_dict) +print(json_str) +``` -## Error handling +## Error Handling & Exceptions -| Exception | When raised | -| ----------------------- | ---------------------------------------------------- | -| `ConfigError` | File missing, unreadable, or invalid TOML syntax | -| `ConfigValidationError` | Required field missing or type mismatch on `load()` | +`quickli` provides dedicated exception types for configuration management, both inheriting from `CLIError`: -Both are subclasses of `CLIError`. +| Exception | Cause | +|---|---| +| `ConfigError` | Raised when the configuration file is missing (on direct `load()`), unreadable, or contains invalid TOML syntax. | +| `ConfigValidationError` | Raised when schema validation fails during `config.load()` (e.g. type mismatch or missing required field). | -## Format support +```python +from quickli import Config, ConfigError, ConfigValidationError + +try: + data = config.load() +except ConfigValidationError as e: + print(f"Configuration invalid: {e}") +except ConfigError as e: + print(f"Failed to read configuration file: {e}") +``` -- **Reading**: uses `tomllib` from the Python standard library (Python 3.11+). -- **Writing**: uses a built-in serialiser for scalars and one level of nested tables. -- `None` values are silently skipped when writing. +## Format Support & Serialization -## Tips +- **Reading**: Uses standard library `tomllib` (Python 3.11+). +- **Writing**: Built-in serialiser supports scalar types (`str`, `int`, `float`, `bool`), lists, and single-level nested tables (`dict`). +- `None` values are silently omitted during serialization. -:::tip[Config vs. Option for persistent settings] -Use a **config file** for settings that users set once and expect to persist between -runs — for example, a default server host or an API base URL. Use a **command option** -for settings that change on a per-invocation basis, such as the output format or a -one-off target path. +:::warning[Serialization Scope] +The built-in TOML writer is designed for scalar fields and shallow dictionary tables. For complex multi-level structures, use **[Parsers](./parsers.md)**. ::: -:::tip[Auto-init on first run] -`add_auto_init_config` is the recommended way to initialise a config file. It writes a -file with all default values on the first run so the user has a concrete starting point -to edit. On every subsequent run it loads and validates the existing file. +## Config vs Option Guidelines + +:::tip[Choosing Between Config and Option] +- Use **`Config`** for persistent settings that users configure once and expect to remain active across multiple CLI runs (e.g., API keys, server base URLs, or default preferences). +- Use **[Option](./option.md)** for per-invocation flags and overrides (e.g., `--verbose`, `--output=json`, or `--dry-run`). ::: -## Where to go next +## Where to Go Next + +- Learn how to initialize configuration in application startup scripts in **[Application](./application.md)**. +- Pass runtime option overrides over configuration defaults using **[Option](./option.md)**. +- Parse or format arbitrary TOML, JSON, or YAML strings using **[Parsers](./parsers.md)**. -- See **[Application](./application.md)** for how to integrate config loading at startup. -- See **[Parsers](./parsers.md)** if you need to parse structured data from command arguments - rather than from a persistent file. diff --git a/packages/quickli-docs/docs/concepts/option.md b/packages/quickli-docs/docs/concepts/option.md index 325aff9..13fc823 100644 --- a/packages/quickli-docs/docs/concepts/option.md +++ b/packages/quickli-docs/docs/concepts/option.md @@ -1,150 +1,199 @@ --- sidebar_position: 5 -description: Option describes a named, order-independent input that modifies command behavior. -keywords: [quickli, option, flag, named, global, local, repeatable] +description: Option describes a named, order-independent input that modifies command or application behavior. +keywords: [quickli, option, flag, short alias, combined flags, repeatable, global options, local options, validators] --- # Option -`Option` describes a named input that modifies command behavior. Options belong to a -command (local options) or to the `Application` itself (global options). +`Option` describes a named, order-independent input that modifies how a command or application behaves. Options can be **local** to a specific **[Command](./command.md)** or **global** across the entire **[Application](./application.md)**. ``` Application -├── global options ← available to every command +├── global_options ← available to every command (e.g. --verbose, --config) └── Command - └── Option ← you are here (local option) + └── Option ← local option (you are here) ``` -## Supported forms +## Option Properties -- long form: `--output value` -- long assignment form: `--output=value` -- short form: `-o value` +When creating an `Option`, you can configure the following properties: -## Core capabilities +| Property | Type | Default | Description | +|---|---|---|---| +| `name` | `str` | Required | Option name, accessed via CLI flags like `--output` and handler parameters like `output`. | +| `help_text` | `str \| None` | `None` | Human-readable explanation rendered in help output. | +| `short_name` | `str \| None` | `None` | Single-character short alias (e.g., `"o"` for `-o`). | +| `required` | `bool` | `False` | Whether the option must be explicitly supplied by the caller. | +| `default` | `Any` | `None` | Default value if the option is omitted. (Default for `is_flag=True` is `False`). | +| `is_flag` | `bool` | `False` | When `True`, treats the option as a boolean toggle switch that takes no value argument. | +| `converter` | `Callable \| None` | `None` | Transformation callable applied to raw option values before validation. | +| `validators` | `list[Callable] \| None` | `None` | Callable validation rules executed after conversion. | +| `multiple` | `bool` | `False` | When `True`, allows the option to be repeated on the command line. | +| `metavar` | `str \| None` | `None` | Custom placeholder text rendered in usage lines and help screens (e.g. `metavar="PATH"`). | -- default values -- required options -- boolean flags (`is_flag=True`) -- conversion callables for non-flag values -- validators for converted values -- repeatable values (`multiple=True`) +## Supported CLI Syntax Forms -Repeatable non-flag options accumulate values in a list. Repeatable flags accumulate -occurrence counts. +`quickli` supports standard POSIX and GNU CLI option syntax out of the box: -## Local and global options +- **Long option with space**: `--output out.json` +- **Long option with equal sign**: `--output=out.json` +- **Short option with space**: `-o out.json` +- **Combined short flags**: `-vxf` (equivalent to `-v -x -f` when all short options are boolean flags) -Options can be defined on commands (local) or at the application level (global). -Global options may appear before or after the command name. +## Boolean Flags vs Value Options -## Basic option example +### Value Option Example ```python -from quickli import Application, Argument, Option +from quickli import Application, Option app = Application(name="demo") - @app.command( - help_text="Write text to a file.", - arguments=[Argument("text")], options=[Option("output", short_name="o", default="out.txt")], ) -def write(text: str, output: str = "out.txt") -> str: - return f"writing '{text}' to {output}" - +def export_data(output: str = "out.txt") -> str: + return f"saving to {output}" -print(app.run(["write", "hello"])) # writing 'hello' to out.txt -print(app.run(["write", "hello", "--output", "a.txt"])) # writing 'hello' to a.txt +print(app.run(["export-data", "--output", "report.pdf"])) # saving to report.pdf +print(app.run(["export-data", "-o", "report.pdf"])) # saving to report.pdf +print(app.run(["export-data", "--output=report.pdf"])) # saving to report.pdf ``` - -## Boolean flag example +### Boolean Flag Example ```python -from quickli import Application, Option - -app = Application(name="demo") - - @app.command( - help_text="Print version information.", - options=[Option("verbose", short_name="v", is_flag=True)], + options=[ + Option("verbose", short_name="v", is_flag=True, help_text="Enable debug mode."), + ], ) -def version(verbose: bool = False) -> str: - if verbose: - return "demo version 1.0.0 (debug build)" - return "demo 1.0.0" +def status(verbose: bool = False) -> str: + return "Status: OK (debug enabled)" if verbose else "Status: OK" - -print(app.run(["version"])) # demo 1.0.0 -print(app.run(["version", "--verbose"])) # demo version 1.0.0 (debug build) -print(app.run(["version", "-v"])) # demo version 1.0.0 (debug build) +print(app.run(["status"])) # Status: OK +print(app.run(["status", "--verbose"])) # Status: OK (debug enabled) +print(app.run(["status", "-v"])) # Status: OK (debug enabled) ``` -:::tip[Flags for on/off switches] -Use `is_flag=True` when the option represents a boolean toggle that does not take a -value. The presence of the flag sets it to `True`; its absence leaves it as `False`. +:::info[Combined Short Flags] +When multiple single-character boolean flags are passed together (e.g., `-abc`), `quickli` splits them into individual flags (`-a`, `-b`, `-c`), provided all short options in the cluster are registered as flags (`is_flag=True`). ::: -## Repeatable option example +## Repeatable Options (`multiple=True`) -```python -from quickli import Application, Option +`Option` supports two distinct types of repeatable behavior: -app = Application(name="demo") +### 1. Repeatable Value Options (Lists) +When `multiple=True` on a value option (`is_flag=False`), repeated invocations accumulate converted values in a Python `list`: +```python @app.command( - help_text="Tag a resource.", - options=[Option("tag", short_name="t", multiple=True)], + options=[ + Option("tag", short_name="t", multiple=True, help_text="Add tag label."), + ], ) -def tag_resource(tag: list[str] | None = None) -> str: - labels = tag or [] - return f"tags: {', '.join(labels)}" +def tag_item(tag: list[str] | None = None) -> str: + tags = tag or [] + return f"Applied tags: {', '.join(tags)}" - -print(app.run(["tag-resource", "--tag", "a", "--tag", "b"])) # tags: a, b +print(app.run(["tag-item", "--tag", "web", "-t", "v1.0", "--tag", "prod"])) +# Returns: Applied tags: web, v1.0, prod ``` -## Global option example +### 2. Repeatable Flag Options (Occurrence Counts) + +When `multiple=True` on a boolean flag (`is_flag=True`), repeated flags accumulate an integer occurrence count (e.g. `-v`, `-vv`, `-v -v -v`): ```python -from quickli import Application, Option +@app.command( + options=[ + Option("verbose", short_name="v", is_flag=True, multiple=True), + ], +) +def run_job(verbose: int = 0) -> str: + return f"Log verbosity level: {verbose}" + +print(app.run(["run-job", "-v"])) # Log verbosity level: 1 +print(app.run(["run-job", "-vvv"])) # Log verbosity level: 3 +``` +:::warning[Repeatable Flag Return Types] +A standard flag (`is_flag=True`, `multiple=False`) returns a `bool`. A repeatable flag (`is_flag=True`, `multiple=True`) returns an `int` representing the occurrence count. Make sure your handler function type hint matches the expected return type. +::: + +## Global and Local Options + +Options can be registered as **local** options on a command or **global** options on the application: + +```python app = Application( name="demo", - global_options=[Option("verbose", short_name="v", is_flag=True)], + global_options=[ + Option("config", short_name="c", help_text="Path to config file."), + Option("quiet", short_name="q", is_flag=True, help_text="Suppress output."), + ], ) +@app.command( + options=[ + Option("format", short_name="f", default="json", help_text="Local format option."), + ], +) +def process(format: str = "json", config: str | None = None, quiet: bool = False) -> str: + return f"format={format}, config={config}, quiet={quiet}" -@app.command(help_text="Build the project.") -def build(verbose: bool = False) -> str: - return f"building… (verbose={verbose})" +# Global options can be placed BEFORE or AFTER the command name: +print(app.run(["--config", "settings.toml", "process", "-f", "yaml"])) +print(app.run(["process", "-f", "yaml", "--config", "settings.toml", "-q"])) +``` +:::note[Global Option Placement] +`quickli` filters global options flexibly regardless of whether the user places them before the command name (`mycli -q process`) or after the command name (`mycli process -q`). +::: -print(app.run(["--verbose", "build"])) # building… (verbose=True) -print(app.run(["build", "--verbose"])) # building… (verbose=True) -``` +## Converters and Built-in Validators -## Tips +Options support type conversion and validation in the same way as **[Argument](./argument.md)**: -:::note[Argument vs. Option] -Use an `Argument` for the primary subject of the command (what it acts on). Use an -`Option` for anything that *changes how* the command behaves — output format, verbosity, -a toggle, or a secondary target. -::: +```python +from pathlib import Path +from quickli import Application, Option +from quickli import file_path, positive_number -:::tip[Local vs. global options] -Define an option as **local** when it only makes sense for one command (like `--output` -for a write command). Define it as **global** when it should apply to every command in -the application (like `--verbose` or `--config`). -::: +app = Application(name="demo") + +@app.command( + options=[ + Option( + "file", + short_name="f", + converter=Path, + validators=[file_path], + metavar="PATH", + help_text="Input file path.", + ), + Option( + "retries", + short_name="r", + converter=int, + validators=[positive_number], + default=3, + help_text="Retry count.", + ), + ], +) +def sync(file: Path | None = None, retries: int = 3) -> str: + file_name = file.name if file else "none" + return f"Syncing {file_name} with max {retries} retries" +``` + +## Where to Go Next -## Where to go next +- Contrast options with positional input in **[Argument](./argument.md)**. +- See how options are attached to command handlers in **[Command](./command.md)**. +- Learn how application-wide global options are configured in **[Application](./application.md)**. +- Store persistent settings on disk instead of per-run options using **[Config](./config.md)**. -- Use **[Arguments](./argument.md)** for positional, ordered input. -- Go back to **[Command](./command.md)** to see how options and arguments fit together. -- See **[Application](./application.md)** for how global options are declared. diff --git a/packages/quickli-docs/docs/concepts/parsers.md b/packages/quickli-docs/docs/concepts/parsers.md index a42f2a1..7201976 100644 --- a/packages/quickli-docs/docs/concepts/parsers.md +++ b/packages/quickli-docs/docs/concepts/parsers.md @@ -1,81 +1,130 @@ --- sidebar_position: 8 -description: Helpers for parsing and rendering JSON, YAML, and TOML in quiCkLI CLIs. -keywords: [quickli, parsers, json, yaml, toml, load, render] +description: Structured data serialization and deserialization helpers (JSON, YAML, TOML) in quiCkLI. +keywords: [quickli, parsers, json, yaml, toml, load_json, render_json, load_yaml, render_yaml, load_toml, render_toml] --- # Parsers -`quickli.parsers` provides explicit helpers for structured JSON, YAML, and TOML input and -output. Parsers are utility functions that sit outside the command hierarchy — you can call -them from any command handler that needs to read or produce structured data. +`quickli.parsers` provides utility functions for parsing and rendering structured data in **JSON**, **YAML**, and **TOML** formats. Parsers are stateless utility functions that exist outside the command token hierarchy — you can invoke them inside **[Command](./command.md)** handlers, argument/option **[Argument](./argument.md#type-converters)** converters, or data processing pipelines. ``` Application └── Command - └── handler() ← call parser helpers from here - load_yaml / render_json / … + └── handler() ← call parser helpers inside handlers or converters + load_json / render_yaml / load_toml / … ``` -## Public APIs +## Public API Reference -| Function | Description | -|---|---| -| `load_json(text)` | Parse a JSON string into a Python object | -| `render_json(value)` | Serialise a Python object to a JSON string | -| `load_yaml(text)` | Parse a YAML string into a Python object | -| `render_yaml(value)` | Serialise a Python object to a YAML string | -| `load_toml(text)` | Parse a TOML string into a Python object | -| `render_toml(value)` | Serialise a Python object to a TOML string | +All parser helpers are exported directly from top-level `quickli`: -## Convert between formats +| Function | Signature | Description | +|---|---|---| +| `load_json` | `(text: str) -> Any` | Parses a JSON string into Python dictionaries/lists. | +| `render_json` | `(value: Any) -> str` | Serializes Python data structures into a formatted JSON string. | +| `load_yaml` | `(text: str) -> Any` | Parses a YAML string into Python dictionaries/lists. | +| `render_yaml` | `(value: Any) -> str` | Serializes Python data structures into a formatted YAML string. | +| `load_toml` | `(text: str) -> Any` | Parses a TOML string into Python dictionaries/lists. | +| `render_toml` | `(value: Any) -> str` | Serializes Python data structures into a formatted TOML string. | + +:::info[Top-Level Module Export] +You can import all parser helpers directly from `quickli` (e.g. `from quickli import load_json, render_yaml`). Importing from `quickli.parsers` is also supported. +::: + +## Example 1: Format Conversion Command (YAML to JSON) + +You can combine `load_yaml` and `render_json` to build format conversion tools: ```python -from quickli import load_yaml, render_json +from pathlib import Path +from quickli import Application, Argument, load_yaml, render_json + +app = Application(name="yaml2json") -data = load_yaml("kind: Pod\nmetadata:\n name: web-preview\n") -print(render_json(data)) +@app.command( + help_text="Convert a YAML file to formatted JSON.", + arguments=[Argument("path", converter=Path)], +) +def convert(path: Path) -> str: + raw_yaml = path.read_text(encoding="utf-8") + data = load_yaml(raw_yaml) + return render_json(data) + +# Usage: yaml2json convert manifest.yaml ``` -## Read structured input from an argument +## Example 2: Argument Conversion with `load_json` + +Use `load_json` as an `Argument` or `Option` converter to parse JSON strings passed on the CLI: ```python -from pathlib import Path from quickli import Application, Argument, load_json app = Application(name="demo") - @app.command( - help_text="Summarise a JSON file.", - arguments=[Argument("path", converter=Path)], + help_text="Inspect JSON payload keys.", + arguments=[ + Argument("payload", converter=load_json, help_text="Raw JSON string."), + ], ) -def summarise(path: Path) -> str: - data = load_json(path.read_text()) - return f"{len(data)} top-level keys" - +def inspect_payload(payload: dict) -> str: + keys = ", ".join(payload.keys()) + return f"Payload contains {len(payload)} keys: {keys}" -print(app.run(["summarise", "data.json"])) +print(app.run(["inspect-payload", '{"name": "Alice", "role": "admin"}'])) +# Returns: Payload contains 2 keys: name, role ``` -## Tips +## Example 3: Formatting Command Output Based on Option -:::tip[Which format to choose] -- Use **JSON** for machine-to-machine data and API responses. -- Use **YAML** for human-edited configuration and Kubernetes-style manifests. -- Use **TOML** for end-user configuration files (see [Configuration Files](./config.md)). +Use parser renderers to format handler responses dynamically based on a user `--format` option: + +```python +from quickli import Application, Option +from quickli import render_json, render_yaml, render_toml -All three helpers are available from the top-level `quickli` import, so you do not need -to import `quickli.parsers` directly. +app = Application(name="demo") + +@app.command( + options=[ + Option("format", short_name="f", default="json", help_text="Output format (json, yaml, toml)."), + ], +) +def info(format: str = "json") -> str: + data = { + "app": "demo", + "status": "healthy", + "metrics": {"cpu": 12.5, "memory_mb": 256}, + } + if format == "yaml": + return render_yaml(data) + elif format == "toml": + return render_toml(data) + return render_json(data) + +print(app.run(["info", "-f", "yaml"])) +``` + +## Format Selection Guide + +:::tip[Choosing the Right Format] +- **JSON**: Best for machine-to-machine interaction, API payloads, and automated script pipelines. +- **YAML**: Best for human-readable configurations and Kubernetes-style manifest files. +- **TOML**: Best for human-editable application configuration files and project settings. ::: -:::tip[Parsers vs. Config] -`load_toml` / `render_toml` are useful for one-off parsing of TOML strings or files that -you manage yourself. For persistent application configuration with schema validation and -auto-initialisation, use the dedicated [Config](./config.md) resources instead. +## Parsers vs Configuration Module + +:::note[Parsers vs Config] +- Use **`parsers`** helpers for one-off parsing or rendering of arbitrary JSON/YAML/TOML strings or files inside command logic. +- Use **[Config](./config.md)** when you need a persistent, schema-validated TOML configuration file on disk with default fallback generation (`add_auto_init_config`). ::: -## Where to go next +## Where to Go Next + +- Learn how to integrate converters into positional input in **[Argument](./argument.md)**. +- Learn how to format CLI options in **[Option](./option.md)**. +- See how persistent TOML configuration works in **[Config](./config.md)**. -- See **[Configuration Files](./config.md)** for persistent, schema-validated config. -- Go back to **[Command](./command.md)** to see how to wire parser helpers into a handler. diff --git a/packages/quickli-docs/docs/concepts/plugin.md b/packages/quickli-docs/docs/concepts/plugin.md index a7c3c5d..8d141fd 100644 --- a/packages/quickli-docs/docs/concepts/plugin.md +++ b/packages/quickli-docs/docs/concepts/plugin.md @@ -1,115 +1,131 @@ --- sidebar_position: 6 -description: Plugins extend a quiCkLI application with reusable command sets. -keywords: [quickli, plugin, extension, register, load_plugin] +description: Modular plugin architecture for extending quiCkLI applications with reusable command sets. +keywords: [quickli, plugin, extension, register, load_plugin, PluginLoadError, modular commands] --- # Plugin -Plugins extend a `quickli` application without modifying the core package. -Each plugin registers its commands and resources against an `Application` instance through a -well-defined contract. +Plugins provide an extensibility mechanism for `quickli` applications without modifying core application code. Each plugin encapsulates a set of **[Command](./command.md)** handlers and resources, registering them against an **[Application](./application.md)** instance through a strict contract. -Plugins exist at the same level as regular commands in the hierarchy: they attach new -commands to an existing `Application` from the outside. +Plugins sit alongside regular application commands in the architecture: ``` Application -├── Command (registered directly) +├── Command (registered directly by app) └── Plugin ← you are here - └── Command (registered by the plugin) + └── Command (registered by plugin.register()) ``` -## Plugin contract +## The Plugin Interface Contract -Every plugin must subclass `quickli.Plugin` and implement three members: +Every plugin must subclass `quickli.Plugin` and implement three required abstract members: -| Member | Kind | Required | Description | -|---|---|---|---| -| `name` | `str` property | yes | Unique, non-empty plugin identifier | -| `description` | `str` property | yes | Short description of what the plugin provides | -| `register(application)` | method | yes | Registers commands and resources against the app | +| Member | Kind | Description | +|---|---|---| +| `name` | `@property -> str` | Unique, non-empty string identifier for the plugin (e.g. `"security-audit"`). | +| `description` | `@property -> str` | Short explanation of the functionality provided by the plugin. | +| `register(application)` | `method(Application) -> None` | Hook method where the plugin registers commands, subcommands, or resources onto the `Application`. | + +### Complete Plugin Example ```python import quickli - -class VersionPlugin(quickli.Plugin): +class DatabasePlugin(quickli.Plugin): @property def name(self) -> str: - return "version-plugin" + return "database" @property def description(self) -> str: - return "Adds a version command." + return "Provides database migration and seeding commands." def register(self, application: quickli.Application) -> None: - @application.command(help_text="Prints the application version.") - def version() -> str: - return "1.0.0" + @application.command( + name="db-migrate", + help_text="Run database schema migrations.", + ) + def migrate() -> str: + return "migrations applied" + + @application.command( + name="db-seed", + help_text="Seed database with sample data.", + ) + def seed() -> str: + return "database seeded" ``` -## Loading a plugin +## Loading Plugins (`Application.load_plugin`) -Call `Application.load_plugin(plugin)` to load a plugin into your application. +To register a plugin, pass an instance of your plugin class to `Application.load_plugin()`: ```python -app = quickli.Application(name="demo") -app.load_plugin(VersionPlugin()) -print(app.run(["version"])) # 1.0.0 -``` +app = quickli.Application(name="mycli") -`load_plugin` validates the plugin name, prevents duplicate loading, and calls -`plugin.register(application)` to let the plugin register its commands. +# Load database plugin: +app.load_plugin(DatabasePlugin()) -## Inspecting loaded plugins +# Execute plugin-provided commands: +print(app.run(["db-migrate"])) # migrations applied +print(app.run(["db-seed"])) # database seeded +``` + +## Inspecting Loaded Plugins (`app.plugins`) -`Application.plugins` returns a copy of the loaded plugin list. +You can inspect all plugins currently loaded into an `Application` by accessing `app.plugins`, which returns a read-only snapshot list of loaded plugin instances: ```python for plugin in app.plugins: - print(f"{plugin.name}: {plugin.description}") + print(f"Plugin: {plugin.name} — {plugin.description}") ``` -## Error handling +## Error Handling (`PluginLoadError`) -`quickli.PluginLoadError` is raised when: +`quickli` enforces plugin validity and unique names during registration. Loading a plugin raises `PluginLoadError` if: -- the plugin name is empty, -- a plugin with the same name is already loaded, -- or the plugin's `register` method raises an exception. +1. **Empty Name**: The plugin `name` property returns an empty or whitespace string. +2. **Duplicate Name**: A plugin with the same `name` is already registered on the `Application`. +3. **Registration Exception**: The plugin's `register(application)` method raises an unhandled exception. ```python +from quickli import Application, PluginLoadError + +app = Application(name="demo") +db_plugin = DatabasePlugin() + +app.load_plugin(db_plugin) + try: - app.load_plugin(VersionPlugin()) -except quickli.PluginLoadError as error: - print(f"Failed to load plugin: {error}") + # Attempting to load the same plugin twice raises PluginLoadError: + app.load_plugin(db_plugin) +except PluginLoadError as err: + print(f"Failed to load plugin: {err}") ``` -## Tips +:::info[Plugin Uniqueness] +Each plugin must return a unique `name` string to avoid collisions between third-party plugin packages. +::: -:::tip[When to use a plugin] -Use a plugin when you want to package a reusable set of commands as a separate Python -module or package. For example, a shared `audit` plugin can be loaded into any team's -CLI without copying code. For small, app-specific commands, just use `@app.command` -directly. +## Plugin Rules and Constraints + +:::warning[Plugins Cannot Override Existing Commands] +A plugin cannot override or replace a command that has already been registered on the `Application` (either by the application itself or by a previously loaded plugin). Attempting to register a duplicate command name raises a `CommandRegistrationError`. ::: -:::warning[Plugins cannot override existing commands] -A plugin cannot replace a command that has already been registered — either by the -application itself or by an earlier plugin. Design your plugins to add new commands -rather than replace existing ones. +:::tip[Modular Application Architecture] +Use plugins to break large CLI applications into independent, reusable modules. For example, a company can distribute a shared `auth` or `telemetry` plugin across multiple team repositories without duplicating command logic. ::: -## Current status +## Current Status & Roadmap + +- **Current (Alpha)**: Explicit plugin instantiation and loading via `app.load_plugin(PluginInstance())`. +- **Planned (Future)**: Automatic plugin discovery via Python package entry points (`importlib.metadata`). -The plugin system is implemented in the Alpha release with explicit loading through -`Application.load_plugin()`. -Automatic plugin discovery via Python package metadata (`importlib.metadata` entry points) -is planned for a future release. +## Where to Go Next -## Reference +- Learn how commands registered by plugins work in **[Command](./command.md)**. +- See how application execution dispatches plugin commands in **[Application](./application.md)**. +- Load persistent configuration inside plugins using **[Config](./config.md)**. -See the [plugin specification](https://github.com/spmse/quickli/blob/main/packages/core/specs/plugin.md) -and [ADR 0002](https://github.com/spmse/quickli/blob/main/packages/core/docs/adr/0002-plugin-api-design.md) -for the full design rationale. diff --git a/packages/quickli-docs/docs/concepts/quickli-concepts.md b/packages/quickli-docs/docs/concepts/quickli-concepts.md index 1cf918d..d00552d 100644 --- a/packages/quickli-docs/docs/concepts/quickli-concepts.md +++ b/packages/quickli-docs/docs/concepts/quickli-concepts.md @@ -1,63 +1,71 @@ --- sidebar_position: 1 -description: Overview of quiCkLI's core concepts and how they relate to each other. -keywords: [quickli, concepts, application, command, argument, option, plugin, hierarchy] +description: Comprehensive overview of quiCkLI's core concepts and how they relate to each other. +keywords: [quickli, concepts, application, command, argument, option, config, parsers, plugin, hierarchy] --- # quiCkLI Concepts -`quickli` is built from a small set of explicit concepts. Each concept has its own -documentation page so the model can evolve without overcrowding one document. - -## Concept pages - -- [Application](./application.md) -- [Command](./command.md) -- [Argument](./argument.md) -- [Option](./option.md) -- [Parsers](./parsers.md) -- [Plugin](./plugin.md) -- [Configuration Files](./config.md) - -## How the concepts fit together - -Every `quickli` CLI has a single `Application` at its root. Commands live inside the -application. Each command owns its arguments and options. Plugins attach additional commands -to an existing application without changing its core. +`quickli` is built from a small set of explicit, modular concepts. Each concept represents a focused building block in your CLI architecture, ensuring clarity, testability, and extensibility without speculative abstractions. ``` Application -├── global options ← named flags/values available to every command +├── global_options ← application-wide flags/values ├── Command "build" -│ ├── Argument "target" ← required positional input -│ └── Option "--output" ← optional named input -├── Command "deploy" -│ ├── Subcommand "dev" -│ └── Subcommand "prod" -└── Plugin "metrics" - └── Command "stats" +│ ├── Argument "target" ← positional required input +│ └── Option "--output" ← named local flag or setting +├── Command "env" +│ └── Subcommand "create" ← nested action under parent command +├── Plugin "metrics" +│ └── Command "stats" ← external commands attached via plugin contract +├── Config ← persistent TOML config loaded at application startup +└── Parsers ← format helpers (JSON, YAML, TOML) used inside handlers ``` -In a typical flow: +## Core Concept Hierarchy + +Every `quickli` CLI has a single **[Application](./application.md)** instance at its root. + +- **[Application](./application.md)** owns the command registry, handles global options, dispatches command tokens, and manages the execution lifecycle (`run()` vs `main()`). +- **[Command](./command.md)** wraps an individual CLI operation. Commands define positional **[Argument](./argument.md)** resources and named **[Option](./option.md)** resources, and can contain nested **[Subcommand](./command.md#nested-subcommands)** trees. +- **[Argument](./argument.md)** represents required or optional positional input passed to a command in a fixed order. +- **[Option](./option.md)** represents named, order-independent input (such as `--verbose` or `--output=file.txt`). Options can be **local** to a command or **global** across the application. +- **[Config](./config.md)** manages persistent TOML settings on disk, using a `ConfigSchema` to validate user configuration on startup. +- **[Parsers](./parsers.md)** provide utility functions (`load_json`, `render_yaml`, `load_toml`, etc.) to parse and format structured data within handlers or converters. +- **[Plugin](./plugin.md)** allows external packages or modules to attach new commands to an `Application` without modifying the core application code. -1. Create an `Application`. -2. Register one or more `Command` handlers (or a single root `entrypoint`). -3. Define each command's `Argument` and `Option` resources. -4. Run the application with command-line tokens (`argv`). +## When to Use Which Concept -## When to use which concept +| Goal / Requirement | Recommended Concept | Concept Page | +|---|---|---| +| Single-action CLI tool (like `cat` or `head`) | `Application` + `@app.entrypoint` | [Application](./application.md) | +| Multi-action CLI tool (like `git` or `kubectl`) | `Application` + `@app.command` | [Application](./application.md) / [Command](./command.md) | +| Accept required or optional positional input | `Argument` | [Argument](./argument.md) | +| Accept optional named flags, toggles, or key-value settings | `Option` | [Option](./option.md) | +| Define flags that apply to *all* commands in the CLI | Global `Option` on `Application` | [Option](./option.md#global-and-local-options) | +| Persist user settings to a TOML file across runs | `Config` + `ConfigSchema` | [Config](./config.md) | +| Parse or produce JSON, YAML, or TOML data within a command | `parsers` helpers | [Parsers](./parsers.md) | +| Package reusable commands across teams or projects | `Plugin` | [Plugin](./plugin.md) | -| You want to… | Use… | -|---|---| -| Build a tool with a single action | `Application` + `@app.entrypoint` | -| Build a tool with multiple actions | `Application` + `@app.command` | -| Accept positional input like a file path | `Argument` | -| Accept a named flag or setting | `Option` | -| Add behavior from a separate package | `Plugin` | -| Persist user settings between runs | `Config` | -| Parse or produce JSON, YAML, or TOML | `parsers` helpers | +:::info[Explicit Architecture] +`quickli` avoids hidden magic and silent assumptions. Resource definitions (`Argument`, `Option`, `ConfigField`) explicitly declare types, defaults, converters, and validators so that help text, parsing, and signature binding remain completely deterministic. +::: + +:::tip[Separation of Execution Models] +When executing a CLI, `Application.run()` executes the selected command and returns its string result without printing or calling `sys.exit()`. This makes unit testing straightforward. For production binaries, `Application.main()` wraps `run()` with standard output formatting, structured error handling, and process exit codes. See the **[Application](./application.md#execution-models-run-vs-main)** documentation for details. +::: -:::note[Not sure where to start?] -Read [Getting Started](../getting-started.md) first, then come back here to explore the -individual concepts in detail. +:::note[Where to Start] +If you are new to `quickli`, start with the **[Getting Started](../getting-started.md)** guide to build your first CLI, then reference these concept pages to explore advanced capabilities. ::: + +## Concept Index + +- **[Application](./application.md)** — Root CLI container, registration APIs, global options, and execution models. +- **[Command](./command.md)** — Command authoring, docstrings, subcommands, signature binding, and validation. +- **[Argument](./argument.md)** — Positional inputs, converters, built-in validators, and metavar formatting. +- **[Option](./option.md)** — Short/long options, boolean flags, repeatable flags/values, local vs global scope. +- **[Config](./config.md)** — Native TOML configuration files, field validation, auto-initialization, and schema JSON. +- **[Parsers](./parsers.md)** — Format helpers for JSON, YAML, and TOML serialization and deserialization. +- **[Plugin](./plugin.md)** — Plugin contract, registration lifecycle, duplicate protection, and error handling. + diff --git a/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/application.md b/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/application.md index afa0f8b..23eb118 100644 --- a/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/application.md +++ b/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/application.md @@ -1,129 +1,255 @@ --- sidebar_position: 2 -description: Application ist der Wurzel-Container jeder quiCkLI-CLI. -keywords: [quickli, application, entrypoint, befehlsregistrierung, run] +description: Application ist der Wurzel-Container und Ausführungsmanager jeder quiCkLI-CLI. +keywords: [quickli, application, entrypoint, befehlsregistrierung, run, main, globale optionen, shell completion] --- # Application -`Application` ist der Wurzel-CLI-Container in `quickli`. Er steht an der Spitze der -Konzepthierarchie: Alles andere — Befehle, Argumente, Optionen und Plugins — wird bei -einer `Application`-Instanz registriert. +`Application` ist der Wurzel-CLI-Container und Ausführungsmanager in `quickli`. Er steht an der Spitze der Konzepthierarchie: Alles andere — Befehle, globale Optionen, Plugins und Ausführungseinstellungen — wird bei einer `Application`-Instanz registriert oder konfiguriert. ``` Application ← du bist hier -├── Command -├── Command +├── global_options +├── Command / Subcommand +├── Entrypoint (Fallback / Einzelaktion) └── Plugin ``` -## Was er verwaltet +## Was Application verwaltet -- Befehlsregistrierung -- Optionale Root-Einstiegspunkt-Registrierung -- Anwendungsweite globale Optionen -- Befehls-Dispatch aus Eingabe-Token -- Anwendungs- und Befehls-Hilfe-Rendering +- **Befehlsregistrierung**: Verwaltet registrierte **[Command](./command.md)**-Objekte und verhindert doppelte Befehlsnamen. +- **Root-Einstiegspunkt**: Unterstützt Einzelaktions-CLIs über `@app.entrypoint`. +- **Globale Optionen**: Definiert anwendungsweite **[Option](./option.md)**-Flags/Werte, die für alle Befehle verfügbar sind. +- **Befehls-Dispatch**: Parst `argv`-Token, ordnet Befehle/Subcommands zu und bindet Argumente und Optionen an Handler. +- **Hilfe-Rendering**: Generiert automatisch strukturierte Hilfetexte für die Anwendung und alle registrierten Befehle. +- **Shell-Vervollständigung**: Generiert optional Tab-Vervollständigungsskripte für `bash`, `zsh` und `powershell`. +- **Ausführungsmodelle**: Bietet `run()` für Bibliotheks-/Testzwecke und `main()` für ausführbare CLIs. -## Ausführungsmodell +## Anwendungskonstruktionsparameter -`Application.run()` dispatcht den ausgewählten Befehl und gibt das Handler-Ergebnis -(oder generierten Hilfstext) zurück. +Bei der Instanziierung von `Application` kannst du mehrere Kernverhalten anpassen: -- Er liest `sys.argv[1:]` **standardmäßig**, wenn er ohne Argumente aufgerufen wird. -- Übergib eine explizite Liste zum Überschreiben: `app.run(["greet", "Ada"])`. -- Setze `auto_sys_argv=False` bei der Konstruktion, um stattdessen immer eine leere Liste zu verwenden. -- Er gibt **standardmäßig keine** Ausgabe aus. -- Er wählt **keine** Prozess-Exit-Codes. +```python +from quickli import Application, Option + +app = Application( + name="mytool", + description="Ein Mehrzweck-Entwickler-CLI.", + global_options=[ + Option("verbose", short_name="v", is_flag=True, help_text="Ausführliche Ausgabe aktivieren."), + ], + shell_completion=True, + auto_sys_argv=True, + error_handler=None, +) +``` + +| Parameter | Typ | Standard | Beschreibung | +|---|---|---|---| +| `name` | `str` | `"app"` | Der Anwendungsname, der in Hilfetexten und Usage-Zeilen verwendet wird. | +| `description` | `str \| None` | `None` | Kurze Beschreibung, die oben in der Anwendungshilfe angezeigt wird. | +| `global_options` | `list[Option] \| None` | `None` | Liste globaler **[Option](./option.md)**-Definitionen, die für alle Befehle gelten. | +| `shell_completion` | `bool` | `False` | Bei `True` wird automatisch ein integrierter `shell-completion`-Befehl registriert. | +| `auto_sys_argv` | `bool` | `True` | Bei `True` liest `run()` standardmäßig `sys.argv[1:]`, wenn `argv` gleich `None` ist. | +| `error_handler` | `Callable \| None` | `None` | Optionaler Fehler-Callback zur Überprüfung oder Anpassung von Ausnahmen vor der Ausgabe in `main()`. | + +## Befehlsregistrierungs-Methoden + +`Application` unterstützt vier verschiedene Wege zum Erstellen und Registrieren von Befehlen: -`Application.main(argv=None)` ergänzt darüber die Standardhülle für ausführbare Programme. +### 1. Dekorator-Registrierung (`@app.command`) -- Er liest `sys.argv[1:]`, wenn `argv` weggelassen wird. -- Er gibt normale Befehlsresultate aus. -- Er wandelt Laufzeitfehler in strukturierte quickli-Fehler um. -- Er liefert prozessfreundliche Exit-Codes zurück. +Der gebräuchlichste Weg zur Registrierung benannter Befehle in Multi-Command-Tools. -Diese Aufteilung hält die Bibliotheksnutzung explizit und gibt ausführbaren Anwendungen -trotzdem eine einfache Standardlaufzeit. +```python +from quickli import Application, Argument, Option + +app = Application(name="demo") + +@app.command( + name="greet", + help_text="Begrüße einen Benutzer namentlich.", + arguments=[Argument("name")], + options=[Option("shout", is_flag=True)], +) +def greet_user(name: str, shout: bool = False) -> str: + msg = f"Hello, {name}!" + return msg.upper() if shout else msg +``` + +### 2. Imperative Befehlsregistrierung (`app.register_command`) -## Registrierungs-API +Nützlich, wenn Befehle dynamisch erstellt oder in separaten Modulen mit der `Command`-Klasse definiert werden. + +```python +from quickli import Application, Command, Argument -`Application` bietet Dekorator-APIs für die Befehlsregistrierung: +app = Application(name="demo") -- `@app.command(...)` für benannte Befehle in einem Multi-Command-CLI -- `@app.entrypoint(...)` für einen befehlslosen Root-Ablauf +build_cmd = Command( + name="build", + help_text="Ziel-Artefakt bauen.", + arguments=[Argument("target")], + handler=lambda target: f"building {target}…", +) -Wenn beide existieren, haben Befehlsnamen Vorrang, und der Einstiegspunkt wirkt als -Fallback. +app.register_command(build_cmd) +``` -## Beispiel für ein Einzelaktions-Tool +### 3. Root-Einstiegspunkt-Registrierung (`@app.entrypoint`) -Verwende `@app.entrypoint`, wenn dein Tool genau eine Aktion ausführt und keine benannten -Unterbefehle benötigt. +Verwendet für Einzelaktions-Tools (wie `cat` oder `head`), die keine Befehlsnamen benötigen. ```python from quickli import Application, Argument -app = Application(name="greet") +app = Application(name="quickhead") + +@app.entrypoint( + arguments=[Argument("file_path")], +) +def main_action(file_path: str) -> str: + return f"Reading top lines of {file_path}" +print(app.run(["notes.txt"])) +``` + +:::note[Befehlsvorrang bei Einstiegspunkten] +Wenn eine `Application` sowohl Befehle als auch einen Root-Einstiegspunkt definiert, werden Eingabe-Token, die einem registrierten Befehlsnamen entsprechen, an diesen Befehl weitergeleitet. Passt kein Befehlsname, greift der Root-Einstiegspunkt als Fallback. +::: -@app.entrypoint(arguments=[Argument("name")]) -def main(name: str) -> str: - return f"Hello, {name}!" +### 4. Plugin-Registrierung (`app.load_plugin`) +Verwendet zum Anhängen externer Befehlsmodule über den **[Plugin](./plugin.md)**-Vertrag. -print(app.run(["Alice"])) # Hello, Alice! +```python +from quickli import Application, Plugin + +class AuditPlugin(Plugin): + @property + def name(self) -> str: + return "audit" + + @property + def description(self) -> str: + return "Sicherheitsaudit-Befehle" + + def register(self, application: Application) -> None: + @application.command(help_text="Sicherheitsprüfung ausführen.") + def check() -> str: + return "audit passed" + +app = Application(name="demo") +app.load_plugin(AuditPlugin()) ``` -## Beispiel für ein Multi-Command-Tool +## Globale Optionen -Verwende `@app.command`, wenn dein Tool mehrere verschiedene Aktionen bereitstellt, etwa -`build`, `deploy` und `clean`. +Globale Optionen gelten für die gesamte Anwendung und können vor oder nach dem Befehlsnamen übergeben werden: ```python -from quickli import Application +app = Application( + name="demo", + global_options=[ + Option("config", short_name="c", help_text="Pfad zur Konfigurationsdatei."), + Option("verbose", short_name="v", is_flag=True, help_text="Ausführliches Logging."), + ], +) + +@app.command(help_text="Deployment ausführen.") +def deploy(config: str | None = None, verbose: bool = False) -> str: + return f"deploying (config={config}, verbose={verbose})" + +# Beide Token-Reihenfolgen sind gültig: +print(app.run(["--verbose", "deploy", "-c", "app.toml"])) +print(app.run(["deploy", "-c", "app.toml", "--verbose"])) +``` -app = Application(name="mytool") +:::info[Verfügbarkeit globaler Optionen] +Globale Optionen werden automatisch in Handler-Signaturen injiziert, wenn der Handler Parameter akzeptiert, deren Namen mit den globalen Optionen übereinstimmen. Siehe **[Option](./option.md#globale-und-lokale-optionen)** für weitere Details. +::: +## Ausführungsmodelle: `run()` vs. `main()` -@app.command(help_text="Projekt bauen.") -def build() -> str: - return "building…" +`Application` trennt Bibliotheks-Befehlsausführung explizit von ausführbaren Einstiegspunkten: +### `Application.run(argv=None)` -@app.command(help_text="Build-Artefakte bereinigen.") -def clean() -> str: - return "cleaning…" +Führt die Befehlsausführungsschleife aus und gibt das Ergebnis oder den Hilfetext als String zurück. +- **Argumente**: `argv` (`list[str] | None`). Wenn `None` und `auto_sys_argv=True`, wird `sys.argv[1:]` gelesen. +- **Nebeneffekte**: Keine (druckt nicht auf stdout/stderr und beendet den Prozess nicht). +- **Rückgabewert**: Rückgabewert des gematchten Befehlshandlers (in String umgewandelt) oder generierter Hilfetext. -print(app.run(["build"])) # building… +```python +# Reine String-Ausführung — ideal für Unit-Tests: +output = app.run(["greet", "Alice"]) +assert output == "Hello, Alice!" ``` -## Tipps +### `Application.main(argv=None, output_format="text")` -:::note[Einzelaktion vs. mehrere Befehle] -Verwende `@app.entrypoint` für ein Einzelaktions-Tool (wie `cat` oder `head`) und -`@app.command` für ein Multi-Aktions-Tool (wie `git` oder `kubectl`). Du kannst jederzeit -Befehle hinzufügen — der Einstiegspunkt wirkt als Fallback, wenn kein Befehlsname passt. -::: +Bietet eine ausführbare Hülle für CLI-Binärdateien (`if __name__ == "__main__":`). -:::tip[`run()` in einem Wrapper aufrufen] -`Application.run()` liest `sys.argv[1:]` standardmäßig und gibt einen String zurück. -Ausgabe und Exit-Code-Behandlung gehören in deinen `main()`-Wrapper, damit die Anwendung -unabhängig testbar bleibt. +- **Ausgabehandhabung**: Druckt erfolgreiche Ergebnisse direkt auf die Standardausgabe. +- **Exit-Codes**: Liefer `0` bei Erfolg oder Fehler-Exit-Codes bei Fehlschlägen zurück. +- **Maschinenlesbare Ausgabe**: Übergib `output_format="json"`, um JSON-Payloads für automatisierte Agenten oder Skripte auszugeben. +- **Fehlerbehandlung**: Wandelt unbehandelte Ausnahmen in strukturierte `UserCodeError`- oder `InternalCLIError`-Instanzen um. ```python if __name__ == "__main__": - print(app.run()) + app.main() ``` -Übergib eine explizite Liste, um den Standard zu überschreiben: `app.run(["greet", "Ada"])`. -Setze `auto_sys_argv=False` bei der Konstruktion, um das automatische Lesen vollständig zu -deaktivieren. +#### JSON-Ausgabeformat-Beispiel + +```python +app.main(argv=["greet", "Alice"], output_format="json") +# Gibt JSON-Payload aus: +# {"status": "success", "result": "Hello, Alice!"} +``` + +## Integrierte Shell-Vervollständigung + +Wenn `shell_completion=True` gesetzt ist, registriert die `Application` automatisch einen `shell-completion`-Befehl: + +```python +app = Application(name="mycli", shell_completion=True) + +# Vervollständigungsskript programmgesteuert generieren: +bash_script = app.generate_completion("bash") +zsh_script = app.generate_completion("zsh") +ps_script = app.generate_completion("powershell") +``` + +Benutzer können Shell-Vervollständigungsskripte direkt aus der CLI generieren: + +```bash +mycli shell-completion bash > /etc/bash_completion.d/mycli +``` + +:::tip[Shell-Vervollständigung testen] +Die eigenständigen Generatoren `generate_bash_completion`, `generate_zsh_completion` und `generate_powershell_completion` sind auch direkt über das Hauptpaket `quickli` verfügbar. ::: +## Benutzerdefinierte Fehler-Handler + +Du kannst einen benutzerdefinierten `error_handler`-Callback an `Application` übergeben, um Ausnahmen zu verarbeiten oder zu formatieren, bevor `main()` sie ausgibt: + +```python +def log_and_format_error(error: Exception) -> str | None: + print(f"[LOG] CLI-Fehler aufgetreten: {error}") + return f"Benutzerdefinierter Fehler: {error}" + +app = Application(name="demo", error_handler=log_and_format_error) +``` + ## Wie geht es weiter? -- Füge **[Commands](./command.md)** hinzu, um deiner Anwendung benannte Aktionen zu geben. -- Füge **[Arguments](./argument.md)** und **[Options](./option.md)** hinzu, um Eingaben anzunehmen. -- Verwende **[Plugins](./plugin.md)**, um wiederverwendbare Befehlssets zu laden, ohne den Kern zu ändern. +- Definiere benannte Aktionen mit **[Command](./command.md)** und **[Subcommand](./command.md#verschachtelte-subcommands)**. +- Akzeptiere positionale Eingaben mit **[Argument](./argument.md)**. +- Akzeptiere Flags und Optionen mit **[Option](./option.md)**. +- Speichere Einstellungen dauerhaft mit **[Config](./config.md)**. +- Modularisiere deine Anwendung mit **[Plugin](./plugin.md)**. + diff --git a/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/argument.md b/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/argument.md index c9d401a..bd8991f 100644 --- a/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/argument.md +++ b/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/argument.md @@ -1,14 +1,12 @@ --- sidebar_position: 4 -description: Argument beschreibt einen erforderlichen positionalen Eingabewert für einen Befehl. -keywords: [quickli, argument, positional, erforderlich, optional, converter, validator] +description: Argument beschreibt einen erforderlichen oder optionalen positionalen Eingabewert für einen Befehl. +keywords: [quickli, argument, positional, erforderlich, optional, converter, validators, metavar, file_path, number_range] --- # Argument -`Argument` beschreibt einen positionalen Eingabewert für einen Befehl. Argumente gehören -zu einem Befehl (oder Subcommand) und werden in der Reihenfolge aufgelöst, in der sie -deklariert wurden. +`Argument` beschreibt einen positionalen Eingabewert für einen **[Command](./command.md)** oder **[Subcommand](./command.md#verschachtelte-subcommands)**. Argumente werden in der festen Reihenfolge abgeglichen, in der sie in der Befehlsdefinition deklariert wurden. ``` Application @@ -16,111 +14,142 @@ Application └── Argument ← du bist hier ``` -## Grundlegendes Verhalten +## Argument-Eigenschaften -- Argumente sind positional und geordnet. -- Sie können erforderlich oder optional sein. -- Ein Argument wird optional, sobald es einen Standardwert besitzt. -- Ein Konverter kann Rohtext vor dem Aufruf des Handlers umwandeln. -- Validatoren können konvertierte Werte prüfen. +Beim Definieren eines `Argument` kannst du mehrere Eigenschaften konfigurieren: -## Einfaches Beispiel +| Eigenschaft | Typ | Standard | Beschreibung | +|---|---|---|---| +| `name` | `str` | Erforderlich | Interner Parametername, der an die Signatur der Handler-Funktion gebunden wird. | +| `help_text` | `str \| None` | `None` | Beschreibung, die in der Befehlshilfe angezeigt wird. | +| `required` | `bool` | `True` | Ob das positionale Argument angegeben werden muss. Bei gesetztem `default` automatisch `False`. | +| `default` | `Any` | `None` | Ersatzwert, falls ein optionales positionales Argument weggelassen wird. | +| `converter` | `Callable \| None` | `None` | Umwandlungsfunktion für das rohe String-Token vor Validierung und Ausführung. | +| `validators` | `list[Callable] \| None` | `None` | Prüffunktionen, die auf den konvertierten Wert angewendet werden. | +| `metavar` | `str \| None` | `None` | Benutzerdefinierter Platzhaltername in Usage-Zeilen und Hilfelisten. | + +## Erforderliche vs. optionale Argumente + +Positionale Argumente sind standardmäßig **erforderlich**. Sie werden **optional**, sobald ein `default`-Wert angegeben wird: ```python from quickli import Application, Argument app = Application(name="demo") - @app.command( - help_text="Zeichen in einem Wort zählen.", - arguments=[Argument("word")], + help_text="Einen Benutzer mit optionalem Ersatznamen begrüßen.", + arguments=[ + Argument("greeting", help_text="Begrüßungswort."), + Argument("name", default="World", help_text="Name des Empfängers."), + ], ) -def count(word: str) -> str: - return str(len(word)) +def greet(greeting: str, name: str = "World") -> str: + return f"{greeting}, {name}!" - -print(app.run(["count", "hello"])) # 5 +print(app.run(["greet", "Hello"])) # Hello, World! +print(app.run(["greet", "Hello", "Alice"])) # Hello, Alice! ``` -## Optionales Argument mit Standardwert +:::warning[Reihenfolge positionaler Argumente] +Argumente werden fortlaufend von links nach rechts abgeglichen. Platziere alle erforderlichen positionalen Argumente *vor* optionalen positionalen Argumenten, um ein deterministisches Token-Parsing zu gewährleisten. +::: + +## Typ-Konverter + +Standardmäßig werden Token als rohe Strings an Handler übergeben. Übergib ein `converter`-Callable (wie `int`, `float`, `Path` oder eine benutzerdefinierte Funktion), um das Token automatisch umzuwandeln: ```python +from pathlib import Path from quickli import Application, Argument app = Application(name="demo") - @app.command( - help_text="Einen Nutzer begrüßen.", - arguments=[Argument("name", default="World")], + help_text="Zeilenanzahl einer Datei berechnen.", + arguments=[ + Argument("file", converter=Path, help_text="Zieldateipfad."), + ], ) -def greet(name: str = "World") -> str: - return f"Hello, {name}!" +def count_lines(file: Path) -> str: + lines = len(file.read_text().splitlines()) + return f"{file.name} has {lines} lines" +``` +## Integrierte und benutzerdefinierte Validatoren -print(app.run(["greet"])) # Hello, World! -print(app.run(["greet", "Alice"])) # Hello, Alice! -``` +`quickli` exportiert mehrere integrierte Validatoren, die nach der Typkonvertierung ausgeführt werden: -## Argument mit Konverter +- `file_path`: Prüft, ob der Pfad existiert und eine Datei ist. +- `directory_path`: Prüft, ob der Pfad existiert und ein Verzeichnis ist. +- `positive_number`: Prüft, ob ein numerischer Wert strikt größer als 0 ist. +- `number_range(min_val, max_val)`: Prüft, ob ein numerischer Wert im Bereich `[min_val, max_val]` liegt. -Übergib ein `converter`-Callable, um den Rohstring vor der Weitergabe an den Handler -umzuwandeln. +### Beispiel mit integrierten Validatoren ```python +from pathlib import Path from quickli import Application, Argument +from quickli import file_path, number_range app = Application(name="demo") - @app.command( - help_text="Eine Zahl verdoppeln.", - arguments=[Argument("value", converter=int)], + help_text="Zieldatei mit bestimmter Thread-Anzahl verarbeiten.", + arguments=[ + Argument("path", converter=Path, validators=[file_path], metavar="FILE"), + Argument("threads", converter=int, validators=[number_range(1, 16)], metavar="N"), + ], ) -def double(value: int) -> str: - return str(value * 2) - +def process(path: Path, threads: int) -> str: + return f"Processing {path.name} with {threads} threads" -print(app.run(["double", "7"])) # 14 +print(app.run(["process", "README.md", "4"])) ``` -## Typische Verwendung +### Beispiel für benutzerdefinierte Validatoren -Verwende Argumente für den erforderlichen Kontext eines Befehls, zum Beispiel Pfade, -Bezeichner oder Zielnamen: +```python +def validate_even_number(val: int) -> bool: + if val % 2 != 0: + raise ValueError("Wert muss eine gerade Zahl sein") + return True -- Quellpfad -- Ressourcenname -- numerische Eingabe für eine Operation +@app.command( + arguments=[Argument("amount", converter=int, validators=[validate_even_number])], +) +def split(amount: int) -> str: + return f"Half is {amount // 2}" +``` -Fehlen erforderliche Argumente, schlägt die Befehlsausführung mit einem deterministischen -Fehler fehl. +:::info[Validator-Metadaten im Hilfetext] +Integrierte Validatoren reichern die Befehlshilfe automatisch an, indem sie erwartete Einschränkungen (z. B. `[1..16]`) neben der Beschreibung darstellen. +::: -## Tipps +## Benutzerdefinierte Metavar-Formatierung -:::tip[Argument vs. Option] -Verwende ein `Argument`, wenn der Wert das *Subjekt* des Befehls ist — das, worauf der -Befehl wirkt (ein Dateipfad, ein Name, eine ID). Verwende eine `Option`, wenn der Wert -*verändert, wie* der Befehl sich verhält (ein Format, ein Ausführlichkeitsgrad, ein -Schalter). +Verwende `metavar`, um anzupassen, wie das positionale Argument in generierten Usage-Strings und Hilfeseiten erscheint: +```python +@app.command( + arguments=[Argument("target_url", metavar="URL")], +) +def fetch(target_url: str) -> str: + return f"fetching {target_url}" ``` -# Argument: Die Datei ist das, worauf der Befehl wirkt -cat myfile.txt -# Option: Das Format verändert, wie die Ausgabe aussieht -cat --format json myfile.txt -``` -::: +Usage-Zeile: `Usage: demo fetch URL` + +## Leitfaden: Argument vs. Option -:::tip[Reihenfolge ist wichtig] -Argumente werden positional in der Reihenfolge abgeglichen, in der sie deklariert wurden. -Platziere erforderliche Argumente vor optionalen, um die Befehlssignatur vorhersehbar zu -halten. +:::tip[Entscheidung zwischen Argument und Option] +- Verwende ein **`Argument`** für das primäre **Subjekt** oder Ziel der Operation — das Kern-Objekt, auf das der Befehl wirkt (z. B. einen Quellpfad, eine Benutzer-ID oder einen Projektnamen). +- Verwende eine **`Option`** für Einstellungen oder Flags, die die **Ausführung verändern** (z. B. Ausgabeformat, Log-Level oder Dry-Run-Schalter). ::: ## Wie geht es weiter? -- Verwende **[Options](./option.md)** für benannte, reihenfolgeunabhängige Eingaben. -- Geh zurück zu **[Command](./command.md)**, um die vollständige Befehlsstruktur zu sehen. +- Erfahre mehr über benannte Eingaben und Flags in **[Option](./option.md)**. +- Siehe, wie Argumente an Befehle gebunden werden in **[Command](./command.md)**. +- Verwende **[Parsers](./parsers.md)** in Konvertern für komplexe String-Eingaben wie JSON oder YAML. + diff --git a/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/command.md b/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/command.md index 0cbbad5..41d6091 100644 --- a/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/command.md +++ b/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/command.md @@ -1,14 +1,12 @@ --- sidebar_position: 3 -description: Command repräsentiert eine ausführbare Aktion in einer quiCkLI-CLI. -keywords: [quickli, command, subcommand, handler, registrierung] +description: Command repräsentiert eine ausführbare Operation und Subcommand-Hierarchie in einer quiCkLI-CLI. +keywords: [quickli, command, subcommand, handler, erstellung, registrierung, signaturbindung, docstrings] --- # Command -`Command` repräsentiert eine ausführbare Operation in einer CLI. Befehle leben direkt in -einer `Application` und sind der wichtigste Weg, benannte Aktionen für den Nutzer -bereitzustellen. +`Command` repräsentiert eine einzelne ausführbare Operation in einer CLI-Anwendung. Befehle werden bei einer **[Application](./application.md)** registriert und besitzen positionale **[Argument](./argument.md)**-Definitionen, benannte **[Option](./option.md)**-Definitionen und optionale verschachtelte **[Subcommand](#verschachtelte-subcommands)**-Bäume. ``` Application @@ -20,102 +18,178 @@ Application └── Option ``` -`Subcommand` erbt von `Command` und wird für verschachtelte Befehlsbäume verwendet. Er -verhält sich identisch zu einem `Command`, ist aber unter einem übergeordneten Befehl und -nicht direkt unter der `Application` eingehängt. +## Befehlseigenschaften -## Bestandteile eines Befehls +Ein `Command`-Objekt enthält die folgenden expliziten Attribute: -- ein öffentlicher Befehlsname -- Hilfetext (explizit angegeben oder aus einem Docstring abgeleitet) -- Definitionen positionaler Argumente -- Definitionen benannter Optionen -- optionale Definitionen verschachtelter Subcommands -- ein aufrufbares Handler-Objekt +| Eigenschaft | Typ | Standard | Beschreibung | +|---|---|---|---| +| `name` | `str` | Erforderlich | Öffentlicher Aufrufname (z. B. `"build"`). Wird automatisch aus Funktionsnamen abgeleitet, falls weggelassen. | +| `help_text` | `str \| None` | `None` | Menschlich lesbarer Hilfetext. Wenn `None`, wird der Docstring der Handler-Funktion verwendet. | +| `arguments` | `list[Argument]` | `[]` | Definitionen positionaler Argumente. | +| `options` | `list[Option]` | `[]` | Definitionen benannter lokaler Optionen. | +| `subcommands` | `list[Subcommand]` | `[]` | Definitionen verschachtelter Unterbefehle. | +| `handler` | `Callable` | Erforderlich | Die Python-Funktion, die ausgeführt wird, wenn der Befehl gematcht wird. | -## Verantwortlichkeiten +## Wege zum Erstellen und Registrieren von Befehlen -Ein Befehl parst die zu ihm gehörenden Tokens, validiert die geparsten Werte anhand der -Ressourcendefinitionen, bindet Werte an die Handler-Signatur und führt den Handler aus. +`quickli` unterstützt drei explizite Wege zur Definition und Registrierung von Befehlen: -## Namensverhalten +### 1. Erstellung per Dekorator (`@app.command`) -Befehle werden unter eindeutigen Namen registriert. Funktionsnamen werden durch das -Ersetzen von Unterstrichen durch Bindestriche normalisiert, sofern kein Name ausdrücklich -angegeben wurde. - -## Einfaches Befehlsbeispiel +Das Dekorator-Muster ist der prägnanteste Weg, Befehle direkt neben Handler-Funktionen zu definieren: ```python from quickli import Application, Argument, Option app = Application(name="demo") - @app.command( - help_text="Jemanden begrüßen.", - arguments=[Argument("name")], - options=[Option("shout", is_flag=True)], + name="deploy", + help_text="Ein Anwendungsziel deployen.", + arguments=[Argument("target")], + options=[Option("env", short_name="e", default="production")], +) +def deploy_app(target: str, env: str = "production") -> str: + return f"Deploying {target} to {env}" + +print(app.run(["deploy", "web-api", "-e", "staging"])) +``` + +### 2. Eigenständige Klasseninstanzierung (`Command`-Instanz) + +Du kannst `Command`-Objekte direkt instanziieren, etwa in modularen Codebasen, in denen Befehle über verschiedene Module hinweg definiert sind: + +```python +from quickli import Application, Command, Argument, Option + +# Modul: commands/build.py +def build_handler(target: str, release: bool = False) -> str: + mode = "release" if release else "debug" + return f"Building {target} ({mode})" + +build_command = Command( + name="build", + help_text="Das Quellprojekt kompilieren.", + arguments=[Argument("target")], + options=[Option("release", is_flag=True)], + handler=build_handler, ) -def greet(name: str, shout: bool = False) -> str: - msg = f"Hello, {name}!" - return msg.upper() if shout else msg +# Modul: main.py +app = Application(name="demo") +app.register_command(build_command) -print(app.run(["greet", "Alice"])) # Hello, Alice! -print(app.run(["greet", "Alice", "--shout"])) # HELLO, ALICE! +print(app.run(["build", "core", "--release"])) ``` -## Beispiel für verschachtelte Subcommands +### 3. Hilfetext aus Docstrings -Verwende `Subcommand`, wenn ein Befehl natürlich weitere Aktionen gruppiert, etwa -`env create` und `env delete`. +Wenn `help_text` weggelassen wird, verwendet `quickli` automatisch den Docstring der Handler-Funktion für die Hilfegenerierung: ```python -from quickli import Application, Argument, Subcommand +from quickli import Application app = Application(name="demo") +@app.command() +def test() -> str: + """Alle automatisierten Unit-Tests im Workspace ausführen.""" + return "tests passed" -@app.command( - name="env", - subcommands=[ - Subcommand( - name="create", - arguments=[Argument("name")], - handler=lambda name: f"created:{name}", - ) - ], -) -def env() -> str: - return "env" +print(app.run(["test", "--help"])) +``` + +:::tip[Docstrings halten Code selbstdokumentierend] +Die Verwendung von Docstrings für `help_text` vermeidet Duplikation zwischen Code-Dokumentation und CLI-Hilfebildschirmen. +::: +## Normalisierung von Befehlsnamen -print(app.run(["env", "create", "dev"])) +Wird `@app.command()` ohne explizites `name`-Argument verwendet, normalisiert `quickli` Python-Funktionsnamen, indem Unterstriche (`_`) durch Bindestriche (`-`) ersetzt werden: + +```python +@app.command() +def list_users() -> str: # Registrierter Befehlsname: "list-users" + return "user listing" + +print(app.run(["list-users"])) ``` -## Tipps +:::note[Explizite Namen überschreiben die Normalisierung] +Wenn du `name="list_users"` explizit an `@app.command()` übergibst, wird genau dieser String als CLI-Befehlsname verwendet. +::: + +## Parameter-Bindung & Validierung -:::tip[Command vs. Subcommand] -Verwende einen Top-Level-`@app.command` für voneinander unabhängige Aktionen wie `build` -und `clean`. Verwende einen `Subcommand`, wenn Aktionen logisch einen gemeinsamen -Namensraum teilen, z. B. `env create`, `env list` und `env delete`. +Bei der Ausführung parst `quickli` Token in positionale Argumente und Optionen und gleicht diese mit den Parametern der Handler-Funktion ab: + +1. **Token-Parsing**: Positionale Token füllen **[Argument](./argument.md)**-Definitionen in Deklarationsreihenfolge. Benannte Token füllen **[Option](./option.md)**-Definitionen. +2. **Konvertierung & Validierung**: Rohe Strings durchlaufen Konverter (z. B. `int`, `Path`) und Validatoren (z. B. `file_path`, `number_range`). +3. **Signaturbindung**: Geparste Werte werden an übereinstimmende Funktionsparameter gebunden. Fehlen erforderliche Argumente oder werden unbekannte Optionen übergeben, wird ein `CommandExecutionError` ausgelöst. + +```python +from quickli import Application, Argument, Option + +app = Application(name="demo") + +@app.command( + arguments=[Argument("count", converter=int)], + options=[Option("label", default="item")], +) +def repeat(count: int, label: str = "item") -> str: + return ", ".join([label] * count) + +print(app.run(["repeat", "3", "--label", "hello"])) # hello, hello, hello +``` + +:::info[Parameterzuordnung der Signatur] +Handler-Parameternamen **müssen** exakt mit den definierten `Argument`- und `Option`-Ressourcennamen übereinstimmen. ::: -:::tip[Hilfetext aus Docstrings] -Wenn du kein `help_text` übergibst, verwendet `quickli` automatisch den Docstring der -Funktion. Dadurch bleibt der Handler-Code selbst dokumentierend. +## Verschachtelte Subcommands + +`Subcommand` erbt alle Fähigkeiten von `Command` und ermöglicht verschachtelte Befehlsbäume (z. B. `git remote add` oder `kubectl config view`): ```python -@app.command() -def build() -> str: - """Das Projekt bauen und ein Release-Artefakt erzeugen.""" - return "building…" +from quickli import Application, Command, Subcommand, Argument, Option + +app = Application(name="cloud") + +@app.command( + name="env", + help_text="Cloud-Umgebungen verwalten.", + subcommands=[ + Subcommand( + name="create", + help_text="Eine neue Umgebung erstellen.", + arguments=[Argument("env_name")], + options=[Option("region", default="us-east-1")], + handler=lambda env_name, region="us-east-1": f"Created {env_name} in {region}", + ), + Subcommand( + name="delete", + help_text="Eine Umgebung löschen.", + arguments=[Argument("env_name")], + handler=lambda env_name: f"Deleted {env_name}", + ), + ], +) +def env_root() -> str: + return "Use 'cloud env --help' to list subcommands." + +print(app.run(["env", "create", "staging", "--region", "eu-central-1"])) ``` + +:::tip[Subcommand-Scope] +Subcommands erben globale Optionen der **[Application](./application.md)**, aber lokale Optionen gehören spezifisch zu dem Befehl oder Subcommand, an dem sie deklariert wurden. ::: ## Wie geht es weiter? -- Füge **[Arguments](./argument.md)** hinzu, um positionelle Eingaben anzunehmen. -- Füge **[Options](./option.md)** hinzu, um benannte Flags und Werte anzunehmen. -- Geh zurück zu **[Application](./application.md)**, um zu verstehen, wie Befehle weitergeleitet werden. +- Lerne, wie positionale Eingaben in **[Argument](./argument.md)** funktionieren. +- Lerne mehr über Flags, wiederholbare Werte und Kurzaliase in **[Option](./option.md)**. +- Erfahre, wie globale Optionen in **[Application](./application.md)** konfiguriert werden. +- Verarbeite strukturierte Daten mit **[Parsers](./parsers.md)**. +- Binde wiederverwendbare Befehlssätze über **[Plugin](./plugin.md)** ein. + diff --git a/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/config.md b/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/config.md index 69714eb..7bfecdf 100644 --- a/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/config.md +++ b/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/config.md @@ -1,105 +1,160 @@ --- sidebar_position: 7 -description: Konfigurationsdatei-Unterstützung für quiCkLI-Anwendungen mit TOML. -keywords: [quickli, config, toml, konfiguration, schema, validieren] +description: Native TOML-Konfigurationsdateiverwaltung, Validierung und Auto-Initialisierung in quiCkLI. +keywords: [quickli, config, toml, schema, ConfigField, ConfigSchema, add_auto_init_config, validate_config, generate_schema_json] --- # Konfigurationsdateien -quickli bietet native Unterstützung für TOML-Konfigurationsdateien über das Modul `config`. -Konfigurationsdateien liegen außerhalb der Befehlshierarchie und werden von der Anwendung -geladen, bevor Befehle ausgeführt werden. +`quickli` bietet native TOML-Konfigurationsdateiverwaltung über sein `config`-Modul. Konfigurationsdateien liegen außerhalb des Befehls-Token-Streams und ermöglichen es Anwendungen, dauerhafte, schema-validierte Benutzereinstellungen beim Anwendungsstart zu laden. ``` Application +├── global_options ├── Command -│ ├── Argument -│ └── Option -└── Config ← du bist hier (beim Start geladen, nicht Teil des Befehlsbaums) +└── Config ← du bist hier (beim Start geladen, dauerhaft auf der Festplatte) ``` -## Ressourcen +## Kernressourcen & API-Übersicht -- `ConfigField` — beschreibt einen erwarteten Schlüssel mit Typ, optionalem Standardwert und Validatoren. -- `ConfigSchema` — eine Sammlung von `ConfigField`-Objekten, die die erwartete Struktur definiert. -- `Config` — verwaltet das Lesen und Schreiben einer TOML-Datei an einem angegebenen Pfad. -- `ConfigIssue` — ein strukturiertes Ergebnis, das von `validate_config` zurückgegeben wird. -- `add_auto_init_config` — erstellt beim ersten Start eine Standarddatei und lädt sie bei späteren Starts. -- `validate_config` — gibt alle Probleme einer geladenen Konfiguration zurück, ohne eine Ausnahme auszulösen. +| Ressource / Funktion | Beschreibung | +|---|---| +| `ConfigField` | Beschreibt einen erwarteten Schlüssel mit Typ, Standardwert, Erforderlich-Status, Validatoren und Beschreibung. | +| `ConfigSchema` | Container für eine Liste von `ConfigField`-Definitionen. | +| `Config` | Klasse zum Lesen (`load()`), Schreiben (`save()`) und Schema-Binden einer TOML-Datei an einem angegebenen `Path`. | +| `ConfigIssue` | Datenobjekt für Validierungsergebnisse (`severity`, `field`, `message`). | +| `add_auto_init_config` | Hilfsfunktion, die die TOML-Datei beim ersten Start mit Standardwerten anlegt und bei späteren Starts lädt. | +| `validate_config` | Validiert eine `Config`-Instanz ohne Ausnahmen auszulösen und gibt eine Liste von `ConfigIssue`-Befunden zurück. | +| `generate_schema_json` | Exportiert eine JSON-Schema-Wörterbuchdarstellung eines `ConfigSchema`. | -## Ein Schema definieren +## Definieren eines Konfigurationsschemas + +Ein `ConfigSchema` definiert die erwartete Struktur einer TOML-Datei: ```python from quickli import ConfigField, ConfigSchema +from quickli import positive_number, number_range schema = ConfigSchema( fields=[ - ConfigField("host", value_type=str, required=False, default="localhost"), - ConfigField("port", value_type=int, required=False, default=8080), + ConfigField("host", value_type=str, default="127.0.0.1", description="Server-Bind-Host."), + ConfigField("port", value_type=int, default=8080, validators=[number_range(1024, 65535)], description="Portnummer."), + ConfigField("timeout", value_type=float, default=30.0, validators=[positive_number], description="Timeout in Sekunden."), + ConfigField("debug", value_type=bool, default=False, description="Debug-Logging aktivieren."), ] ) ``` -## Laden und automatische Initialisierung +### ConfigField-Attribute + +| Parameter | Typ | Standard | Beschreibung | +|---|---|---|---| +| `name` | `str` | Erforderlich | Feldname in der TOML-Datei. | +| `value_type` | `type` | `str` | Erwarteter Python-Typ (`str`, `int`, `float`, `bool`, `list`, `dict`). | +| `required` | `bool` | `False` | Bei `True` schlägt die Validierung fehl, wenn das Feld in der Datei fehlt und keinen Standardwert hat. | +| `default` | `Any` | `None` | Standardwert, der bei Auto-Initialisierung geschrieben oder als Ersatz verwendet wird. | +| `validators` | `list[Callable]` | `None` | Prüffunktionen, die auf den Feldwert angewendet werden. | +| `description` | `str \| None` | `None` | Beschreibung des Feldes. | + +## Auto-Initialisierung und Laden (`add_auto_init_config`) + +`add_auto_init_config` ist das empfohlene Muster für CLI-Anwendungen: + +1. **Erster Start**: Existiert die Datei nicht, erstellt `add_auto_init_config` Ordner und Datei mit Standardwerten und gibt diese zurück. +2. **Spätere Starts**: Existiert die Datei, lädt und validiert `add_auto_init_config` die TOML-Datei. ```python from pathlib import Path -from quickli import Config, add_auto_init_config +from quickli import Application, Config, add_auto_init_config -config = Config(path=Path.home() / ".myapp" / "config.toml", schema=schema) -data = add_auto_init_config(config) -# First run: writes defaults to file, returns {"host": "localhost", "port": 8080} -# Later runs: loads and validates the existing file +config_path = Path.home() / ".config" / "myapp" / "config.toml" +config = Config(path=config_path, schema=schema) + +config_data = add_auto_init_config(config) + +app = Application(name="myapp") + +@app.command(help_text="Server starten.") +def start() -> str: + host = config_data.get("host", "127.0.0.1") + port = config_data.get("port", 8080) + return f"Server running on {host}:{port}" ``` -## Konfiguration validieren +:::info[Standardwerte bei Auto-Init] +Felder mit definierten `default`-Werten werden beim ersten Erstellen der Datei auf die Festplatte geschrieben. +::: + +## Überprüfen von Validierungsergebnissen (`validate_config`) + +Möchtest du Ergebnisse ohne Ausnahmen prüfen, rufe `validate_config(config)` auf: ```python from quickli import validate_config issues = validate_config(config) + for issue in issues: - print(f"[{issue.severity.upper()}] {issue.field}: {issue.message}") + if issue.severity == "error": + print(f"❌ [ERROR] Field '{issue.field}': {issue.message}") + elif issue.severity == "warning": + print(f"⚠️ [WARNING] Field '{issue.field}': {issue.message}") ``` -`validate_config` gibt Folgendes zurück: +`validate_config` unterscheidet: +- **`severity="error"`**: Fehlende erforderliche Felder, Typabweichungen oder Validierungsfehler. +- **`severity="warning"`**: Felder in der Datei, die nicht im `ConfigSchema` definiert sind. + +## Exportieren von JSON-Schema (`generate_schema_json`) -- **Fehler** bei fehlenden erforderlichen Feldern, Typabweichungen und fehlgeschlagenen Validatoren. -- **Warnungen** bei Feldern, die in der Datei vorhanden, aber im Schema nicht definiert sind. +`quickli` erlaubt den Export einer JSON-Schema-Darstellung aus einem `ConfigSchema`: + +```python +from quickli import generate_schema_json, render_json -## Fehlerbehandlung +schema_dict = generate_schema_json(schema) +json_str = render_json(schema_dict) +print(json_str) +``` -| Ausnahme | Wann sie ausgelöst wird | -| --- | --- | -| `ConfigError` | Datei fehlt, ist nicht lesbar oder enthält ungültige TOML-Syntax | -| `ConfigValidationError` | Erforderliches Feld fehlt oder Typ stimmt beim Aufruf von `load()` nicht | +## Fehlerbehandlung & Ausnahmen -Beide sind Unterklassen von `CLIError`. +| Ausnahme | Ursache | +|---|---| +| `ConfigError` | Wird ausgelöst, wenn die Datei fehlt (bei direktem `load()`), unlesbar ist oder ungültige TOML-Syntax enthält. | +| `ConfigValidationError` | Wird ausgelöst, wenn die Schemavalidierung bei `config.load()` fehlschlägt. | -## Formatunterstützung +```python +from quickli import Config, ConfigError, ConfigValidationError + +try: + data = config.load() +except ConfigValidationError as e: + print(f"Configuration invalid: {e}") +except ConfigError as e: + print(f"Failed to read configuration file: {e}") +``` -- **Lesen**: verwendet `tomllib` aus der Python-Standardbibliothek (Python 3.11+). -- **Schreiben**: verwendet einen integrierten Serialisierer für Skalare und verschachtelte Tabellen auf einer Ebene. -- `None`-Werte werden beim Schreiben stillschweigend übersprungen. +## Formatunterstützung & Serialisierung -## Tipps +- **Lesen**: Verwendet `tomllib` aus der Standardbibliothek (Python 3.11+). +- **Schreiben**: Unterstützt Skalare (`str`, `int`, `float`, `bool`), Listen und verschachtelte Tabellen auf einer Ebene (`dict`). +- `None`-Werte werden beim Schreiben übersprungen. -:::tip[Config vs. Option für persistente Einstellungen] -Verwende eine **Konfigurationsdatei** für Einstellungen, die Nutzer einmal setzen und -zwischen Ausführungen beibehalten möchten — zum Beispiel einen Standard-Serverhost oder -eine API-Basis-URL. Verwende eine **Befehlsoption** für Einstellungen, die sich pro -Ausführung ändern, etwa das Ausgabeformat oder einen einmaligen Zielpfad. +:::warning[Serialisierungsumfang] +Der integrierte TOML-Writer ist für skalare Felder und flache Tabellen ausgelegt. Für komplexe Strukturen verwende **[Parsers](./parsers.md)**. ::: -:::tip[Auto-Init beim ersten Start] -`add_auto_init_config` ist der empfohlene Weg, eine Konfigurationsdatei zu initialisieren. -Beim ersten Start schreibt es eine Datei mit allen Standardwerten, damit der Nutzer einen -konkreten Ausgangspunkt zum Bearbeiten hat. Bei jedem weiteren Start lädt und validiert es -die vorhandene Datei. +## Leitfaden: Config vs. Option + +:::tip[Entscheidung zwischen Config und Option] +- Verwende **`Config`** für permanente Einstellungen, die Benutzer einmal festlegen (z. B. API-Keys oder Standard-Server-URLs). +- Verwende **[Option](./option.md)** für ausführungsspezifische Flags und Overrides (z. B. `--verbose` oder `--dry-run`). ::: ## Wie geht es weiter? -- Siehe **[Application](./application.md)**, um zu erfahren, wie Config beim Start integriert wird. -- Siehe **[Parsers](./parsers.md)**, wenn du strukturierte Daten aus Befehlsargumenten statt aus - einer persistenten Datei lesen möchtest. +- Erfahre mehr über die Anwendungs-Integration in **[Application](./application.md)**. +- Überschreibe Konfigurationswerte dynamisch mit **[Option](./option.md)**. +- Verarbeite beliebige JSON-, YAML- oder TOML-Strings mit **[Parsers](./parsers.md)**. + diff --git a/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/option.md b/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/option.md index e912e5c..346f10c 100644 --- a/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/option.md +++ b/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/option.md @@ -1,151 +1,199 @@ --- sidebar_position: 5 -description: Option beschreibt eine benannte, reihenfolgeunabhängige Eingabe, die das Befehlsverhalten ändert. -keywords: [quickli, option, flag, benannt, global, lokal, wiederholbar] +description: Option beschreibt eine benannte, reihenfolgeunabhängige Eingabe, die das Befehls- oder Anwendungsverhalten ändert. +keywords: [quickli, option, flag, kurzalias, kombinierte flags, wiederholbar, globale optionen, lokale optionen, validatoren] --- # Option -`Option` beschreibt eine benannte Eingabe, die das Verhalten eines Befehls verändert. -Optionen gehören zu einem Befehl (lokale Optionen) oder zur `Application` selbst (globale -Optionen). +`Option` beschreibt eine benannte, reihenfolgeunabhängige Eingabe, die das Verhalten eines Befehls oder der gesamten Anwendung verändert. Optionen können **lokal** für einen bestimmten **[Command](./command.md)** oder **global** für die gesamte **[Application](./application.md)** deklariert werden. ``` Application -├── globale Optionen ← für jeden Befehl verfügbar +├── global_options ← für jeden Befehl verfügbar (z. B. --verbose, --config) └── Command - └── Option ← du bist hier (lokale Option) + └── Option ← lokale Option (du bist hier) ``` -## Unterstützte Formen +## Options-Eigenschaften -- lange Form: `--output value` -- lange Zuweisungsform: `--output=value` -- kurze Form: `-o value` +Beim Erstellen einer `Option` kannst du die folgenden Eigenschaften konfigurieren: -## Grundlegende Funktionen +| Eigenschaft | Typ | Standard | Beschreibung | +|---|---|---|---| +| `name` | `str` | Erforderlich | Optionsname, auf den über CLI-Flags wie `--output` und Handler-Parameter wie `output` zugegriffen wird. | +| `help_text` | `str \| None` | `None` | Menschlich lesbare Erklärung in der Hilfeausgabe. | +| `short_name` | `str \| None` | `None` | Einzelzeichen-Kurzalias (z. B. `"o"` für `-o`). | +| `required` | `bool` | `False` | Ob die Option vom Aufrufer explizit angegeben werden muss. | +| `default` | `Any` | `None` | Standardwert, wenn die Option weggelassen wird. (Standard für `is_flag=True` ist `False`). | +| `is_flag` | `bool` | `False` | Bei `True` wird die Option als boolescher Schalter ohne Wertargument behandelt. | +| `converter` | `Callable \| None` | `None` | Umwandlungs-Callable vor der Validierung. | +| `validators` | `list[Callable] \| None` | `None` | Prüffunktionen nach der Konvertierung. | +| `multiple` | `bool` | `False` | Bei `True` kann die Option auf der Kommandozeile wiederholt werden. | +| `metavar` | `str \| None` | `None` | Benutzerdefinierter Platzhaltertext in Usage-Zeilen und Hilfeseiten (z. B. `metavar="PATH"`). | -- Standardwerte -- erforderliche Optionen -- boolesche Schalter (`is_flag=True`) -- Konverter für Werte, die keine Schalter sind -- Validatoren für konvertierte Werte -- wiederholbare Werte (`multiple=True`) +## Unterstützte CLI-Syntaxformen -Wiederholbare Optionen, die keine Schalter sind, sammeln Werte in einer Liste. Wiederholbare -Schalter sammeln die Anzahl ihrer Vorkommen. +`quickli` unterstützt standardmäßige POSIX- und GNU-CLI-Optionssyntax out-of-the-box: -## Lokale und globale Optionen +- **Lange Option mit Leerzeichen**: `--output out.json` +- **Lange Option mit Gleichheitszeichen**: `--output=out.json` +- **Kurze Option mit Leerzeichen**: `-o out.json` +- **Kombinierte Kurzflags**: `-vxf` (äquivalent zu `-v -x -f`, wenn alle Kurzoptionen boolesche Flags sind) -Optionen können für Befehle (lokal) oder auf Anwendungsebene (global) definiert werden. -Globale Optionen können vor oder nach dem Befehlsnamen erscheinen. +## Boolesche Flags vs. Wertoptionen -## Einfaches Optionsbeispiel +### Beispiel für Wertoptionen ```python -from quickli import Application, Argument, Option +from quickli import Application, Option app = Application(name="demo") - @app.command( - help_text="Text in eine Datei schreiben.", - arguments=[Argument("text")], options=[Option("output", short_name="o", default="out.txt")], ) -def write(text: str, output: str = "out.txt") -> str: - return f"schreibe '{text}' nach {output}" - +def export_data(output: str = "out.txt") -> str: + return f"saving to {output}" -print(app.run(["write", "hello"])) # schreibe 'hello' nach out.txt -print(app.run(["write", "hello", "--output", "a.txt"])) # schreibe 'hello' nach a.txt +print(app.run(["export-data", "--output", "report.pdf"])) # saving to report.pdf +print(app.run(["export-data", "-o", "report.pdf"])) # saving to report.pdf +print(app.run(["export-data", "--output=report.pdf"])) # saving to report.pdf ``` -## Beispiel für einen booleschen Schalter +### Beispiel für boolesche Flags ```python -from quickli import Application, Option +@app.command( + options=[ + Option("verbose", short_name="v", is_flag=True, help_text="Debug-Modus aktivieren."), + ], +) +def status(verbose: bool = False) -> str: + return "Status: OK (debug enabled)" if verbose else "Status: OK" -app = Application(name="demo") +print(app.run(["status"])) # Status: OK +print(app.run(["status", "--verbose"])) # Status: OK (debug enabled) +print(app.run(["status", "-v"])) # Status: OK (debug enabled) +``` +:::info[Kombinierte Kurzflags] +Werden mehrere Einzelzeichen-Flags zusammen übergeben (z. B. `-abc`), trennt `quickli` diese in einzelne Flags (`-a`, `-b`, `-c`), vorausgesetzt, alle Kurzoptionen sind als Flags registriert (`is_flag=True`). +::: -@app.command( - help_text="Versionsinformationen ausgeben.", - options=[Option("verbose", short_name="v", is_flag=True)], -) -def version(verbose: bool = False) -> str: - if verbose: - return "demo version 1.0.0 (debug build)" - return "demo 1.0.0" +## Wiederholbare Optionen (`multiple=True`) +`Option` unterstützt zwei unterschiedliche Arten wiederholbaren Verhaltens: -print(app.run(["version"])) # demo 1.0.0 -print(app.run(["version", "--verbose"])) # demo version 1.0.0 (debug build) -print(app.run(["version", "-v"])) # demo version 1.0.0 (debug build) -``` +### 1. Wiederholbare Wertoptionen (Listen) -## Beispiel für eine wiederholbare Option +Ist `multiple=True` bei einer Wertoption (`is_flag=False`), sammeln wiederholte Aufrufe konvertierte Werte in einer Python-`list`: ```python -from quickli import Application, Option +@app.command( + options=[ + Option("tag", short_name="t", multiple=True, help_text="Tag-Label hinzufügen."), + ], +) +def tag_item(tag: list[str] | None = None) -> str: + tags = tag or [] + return f"Applied tags: {', '.join(tags)}" -app = Application(name="demo") +print(app.run(["tag-item", "--tag", "web", "-t", "v1.0", "--tag", "prod"])) +# Rückgabe: Applied tags: web, v1.0, prod +``` + +### 2. Wiederholbare Flag-Optionen (Anzahl) +Ist `multiple=True` bei einem booleschen Flag (`is_flag=True`), sammeln wiederholte Flags eine Ganzzahl der Vorkommen (z. B. `-v`, `-vv`, `-v -v -v`): +```python @app.command( - help_text="Eine Ressource taggen.", - options=[Option("tag", short_name="t", multiple=True)], + options=[ + Option("verbose", short_name="v", is_flag=True, multiple=True), + ], ) -def tag_resource(tag: list[str] | None = None) -> str: - labels = tag or [] - return f"tags: {', '.join(labels)}" - +def run_job(verbose: int = 0) -> str: + return f"Log verbosity level: {verbose}" -print(app.run(["tag-resource", "--tag", "a", "--tag", "b"])) # tags: a, b +print(app.run(["run-job", "-v"])) # Log verbosity level: 1 +print(app.run(["run-job", "-vvv"])) # Log verbosity level: 3 ``` -## Beispiel für eine globale Option +:::warning[Rückgabetypen wiederholbarer Flags] +Ein Standard-Flag (`is_flag=True`, `multiple=False`) gibt ein `bool` zurück. Ein wiederholbares Flag (`is_flag=True`, `multiple=True`) gibt ein `int` mit der Vorkommensanzahl zurück. +::: -```python -from quickli import Application, Option +## Globale und lokale Optionen +Optionen können als **lokal** für einen Befehl oder **global** für die Anwendung registriert werden: + +```python app = Application( name="demo", - global_options=[Option("verbose", short_name="v", is_flag=True)], + global_options=[ + Option("config", short_name="c", help_text="Pfad zur Konfigurationsdatei."), + Option("quiet", short_name="q", is_flag=True, help_text="Ausgabe unterdrücken."), + ], ) +@app.command( + options=[ + Option("format", short_name="f", default="json", help_text="Lokale Format-Option."), + ], +) +def process(format: str = "json", config: str | None = None, quiet: bool = False) -> str: + return f"format={format}, config={config}, quiet={quiet}" -@app.command(help_text="Projekt bauen.") -def build(verbose: bool = False) -> str: - return f"building… (verbose={verbose})" +# Globale Optionen können VOR oder NACH dem Befehlsnamen platziert werden: +print(app.run(["--config", "settings.toml", "process", "-f", "yaml"])) +print(app.run(["process", "-f", "yaml", "--config", "settings.toml", "-q"])) +``` +:::note[Platzierung globaler Optionen] +`quickli` filtert globale Optionen flexibel, unabhängig davon, ob der Benutzer sie vor dem Befehlsnamen (`mycli -q process`) oder nach dem Befehlsnamen (`mycli process -q`) platziert. +::: -print(app.run(["--verbose", "build"])) # building… (verbose=True) -print(app.run(["build", "--verbose"])) # building… (verbose=True) -``` +## Konverter und integrierte Validatoren -## Tipps +Optionen unterstützen Typkonvertierung und Validierung auf dieselbe Weise wie **[Argument](./argument.md)**: -:::note[Argument vs. Option] -Verwende ein `Argument` für das primäre Subjekt des Befehls (worauf er wirkt). Verwende -eine `Option` für alles, was *verändert, wie* der Befehl sich verhält — Ausgabeformat, -Ausführlichkeitsgrad, ein Schalter oder ein sekundäres Ziel. -::: +```python +from pathlib import Path +from quickli import Application, Option +from quickli import file_path, positive_number -:::tip[Lokale vs. globale Optionen] -Definiere eine Option als **lokal**, wenn sie nur für einen Befehl sinnvoll ist (wie -`--output` für einen Schreibbefehl). Definiere sie als **global**, wenn sie für jeden -Befehl in der Anwendung gelten soll (wie `--verbose` oder `--config`). -::: +app = Application(name="demo") -:::tip[Schalter für An/Aus-Umschalter] -Verwende `is_flag=True`, wenn die Option einen booleschen Schalter darstellt, der keinen -Wert annimmt. Das Vorhandensein des Flags setzt es auf `True`; sein Fehlen lässt es auf -`False`. -::: +@app.command( + options=[ + Option( + "file", + short_name="f", + converter=Path, + validators=[file_path], + metavar="PATH", + help_text="Eingabedateipfad.", + ), + Option( + "retries", + short_name="r", + converter=int, + validators=[positive_number], + default=3, + help_text="Wiederholungsanzahl.", + ), + ], +) +def sync(file: Path | None = None, retries: int = 3) -> str: + file_name = file.name if file else "none" + return f"Syncing {file_name} with max {retries} retries" +``` ## Wie geht es weiter? -- Verwende **[Arguments](./argument.md)** für positionelle, geordnete Eingaben. -- Geh zurück zu **[Command](./command.md)**, um zu sehen, wie Optionen und Argumente zusammenpassen. -- Siehe **[Application](./application.md)**, um zu erfahren, wie globale Optionen deklariert werden. +- Vergleiche Optionen mit positionalen Eingaben in **[Argument](./argument.md)**. +- Siehe, wie Optionen an Handler gebunden werden in **[Command](./command.md)**. +- Erfahre, wie anwendungsweite Optionen in **[Application](./application.md)** konfiguriert werden. +- Speichere permanente Einstellungen auf der Festplatte mit **[Config](./config.md)**. + diff --git a/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/parsers.md b/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/parsers.md index 1fa4883..336021e 100644 --- a/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/parsers.md +++ b/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/parsers.md @@ -1,83 +1,127 @@ --- sidebar_position: 8 -description: Helfer zum Parsen und Rendern von JSON, YAML und TOML in quiCkLI-CLIs. -keywords: [quickli, parser, json, yaml, toml, laden, rendern] +description: Hilfsfunktionen zur Serialisierung und Deserialisierung strukturierter Daten (JSON, YAML, TOML) in quiCkLI. +keywords: [quickli, parsers, json, yaml, toml, load_json, render_json, load_yaml, render_yaml, load_toml, render_toml] --- -# Parser +# Parsers -`quickli.parsers` bietet klar abgegrenzte Funktionen für strukturierte JSON-, YAML- und -TOML-Ein- und -Ausgabe. Parser sind Hilfsfunktionen außerhalb der Befehlshierarchie — du -kannst sie aus jedem Command-Handler aufrufen, der strukturierte Daten lesen oder erzeugen -muss. +`quickli.parsers` bietet Hilfsfunktionen zum Parsen und Rendern strukturierter Daten in den Formaten **JSON**, **YAML** und **TOML**. Parser sind zustandslose Hilfsfunktionen außerhalb der Befehlshierarchie — du kannst sie in **[Command](./command.md)**-Handlern, Konvertern für **[Argument](./argument.md#typ-konverter)** oder Datenverarbeitungspipelines aufrufen. ``` Application └── Command - └── handler() ← Parser-Helfer hier aufrufen - load_yaml / render_json / … + └── handler() ← Parser-Helfer in Handlern oder Konvertern aufrufen + load_json / render_yaml / load_toml / … ``` -## Öffentliche APIs +## Öffentliche API-Referenz -| Funktion | Beschreibung | -|---|---| -| `load_json(text)` | Einen JSON-String in ein Python-Objekt parsen | -| `render_json(value)` | Ein Python-Objekt in einen JSON-String serialisieren | -| `load_yaml(text)` | Einen YAML-String in ein Python-Objekt parsen | -| `render_yaml(value)` | Ein Python-Objekt in einen YAML-String serialisieren | -| `load_toml(text)` | Einen TOML-String in ein Python-Objekt parsen | -| `render_toml(value)` | Ein Python-Objekt in einen TOML-String serialisieren | +Alle Parser-Helfer werden direkt aus dem Hauptpaket `quickli` re-exportiert: -## Zwischen Formaten konvertieren +| Funktion | Signatur | Beschreibung | +|---|---|---| +| `load_json` | `(text: str) -> Any` | Parst einen JSON-String in Python-Dictionaries/Listen. | +| `render_json` | `(value: Any) -> str` | Serialisiert Python-Datenstrukturen in einen formatierten JSON-String. | +| `load_yaml` | `(text: str) -> Any` | Parst einen YAML-String in Python-Dictionaries/Listen. | +| `render_yaml` | `(value: Any) -> str` | Serialisiert Python-Datenstrukturen in einen formatierten YAML-String. | +| `load_toml` | `(text: str) -> Any` | Parst einen TOML-String in Python-Dictionaries/Listen. | +| `render_toml` | `(value: Any) -> str` | Serialisiert Python-Datenstrukturen in einen formatierten TOML-String. | + +:::info[Top-Level Modul-Export] +Du kannst alle Parser-Helfer direkt aus `quickli` importieren (z. B. `from quickli import load_json, render_yaml`). +::: + +## Beispiel 1: Format-Konvertierungs-Befehl (YAML zu JSON) + +Kombiniere `load_yaml` und `render_json`, um Konvertierungstools zu bauen: ```python -from quickli import load_yaml, render_json +from pathlib import Path +from quickli import Application, Argument, load_yaml, render_json + +app = Application(name="yaml2json") -data = load_yaml("kind: Pod\nmetadata:\n name: web-preview\n") -print(render_json(data)) +@app.command( + help_text="Eine YAML-Datei in formatiertes JSON umwandeln.", + arguments=[Argument("path", converter=Path)], +) +def convert(path: Path) -> str: + raw_yaml = path.read_text(encoding="utf-8") + data = load_yaml(raw_yaml) + return render_json(data) ``` -## Strukturierte Eingabe aus einem Argument lesen +## Beispiel 2: Argument-Konvertierung mit `load_json` + +Verwende `load_json` als Konverter für `Argument` oder `Option`, um JSON-Strings auf der CLI zu parsen: ```python -from pathlib import Path from quickli import Application, Argument, load_json app = Application(name="demo") - @app.command( - help_text="Eine JSON-Datei zusammenfassen.", - arguments=[Argument("path", converter=Path)], + help_text="JSON-Payload prüfen.", + arguments=[ + Argument("payload", converter=load_json, help_text="Roher JSON-String."), + ], ) -def summarise(path: Path) -> str: - data = load_json(path.read_text()) - return f"{len(data)} Schlüssel auf oberster Ebene" +def inspect_payload(payload: dict) -> str: + keys = ", ".join(payload.keys()) + return f"Payload contains {len(payload)} keys: {keys}" - -print(app.run(["summarise", "data.json"])) +print(app.run(["inspect-payload", '{"name": "Alice", "role": "admin"}'])) ``` -## Tipps +## Beispiel 3: Ausgaben basierend auf Optionen formatieren + +Verwende Parser-Renderer, um Handler-Antworten dynamisch basierend auf einer `--format`-Option zu formatieren: + +```python +from quickli import Application, Option +from quickli import render_json, render_yaml, render_toml + +app = Application(name="demo") + +@app.command( + options=[ + Option("format", short_name="f", default="json", help_text="Ausgabeformat (json, yaml, toml)."), + ], +) +def info(format: str = "json") -> str: + data = { + "app": "demo", + "status": "healthy", + "metrics": {"cpu": 12.5, "memory_mb": 256}, + } + if format == "yaml": + return render_yaml(data) + elif format == "toml": + return render_toml(data) + return render_json(data) + +print(app.run(["info", "-f", "yaml"])) +``` -:::tip[Welches Format wählen] -- Verwende **JSON** für maschinelle Kommunikation und API-Antworten. -- Verwende **YAML** für manuell bearbeitete Konfigurationen und Kubernetes-ähnliche Manifeste. -- Verwende **TOML** für endnutzerorientierte Konfigurationsdateien (siehe [Konfigurationsdateien](./config.md)). +## Leitfaden zur Formatauswahl -Alle drei Helfer sind über den Top-Level-Import `quickli` verfügbar, du musst -`quickli.parsers` nicht direkt importieren. +:::tip[Wahl des richtigen Formats] +- **JSON**: Optimal für Maschinen-zu-Maschinen-Kommunikation und API-Payloads. +- **YAML**: Optimal für menschlich lesbare Konfigurationen und Kubernetes-Manifeste. +- **TOML**: Optimal für menschlich bearbeitbare Anwendungskonfigurationsdateien. ::: -:::tip[Parser vs. Config] -`load_toml` / `render_toml` sind nützlich für einmaliges Parsen von TOML-Strings oder -Dateien, die du selbst verwaltest. Für persistente Anwendungskonfiguration mit -Schema-Validierung und Auto-Initialisierung verwende stattdessen die dedizierten -[Config](./config.md)-Ressourcen. +## Parsers vs. Konfigurationsmodul + +:::note[Parsers vs. Config] +- Verwende **`parsers`**-Helfer für einmaliges Parsen oder Rendern beliebiger JSON/YAML/TOML-Strings oder Dateien innerhalb der Befehlslogik. +- Verwende **[Config](./config.md)**, wenn du eine dauerhafte, schema-validierte TOML-Konfigurationsdatei auf der Festplatte benötigst. ::: ## Wie geht es weiter? -- Siehe **[Konfigurationsdateien](./config.md)** für persistente, schema-validierte Konfiguration. -- Geh zurück zu **[Command](./command.md)**, um zu sehen, wie Parser-Helfer in einen Handler eingebunden werden. +- Erfahre mehr über Konverter in positionalen Eingaben in **[Argument](./argument.md)**. +- Erfahre mehr über CLI-Optionen in **[Option](./option.md)**. +- Siehe, wie dauerhafte TOML-Konfiguration funktioniert in **[Config](./config.md)**. + diff --git a/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/plugin.md b/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/plugin.md index 2d3e96e..38c561f 100644 --- a/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/plugin.md +++ b/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/plugin.md @@ -1,115 +1,131 @@ --- sidebar_position: 6 -description: Plugins erweitern eine quiCkLI-Anwendung mit wiederverwendbaren Befehlssets. -keywords: [quickli, plugin, erweiterung, register, load_plugin] +description: Modulare Plugin-Architektur zur Erweiterung von quiCkLI-Anwendungen mit wiederverwendbaren Befehlssätzen. +keywords: [quickli, plugin, erweiterung, register, load_plugin, PluginLoadError, modulare befehle] --- # Plugin -Plugins erweitern eine `quickli`-Anwendung, ohne das Kernpaket zu verändern. -Jedes Plugin registriert seine Befehle und Ressourcen anhand eines klar definierten Vertrags -bei einer `Application`-Instanz. +Plugins bieten einen Erweiterungsmechanismus für `quickli`-Anwendungen, ohne den Anwendungscode im Kern zu verändern. Jedes Plugin kapselt eine Reihe von **[Command](./command.md)**-Handlern und -Ressourcen und registriert diese über einen strikten Vertrag bei einer **[Application](./application.md)**-Instanz. -Plugins befinden sich in der Hierarchie auf derselben Ebene wie reguläre Befehle: Sie -hängen neue Befehle von außen an eine bestehende `Application` an. +Plugins befinden sich in der Architektur auf derselben Ebene wie reguläre Anwendungsbefehle: ``` Application -├── Command (direkt registriert) +├── Command (direkt von der Anwendung registriert) └── Plugin ← du bist hier - └── Command (vom Plugin registriert) + └── Command (von plugin.register() registriert) ``` -## Plugin-Vertrag +## Der Plugin-Schnittstellenvertrag -Jedes Plugin muss `quickli.Plugin` unterklassen und drei Elemente implementieren: +Jedes Plugin muss `quickli.Plugin` unterklassen und drei erforderliche abstrakte Elemente implementieren: -| Element | Art | Erforderlich | Beschreibung | -|---|---|---|---| -| `name` | `str`-Property | ja | Eindeutiger, nicht leerer Plugin-Bezeichner | -| `description` | `str`-Property | ja | Kurze Beschreibung der bereitgestellten Funktionen | -| `register(application)` | Methode | ja | Registriert Befehle und Ressourcen bei der Anwendung | +| Element | Art | Beschreibung | +|---|---|---| +| `name` | `@property -> str` | Eindeutiger, nicht leerer String-Bezeichner für das Plugin (z. B. `"security-audit"`). | +| `description` | `@property -> str` | Kurze Beschreibung der vom Plugin bereitgestellten Funktionen. | +| `register(application)` | `method(Application) -> None` | Hook-Methode, in der das Plugin Befehle, Unterbefehle oder Ressourcen an der `Application` registriert. | + +### Vollständiges Plugin-Beispiel ```python import quickli - -class VersionPlugin(quickli.Plugin): +class DatabasePlugin(quickli.Plugin): @property def name(self) -> str: - return "version-plugin" + return "database" @property def description(self) -> str: - return "Adds a version command." + return "Bietet Datenbankmigrations- und Seeding-Befehle." def register(self, application: quickli.Application) -> None: - @application.command(help_text="Prints the application version.") - def version() -> str: - return "1.0.0" + @application.command( + name="db-migrate", + help_text="Datenbankschemamigrationen ausführen.", + ) + def migrate() -> str: + return "migrations applied" + + @application.command( + name="db-seed", + help_text="Datenbank mit Beispieldaten befüllen.", + ) + def seed() -> str: + return "database seeded" ``` -## Ein Plugin laden +## Laden von Plugins (`Application.load_plugin`) -Rufe `Application.load_plugin(plugin)` auf, um ein Plugin in deine Anwendung zu laden. +Um ein Plugin zu registrieren, übergib eine Instanz deiner Plugin-Klasse an `Application.load_plugin()`: ```python -app = quickli.Application(name="demo") -app.load_plugin(VersionPlugin()) -print(app.run(["version"])) # 1.0.0 -``` +app = quickli.Application(name="mycli") -`load_plugin` validiert den Plugin-Namen, verhindert doppelte Ladevorgänge und ruft -`plugin.register(application)` auf, damit das Plugin seine Befehle registrieren kann. +# Datenbank-Plugin laden: +app.load_plugin(DatabasePlugin()) -## Geladene Plugins anzeigen +# Vom Plugin bereitgestellte Befehle ausführen: +print(app.run(["db-migrate"])) # migrations applied +print(app.run(["db-seed"])) # database seeded +``` -`Application.plugins` gibt eine Kopie der Liste geladener Plugins zurück. +## Geladene Plugins anzeigen (`app.plugins`) + +Du kannst alle geladenen Plugins über `app.plugins` abfragen: ```python for plugin in app.plugins: - print(f"{plugin.name}: {plugin.description}") + print(f"Plugin: {plugin.name} — {plugin.description}") ``` -## Fehlerbehandlung +## Fehlerbehandlung (`PluginLoadError`) -`quickli.PluginLoadError` wird ausgelöst, wenn: +`quickli` erzwingt die Gültigkeit von Plugins und eindeutige Namen bei der Registrierung. Das Laden löst einen `PluginLoadError` aus, wenn: -- der Plugin-Name leer ist, -- bereits ein Plugin mit demselben Namen geladen wurde, -- oder die `register`-Methode des Plugins eine Ausnahme auslöst. +1. **Leerer Name**: Die `name`-Property gibt einen leeren String zurück. +2. **Doppelter Name**: Ein Plugin mit demselben `name` ist bereits an der `Application` registriert. +3. **Registrierungsausnahme**: Die `register(application)`-Methode des Plugins löst eine unbehandelte Ausnahme aus. ```python +from quickli import Application, PluginLoadError + +app = Application(name="demo") +db_plugin = DatabasePlugin() + +app.load_plugin(db_plugin) + try: - app.load_plugin(VersionPlugin()) -except quickli.PluginLoadError as error: - print(f"Failed to load plugin: {error}") + # Das zweimalige Laden desselben Plugins löst PluginLoadError aus: + app.load_plugin(db_plugin) +except PluginLoadError as err: + print(f"Failed to load plugin: {err}") ``` -## Tipps - -:::tip[Wann ein Plugin verwenden] -Verwende ein Plugin, wenn du einen wiederverwendbaren Befehlssatz als separates Python-Modul -oder -Paket verpacken möchtest. Ein gemeinsames `audit`-Plugin kann z. B. in jede Team-CLI -geladen werden, ohne Code zu kopieren. Für kleine, anwendungsspezifische Befehle verwende -einfach direkt `@app.command`. +:::info[Eindeutigkeit von Plugins] +Jedes Plugin muss einen eindeutigen `name`-String zurückgeben, um Kollisionen zwischen Plugin-Paketen zu vermeiden. ::: +## Reglementierung & Einschränkungen + :::warning[Plugins können keine bestehenden Befehle überschreiben] -Ein Plugin kann keinen Befehl ersetzen, der bereits registriert wurde — weder von der -Anwendung selbst noch von einem früheren Plugin. Entwirf deine Plugins so, dass sie neue -Befehle hinzufügen und keine bestehenden ersetzen. +Ein Plugin kann keinen Befehl ersetzen, der bereits an der `Application` registriert wurde (weder von der Anwendung selbst noch von einem zuvor geladenen Plugin). Der Versuch, einen doppelten Befehlsnamen zu registrieren, löst einen `CommandRegistrationError` aus. ::: -## Aktueller Status +:::tip[Modulare Anwendungsarchitektur] +Verwende Plugins, um große CLI-Anwendungen in unabhängige, wiederverwendbare Module aufzuteilen. So kann beispielsweise ein gemeinsames `auth`- oder `telemetry`-Plugin teamübergreifend verwendet werden. +::: + +## Aktueller Status & Roadmap + +- **Aktuell (Alpha)**: Explizite Plugin-Instanziierung und Laden über `app.load_plugin(PluginInstance())`. +- **Geplant (Zukunft)**: Automatische Plugin-Erkennung über Python-Paketeigenschaften (`importlib.metadata`). -Das Plugin-System ist im Alpha-Release mit explizitem Laden über -`Application.load_plugin()` implementiert. -Die automatische Plugin-Erkennung über Paketmetadaten (Entry Points von `importlib.metadata`) -ist für ein zukünftiges Release geplant. +## Wie geht es weiter? -## Referenz +- Erfahre mehr über Befehle in **[Command](./command.md)**. +- Erfahre, wie die Befehlsausführung funktioniert in **[Application](./application.md)**. +- Lade dauerhafte Konfigurationen in Plugins mit **[Config](./config.md)**. -Siehe die [Plugin-Spezifikation](https://github.com/spmse/quickli/blob/main/packages/core/specs/plugin.md) -und [ADR 0002](https://github.com/spmse/quickli/blob/main/packages/core/docs/adr/0002-plugin-api-design.md) -für die vollständige Begründung des Designs. diff --git a/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/quickli-concepts.md b/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/quickli-concepts.md index 38fde6e..e68bf72 100644 --- a/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/quickli-concepts.md +++ b/packages/quickli-docs/i18n/de/docusaurus-plugin-content-docs/current/concepts/quickli-concepts.md @@ -1,64 +1,71 @@ --- sidebar_position: 1 -description: Überblick über die Kernkonzepte von quiCkLI und wie sie zusammenhängen. -keywords: [quickli, konzepte, application, command, argument, option, plugin, hierarchie] +description: Umfassende Übersicht über die Kernkonzepte von quiCkLI und deren Zusammenspiel. +keywords: [quickli, konzepte, application, command, argument, option, config, parsers, plugin, hierarchie] --- -# quiCkLI-Konzepte +# quiCkLI Konzepte -`quickli` ist aus einer kleinen Menge expliziter Konzepte aufgebaut. Jedes Konzept hat -seine eigene Dokumentationsseite, damit das Modell weiterentwickelt werden kann, ohne ein -einziges Dokument zu überfüllen. - -## Konzeptseiten - -- [Application](./application.md) -- [Command](./command.md) -- [Argument](./argument.md) -- [Option](./option.md) -- [Parsers](./parsers.md) -- [Plugin](./plugin.md) -- [Konfigurationsdateien](./config.md) - -## Wie die Konzepte zusammenpassen - -Jede `quickli`-CLI hat eine einzige `Application` als Wurzel. Befehle leben innerhalb der -Anwendung. Jeder Befehl besitzt seine Argumente und Optionen. Plugins hängen zusätzliche -Befehle an eine bestehende Anwendung an, ohne den Kern zu ändern. +`quickli` basiert auf einer kleinen Gruppe expliziter, modularer Konzepte. Jedes Konzept repräsentiert einen fokussierten Baustein in deiner CLI-Architektur — ohne spekulative Abstraktionen oder versteckte Magie. ``` Application -├── globale Optionen ← benannte Flags/Werte, die jedem Befehl zur Verfügung stehen +├── global_options ← anwendungsweite Flags/Werte ├── Command "build" -│ ├── Argument "target" ← erforderliche positionelle Eingabe -│ └── Option "--output" ← optionale benannte Eingabe -├── Command "deploy" -│ ├── Subcommand "dev" -│ └── Subcommand "prod" -└── Plugin "metrics" - └── Command "stats" +│ ├── Argument "target" ← erforderliche positionale Eingabe +│ └── Option "--output" ← benanntes lokales Flag oder Einstellung +├── Command "env" +│ └── Subcommand "create" ← verschachtelte Aktion unter einem Elternbefehl +├── Plugin "metrics" +│ └── Command "stats" ← externe Befehle, die über das Plugin-Interface eingebunden werden +├── Config ← permanente TOML-Konfiguration, die beim Start geladen wird +└── Parsers ← Format-Helfer (JSON, YAML, TOML) innerhalb von Handlern ``` -In einem typischen Ablauf: +## Kernkonzepthierarchie + +Jede `quickli`-CLI besitzt eine einzelne **[Application](./application.md)**-Instanz an der Wurzel. + +- **[Application](./application.md)** verwaltet die Befehlsregistrierung, verarbeitet globale Optionen, leitet Token an Befehle weiter und steuert das Ausführungsmodell (`run()` vs. `main()`). +- **[Command](./command.md)** kapselt eine einzelne CLI-Operation. Befehle definieren positionale **[Argument](./argument.md)**-Ressourcen und benannte **[Option](./option.md)**-Ressourcen und können verschachtelte **[Subcommand](./command.md#verschachtelte-subcommands)**-Bäume enthalten. +- **[Argument](./argument.md)** repräsentiert erforderliche oder optionale positionale Eingaben in fester Reihenfolge. +- **[Option](./option.md)** repräsentiert benannte, reihenfolgeunabhängige Eingaben (wie `--verbose` oder `--output=file.txt`). Optionen können **lokal** für einen Befehl oder **global** für die gesamte Anwendung sein. +- **[Config](./config.md)** verwaltet dauerhafte TOML-Einstellungen auf der Festplatte und validiert diese beim Start mithilfe von `ConfigSchema`. +- **[Parsers](./parsers.md)** bieten Hilfsfunktionen (`load_json`, `render_yaml`, `load_toml` etc.) zum Parsen und Formatieren strukturierter Daten. +- **[Plugin](./plugin.md)** ermöglicht es externen Paketen oder Modulen, neue Befehle an eine `Application` anzuhängen, ohne den Kerncode zu verändern. -1. Eine `Application` erstellen. -2. Einen oder mehrere `Command`-Handler registrieren (oder einen einzelnen Root-`entrypoint`). -3. Die `Argument`- und `Option`-Ressourcen jedes Befehls definieren. -4. Die Anwendung mit Kommandozeilen-Token (`argv`) ausführen. +## Wann welches Konzept verwendet werden sollte -## Wann welches Konzept verwenden +| Ziel / Anforderung | Empfohlenes Konzept | Konzeptseite | +|---|---|---| +| CLI-Tool mit einer einzelnen Aktion (wie `cat` oder `head`) | `Application` + `@app.entrypoint` | [Application](./application.md) | +| CLI-Tool mit mehreren Aktionen (wie `git` oder `kubectl`) | `Application` + `@app.command` | [Application](./application.md) / [Command](./command.md) | +| Positionale Eingaben (z. B. Dateipfade) akzeptieren | `Argument` | [Argument](./argument.md) | +| Benannte Flags, Schalter oder Schlüssel-Wert-Einstellungen akzeptieren | `Option` | [Option](./option.md) | +| Flags definieren, die für *alle* Befehle gelten | Globale `Option` an der `Application` | [Option](./option.md#globale-und-lokale-optionen) | +| Benutzereinstellungen in einer TOML-Datei dauerhaft speichern | `Config` + `ConfigSchema` | [Config](./config.md) | +| JSON, YAML oder TOML-Daten innerhalb von Befehlen verarbeiten | `parsers`-Helfer | [Parsers](./parsers.md) | +| Wiederverwendbare Befehlssätze in Paketen auslagern | `Plugin` | [Plugin](./plugin.md) | -| Du möchtest… | Verwende… | -|---|---| -| Ein Tool mit einer einzigen Aktion bauen | `Application` + `@app.entrypoint` | -| Ein Tool mit mehreren Aktionen bauen | `Application` + `@app.command` | -| Positionelle Eingaben wie einen Dateipfad annehmen | `Argument` | -| Ein benanntes Flag oder eine Einstellung annehmen | `Option` | -| Verhalten aus einem separaten Paket hinzufügen | `Plugin` | -| Benutzereinstellungen zwischen Ausführungen speichern | `Config` | -| Strukturierte Daten als JSON, YAML oder TOML lesen/schreiben | `parsers`-Helfer | +:::info[Explizite Architektur] +`quickli` vermeidet versteckte Magie und implizite Annahmen. Ressourcendefinitionen (`Argument`, `Option`, `ConfigField`) deklarieren Typen, Standardwerte, Konverter und Validatoren explizit, sodass Hilfetexte, Parsing und Signaturbindung vollständig deterministisch bleiben. +::: + +:::tip[Trennung der Ausführungsmodelle] +Beim Ausführen einer CLI führt `Application.run()` den ausgewählten Befehl aus und gibt dessen String-Ergebnis zurück, ohne Ausgaben zu drucken oder `sys.exit()` aufzurufen. Das macht Unit-Tests denkbar einfach. Für ausführbare Programme ergänzt `Application.main()` `run()` um Standardausgabe-Formatierung, strukturierte Fehlerbehandlung und Prozess-Exit-Codes. Siehe die **[Application](./application.md#ausführungsmodelle-run-vs-main)**-Dokumentation für Details. +::: -:::note[Nicht sicher, wo du anfangen sollst?] -Lies zuerst [Erste Schritte](../getting-started.md) und schau dann hier nach, um die -einzelnen Konzepte im Detail zu erkunden. +:::note[Wo anfangen?] +Wenn du neu bei `quickli` bist, beginne mit der Anleitung **[Erste Schritte](../getting-started.md)**, um deine erste CLI zu bauen, und kehre dann hierher zurück, um die einzelnen Konzepte im Detail zu erkunden. ::: + +## Konzeptindex + +- **[Application](./application.md)** — Wurzel-CLI-Container, Registrierungs-APIs, globale Optionen und Ausführungsmodelle. +- **[Command](./command.md)** — Befehlserstellung, Docstrings, Subcommands, Signaturbindung und Validierung. +- **[Argument](./argument.md)** — Positionale Eingaben, Konverter, integrierte Validatoren und Metavar-Formatierung. +- **[Option](./option.md)** — Kurz-/Langoptionen, Boolesche Flags, wiederholbare Flags/Werte, lokaler vs. globaler Scope. +- **[Config](./config.md)** — Native TOML-Konfigurationsdateien, Feldvalidierung, Auto-Initialisierung und Schema-JSON. +- **[Parsers](./parsers.md)** — Format-Helfer für JSON-, YAML- und TOML-Serialisierung und -Deserialisierung. +- **[Plugin](./plugin.md)** — Plugin-Vertrag, Registrierungslebenszyklus, Duplikatschutz und Fehlerbehandlung. +