Application
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
├── global_options
├── Command / Subcommand
├── Entrypoint (fallback / single-action)
└── Plugin
What Application Owns
- Command Registry: Manages registered Command objects and prevents duplicate command names.
- Root Entrypoint: Supports single-action CLIs via
@app.entrypoint. - Global Options: Defines application-wide Option flags/values available to all commands.
- Command Dispatch: Parses
argvtokens, 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, andpowershell. - Execution Lifecycles: Provides
run()for library/testing use andmain()for executable CLIs.
Application Construction Parameters
When instantiating Application, you can customize several core behaviors:
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 | Required | The application name used in help and usage lines. |
description | str | "" | Short description shown in application help. |
global_options | list[Option] | None | None | List of global Option definitions available to all commands. |
shell_completion | bool | False | When True, automatically registers a built-in shell-completion command. |
auto_sys_argv | bool | True | run() reads sys.argv[1:] when its argument is omitted. Explicit None or [] means no tokens. |
error_handler | Callable | None | None | Optional error callback that can inspect or adapt exceptions before main() renders them. |
config | Config | None | None | Keyword-only configuration carrying a custom schema, path, and format. |
auto_init_config | bool | False | Keyword-only switch registering a built-in init command; requires config. |
auto_help | bool | True | Keyword-only switch enabling automatic standalone --help and -h handling. |
Command Registration Methods
Application supports four distinct ways to author and register commands:
1. Decorator Registration (@app.command)
The most common way to register named commands in multi-command tools.
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
2. Imperative Command Registration (app.register)
Useful when handler functions live in separate modules. Register the callable with explicit
resources; register() returns the original function, just like the decorator.
from quickli import Application, Argument
app = Application(name="demo")
def build(target: str) -> str:
return f"building {target}"
app.register(
build,
name="build",
help_text="Build the target artefact.",
arguments=[Argument("target")],
)
assert app.run(["build", "wheel"]) == "building wheel"
3. Root Entrypoint Registration (@app.entrypoint)
Used for single-action tools (like cat or head) that do not require command names.
from quickli import Application, Argument
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"]))
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)
Used to attach external command modules via the Plugin contract.
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())
Global Options
Global options apply across the entire application and can be passed before or after the command name:
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"]))
Global options are passed as keyword arguments to every selected handler. Accept their
destination names explicitly or use **kwargs; otherwise execution fails during signature
binding. See Option.
Execution Models: run() vs main()
Application explicitly separates library-level command execution from executable entrypoints:
┌──────────────────────┐
│ 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)
Runs command dispatch and returns the handler result unchanged, or generated help text.
- Arguments: Omit
argvto readsys.argv[1:]whenauto_sys_argv=True. ExplicitNoneor[]means an empty token list. - I/O: Does not itself print or terminate the process; handlers can have side effects.
- Return value: The handler's original object, not necessarily a string.
# Pure string execution — ideal for unit tests:
output = app.run(["greet", "Alice"])
assert output == "Hello, Alice!"
Application.main(argv=None, output_format="text")
Provides an executable wrapper suitable for CLI binaries (if __name__ == "__main__":).
- Output handling: Prints successful results directly to standard output.
- Exit codes: Returns
0on 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
UserCodeErrororInternalCLIErrorinstances.
if __name__ == "__main__":
raise SystemExit(app.main())
JSON Output Format Example
app.main(argv=["greet", "Alice"], output_format="json")
# Outputs JSON payload:
# {"ok": true, "result": "Hello, Alice!"}
Errors use {"ok": false, "error": {...}} on stderr and a nonzero exit code.
Success goes to stdout. Return Python dictionaries from handlers for structured JSON results;
returning a pre-serialized JSON string will encode it as a string inside result.
Automatic help
Every application supports standalone --help and -h flags by default, without defining
an Option or accepting a help parameter in handlers:
from quickli import Application, Argument, Subcommand
app = Application("demo")
@app.command(
help_text="Manage environments.",
subcommands=[
Subcommand(
"show", handler=lambda name: name,
arguments=[Argument("name")], help_text="Show one environment.",
),
],
)
def env() -> str:
return "Choose a subcommand."
print(app.run(["--help"])) # Application help
print(app.run(["env", "-h"])) # Group help, including subcommands
print(app.run(["env", "show", "--help"])) # Usage: demo env show NAME
- Use
mytool --helpormytool -hfor application help. - Use
mytool command --helpfor a command's arguments, options, and description. - Use
mytool group subcommand -hto inspect a nested operation.
Help bypasses required arguments/options, converters, validators, and handlers.
For example, mytool init --help does not create configuration files.
run() returns help text; main() prints it on stdout and returns exit code 0.
With output_format="json", help uses {"ok": true, "result": "Usage: ..."}.
- Tokens after
--are positional:mytool echo -- --helppasses the literal--help. - Option values remain data:
mytool echo --label --helpsupplies--helpas the label. - Automatic help recognizes exact standalone flags, not
-vhclusters or--help=true. - Explicit global or selected-command options claiming
--helpor-hkeep their behavior. For example,Option("host", short_name="h")keeps-has a value option; the unclaimed--helpalias still displays help.
Set Application("demo", auto_help=False) to disable automatic help handling and its
Help Options listings. Automatic help does not reserve a help command name, so existing
commands with that name keep working.
app.render_help() remains available for programmatic use. Without an entrypoint,
app.run([]) returns it too; with an entrypoint, an empty token list still invokes that
handler and may fail if required arguments are missing. The lower-level
Command.execute() does not handle application help flags; use Command.render_help()
when executing commands outside an Application.
Configuration initialization
Attach a Config to opt into initialization without changing ordinary startup behavior:
from quickli import Application, Config, ConfigField, ConfigSchema
schema = ConfigSchema(fields=[ConfigField("host", default="localhost")])
app = Application(
"demo",
config=Config("settings.yaml", schema=schema),
auto_init_config=True,
)
data = app.run(["init"])
By default, no init command is added. Call app.init_config() explicitly instead.
Neither constructing the application nor rendering help creates files.
See Configuration Files
for JSON/YAML structures, existing-file behavior, and custom initialization.
Built-in Shell Completion
When constructed with shell_completion=True, the Application automatically registers a shell-completion command:
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")
Users can generate shell completion scripts directly from the CLI:
mycli shell-completion bash > /etc/bash_completion.d/mycli
The standalone completion generators generate_bash_completion, generate_zsh_completion, and generate_powershell_completion are also available from top-level quickli.
Custom Error Handlers
You can pass a custom error_handler callback to Application to process or format exceptions before main() outputs them:
import logging
from quickli import CLIError
logger = logging.getLogger("demo")
def log_error(error: CLIError) -> None:
logger.error("%s", error)
app = Application(name="demo", error_handler=log_error)
Returning None preserves the error. Return a CLIError to replace it, or a string to
replace its displayed message while preserving its code and exit status.
Configure logging at the executable boundary, not in reusable modules.
The logging guide shows configuration and stderr handling.
