Getting Started with quiCkLI: Reading Files and Validating Input
In Part 1 you built a greeting tool with a single argument and a flag. In this article you will add two new ideas: validators that reject invalid input before your handler runs, and global options that apply to every command in an application.
The example is quickcat, a minimal file viewer modeled after the Unix cat command.
Application.run() reads sys.argv[1:] by default. Pass an explicit list to override,
or set auto_sys_argv=False to disable automatic reading.
What you will build
$ python quickcat.py README.md
$ python quickcat.py README.md --number
$ python quickcat.py README.md -i CONTRIBUTING.md --verbose
New concepts
| Concept | Purpose |
|---|---|
file_path() | Built-in validator that checks whether a path points to a real file |
global_options | Options that appear on Application and apply to every handler |
multiple=True | Accept the same option more than once and collect values into a list |
Global options
app = Application(
name="quickcat",
description="A tiny cat-like CLI built with quickli.",
global_options=[
Option("verbose", short_name="v", is_flag=True, help_text="Enable verbose output."),
],
)
Global options live on Application, not on an individual command. They can appear before
or after the command name on the command line, and every handler that wants them declares
the matching parameter.
The file_path validator
Argument("path", help_text="Primary file path.", validators=[file_path()]),
file_path() is a built-in validator factory. It returns a callable that quickli runs
after parsing but before calling your handler. If the path does not exist or is not a
file, execution stops with a clear error message - your handler never sees an invalid
value.
You can pass multiple validators in the list; they run left to right.
Repeatable options
Option(
"include",
short_name="i",
multiple=True,
validators=[file_path()],
help_text="Additional file paths to print after the primary file.",
),
multiple=True lets users pass the option more than once:
$ python quickcat.py main.py -i utils.py -i helpers.py
The handler receives include as list[Path] | None. When absent, it is None.
The full example
from __future__ import annotations
from pathlib import Path
from quickli import Application, Argument, Option, file_path
app = Application(
name="quickcat",
description="A tiny cat-like CLI built with quickli.",
global_options=[
Option("verbose", short_name="v", is_flag=True, help_text="Enable verbose output."),
],
)
@app.entrypoint(
help_text="Print one or more text files to stdout.",
arguments=[
Argument("path", help_text="Primary file path.", validators=[file_path()]),
],
options=[
Option("encoding", short_name="e", default="utf-8", help_text="Text encoding."),
Option("number", short_name="n", help_text="Print line numbers.", is_flag=True),
Option(
"include",
short_name="i",
multiple=True,
validators=[file_path()],
help_text="Additional file paths to print after the primary file.",
),
],
)
def show(
path: Path,
encoding: str = "utf-8",
number: bool = False,
include: list[Path] | None = None,
verbose: bool = False,
) -> str:
input_paths = [path, *(include or [])]
rendered_chunks: list[str] = []
for input_path in input_paths:
text = input_path.read_text(encoding=encoding)
lines = text.splitlines()
if number:
lines = [f"{index:>4} {line}" for index, line in enumerate(lines, start=1)]
if verbose:
rendered_chunks.append(f"==> {input_path} <==")
rendered_chunks.append("\n".join(lines))
return "\n".join(chunk for chunk in rendered_chunks if chunk)
if __name__ == "__main__":
print(app.run())
What you learned
global_optionsonApplicationdeclare options available to every handler.file_path()validates that a value is an existing, readable file.multiple=Trueaccumulates repeated option values into a list.- Validators run before the handler, keeping the handler body clean.
Next in this series
The final article builds a kubectl-like multi-command application with @app.command,
custom validator factories, and CommandExecutionError.

