Skip to main content

Getting Started with quiCkLI: Reading Files and Validating Input

· 4 min read
Sven Patrick Meier
Creator and maintainer of quiCkLI, an educational Python CLI framework

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.

note

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

ConceptPurpose
file_path()Built-in validator that checks whether a path points to a real file
global_optionsOptions that appear on Application and apply to every handler
multiple=TrueAccept 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_options on Application declare options available to every handler.
  • file_path() validates that a value is an existing, readable file.
  • multiple=True accumulates 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.

📖 Part 3: Multi-Command Applications →