Docs
cli applications
💻 Building CLI Applications
📌 What You'll Learn
- •How to build professional command-line tools
- •Using argparse (built-in), Click, and Typer
- •Subcommands, options, and arguments
- •Progress bars and colored output
- •Interactive prompts and configuration
🔍 What is a CLI Application?
A Command-Line Interface (CLI) application runs in the terminal and accepts user input through commands, arguments, and options.
# Example CLI commands you've used
git commit -m "message"
pip install requests
python -m pytest --verbose
Why Build CLI Tools?
- •Automation - Scripts for repetitive tasks
- •DevOps - Deployment and infrastructure tools
- •Data processing - Batch processing pipelines
- •Developer tools - Linters, formatters, generators
📦 argparse - Built-in Module
The standard library's argument parsing module.
Basic Example
import argparse
# Create parser
parser = argparse.ArgumentParser(
description="A simple greeting program",
epilog="Example: python greet.py Alice --formal"
)
# Add positional argument
parser.add_argument(
"name",
help="Name of the person to greet"
)
# Add optional flag
parser.add_argument(
"-f", "--formal",
action="store_true",
help="Use formal greeting"
)
# Add optional with value
parser.add_argument(
"-c", "--count",
type=int,
default=1,
help="Number of times to greet (default: 1)"
)
# Parse arguments
args = parser.parse_args()
# Use the arguments
greeting = "Good day" if args.formal else "Hello"
for _ in range(args.count):
print(f"{greeting}, {args.name}!")
Usage:
python greet.py Alice # Hello, Alice!
python greet.py Alice --formal # Good day, Alice!
python greet.py Alice -c 3 # Greets 3 times
python greet.py --help # Shows help text
Argument Types
import argparse
parser = argparse.ArgumentParser()
# Different argument types
parser.add_argument("--count", type=int) # Integer
parser.add_argument("--rate", type=float) # Float
parser.add_argument("--file", type=argparse.FileType('r')) # File
# Choices - restricted values
parser.add_argument(
"--level",
choices=["debug", "info", "warning", "error"],
default="info"
)
# nargs - multiple values
parser.add_argument("--files", nargs="+") # One or more
parser.add_argument("--coords", nargs=2, type=float) # Exactly 2
# Mutually exclusive options
group = parser.add_mutually_exclusive_group()
group.add_argument("--verbose", action="store_true")
group.add_argument("--quiet", action="store_true")
Subcommands
import argparse
parser = argparse.ArgumentParser(prog="myapp")
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# 'create' subcommand
create_parser = subparsers.add_parser("create", help="Create a new item")
create_parser.add_argument("name", help="Item name")
create_parser.add_argument("--type", default="basic")
# 'delete' subcommand
delete_parser = subparsers.add_parser("delete", help="Delete an item")
delete_parser.add_argument("id", type=int, help="Item ID")
delete_parser.add_argument("--force", action="store_true")
# 'list' subcommand
list_parser = subparsers.add_parser("list", help="List all items")
list_parser.add_argument("--all", action="store_true")
args = parser.parse_args()
if args.command == "create":
print(f"Creating {args.name} of type {args.type}")
elif args.command == "delete":
print(f"Deleting item {args.id}")
elif args.command == "list":
print("Listing items...")
Usage:
python myapp.py create "New Item" --type advanced
python myapp.py delete 123 --force
python myapp.py list --all
🖱️ Click - Decorator-Based CLI
Click is a popular third-party library with cleaner syntax.
pip install click
Basic Example
import click
@click.command()
@click.argument("name")
@click.option("--formal", "-f", is_flag=True, help="Use formal greeting")
@click.option("--count", "-c", default=1, help="Number of greetings")
def greet(name: str, formal: bool, count: int):
"""Greet someone by NAME."""
greeting = "Good day" if formal else "Hello"
for _ in range(count):
click.echo(f"{greeting}, {name}!")
if __name__ == "__main__":
greet()
Click Features
import click
@click.command()
@click.option("--name", prompt="Your name", help="The person to greet")
@click.option("--password", prompt=True, hide_input=True) # Hidden input
@click.option("--shout/--no-shout", default=False) # Boolean flag
@click.option("--color", type=click.Choice(["red", "green", "blue"]))
def cli(name, password, shout, color):
"""Example showing Click features."""
msg = f"Hello, {name}!"
if shout:
msg = msg.upper()
if color:
click.secho(msg, fg=color) # Colored output
else:
click.echo(msg)
# Progress bar
@click.command()
def download():
with click.progressbar(range(100)) as bar:
for item in bar:
import time
time.sleep(0.05)
if __name__ == "__main__":
cli()
Click Groups (Subcommands)
import click
@click.group()
def cli():
"""My CLI application."""
pass
@cli.command()
@click.argument("name")
def create(name):
"""Create a new item."""
click.echo(f"Created: {name}")
@cli.command()
@click.argument("item_id", type=int)
@click.option("--force", is_flag=True)
def delete(item_id, force):
"""Delete an item by ID."""
if force:
click.echo(f"Force deleted: {item_id}")
else:
click.echo(f"Deleted: {item_id}")
@cli.command("list") # Use 'list' as command name
def list_items():
"""List all items."""
click.echo("Listing items...")
if __name__ == "__main__":
cli()
⚡ Typer - Modern CLI with Type Hints
Typer builds on Click with type hint support.
pip install typer[all] # Includes rich for better output
Basic Example
import typer
app = typer.Typer()
@app.command()
def greet(
name: str,
formal: bool = typer.Option(False, "--formal", "-f", help="Formal greeting"),
count: int = typer.Option(1, "--count", "-c", help="Times to greet")
):
"""Greet someone by NAME."""
greeting = "Good day" if formal else "Hello"
for _ in range(count):
typer.echo(f"{greeting}, {name}!")
if __name__ == "__main__":
app()
Typer with Type Inference
import typer
from typing import Optional
from enum import Enum
class Color(str, Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
app = typer.Typer()
@app.command()
def process(
# Positional argument (required)
filename: str,
# Optional with default
output: str = "output.txt",
# Flag
verbose: bool = False,
# Enum choices
color: Color = Color.RED,
# Optional (can be None)
config: Optional[str] = None
):
"""Process a file."""
typer.echo(f"Processing {filename}")
if verbose:
typer.echo(f"Output: {output}")
typer.echo(f"Color: {color.value}")
@app.command()
def version():
"""Show version."""
typer.echo("v1.0.0")
if __name__ == "__main__":
app()
🎨 Rich - Beautiful Terminal Output
pip install rich
Colored and Formatted Output
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.progress import track
from rich import print # Enhanced print
console = Console()
# Colored output
console.print("[bold red]Error:[/bold red] Something went wrong!")
console.print("[green]Success![/green] Operation completed.")
# Tables
table = Table(title="Users")
table.add_column("ID", style="cyan")
table.add_column("Name", style="magenta")
table.add_column("Email")
table.add_row("1", "Alice", "alice@example.com")
table.add_row("2", "Bob", "bob@example.com")
table.add_row("3", "Charlie", "charlie@example.com")
console.print(table)
# Panels
panel = Panel("This is important information!", title="Notice", border_style="blue")
console.print(panel)
# Progress bar
import time
for item in track(range(100), description="Processing..."):
time.sleep(0.02)
# Status spinner
with console.status("[bold green]Working...") as status:
time.sleep(2)
status.update("[bold blue]Almost done...")
time.sleep(1)
📝 Interactive Prompts
Using prompt_toolkit
pip install prompt_toolkit
from prompt_toolkit import prompt
from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.validation import Validator
# Simple prompt
name = prompt("Enter your name: ")
# With auto-completion
commands = WordCompleter(["create", "delete", "list", "help", "quit"])
command = prompt("Command: ", completer=commands)
# With validation
def is_valid_number(text):
return text.isdigit()
validator = Validator.from_callable(
is_valid_number,
error_message="Please enter a valid number"
)
number = prompt("Enter a number: ", validator=validator)
# Password input
password = prompt("Password: ", is_password=True)
Using questionary
pip install questionary
import questionary
# Text input
name = questionary.text("What's your name?").ask()
# Selection
choice = questionary.select(
"Choose an option:",
choices=["Option 1", "Option 2", "Option 3"]
).ask()
# Checkbox (multiple selection)
selected = questionary.checkbox(
"Select items:",
choices=["Item A", "Item B", "Item C", "Item D"]
).ask()
# Confirmation
if questionary.confirm("Are you sure?").ask():
print("Confirmed!")
# Password
password = questionary.password("Enter password:").ask()
📁 Configuration Files
Reading YAML/TOML Config
import tomllib # Python 3.11+, use tomli for earlier
from pathlib import Path
# Read TOML config
def load_config(path: str = "config.toml") -> dict:
config_path = Path(path)
if config_path.exists():
with open(config_path, "rb") as f:
return tomllib.load(f)
return {}
# config.toml:
# [database]
# host = "localhost"
# port = 5432
#
# [logging]
# level = "INFO"
config = load_config()
db_host = config.get("database", {}).get("host", "localhost")
XDG Config Directories
from pathlib import Path
import os
def get_config_dir(app_name: str) -> Path:
"""Get platform-appropriate config directory."""
if os.name == "nt": # Windows
base = Path(os.environ.get("APPDATA", "~"))
else: # Linux/Mac
base = Path(os.environ.get("XDG_CONFIG_HOME", "~/.config"))
config_dir = base.expanduser() / app_name
config_dir.mkdir(parents=True, exist_ok=True)
return config_dir
# Usage
config_dir = get_config_dir("myapp")
config_file = config_dir / "config.toml"
🚀 Complete CLI Example
#!/usr/bin/env python3
"""
A complete CLI application example.
"""
import typer
from rich.console import Console
from rich.table import Table
from pathlib import Path
from typing import Optional
import json
app = typer.Typer(help="Task Manager CLI")
console = Console()
TASKS_FILE = Path.home() / ".tasks.json"
def load_tasks() -> list[dict]:
if TASKS_FILE.exists():
return json.loads(TASKS_FILE.read_text())
return []
def save_tasks(tasks: list[dict]) -> None:
TASKS_FILE.write_text(json.dumps(tasks, indent=2))
@app.command()
def add(description: str, priority: int = 1):
"""Add a new task."""
tasks = load_tasks()
task = {
"id": len(tasks) + 1,
"description": description,
"priority": priority,
"done": False
}
tasks.append(task)
save_tasks(tasks)
console.print(f"[green]✓[/green] Added task: {description}")
@app.command()
def list(all: bool = False):
"""List all tasks."""
tasks = load_tasks()
if not all:
tasks = [t for t in tasks if not t["done"]]
if not tasks:
console.print("[yellow]No tasks found.[/yellow]")
return
table = Table(title="Tasks")
table.add_column("ID", style="cyan")
table.add_column("Description")
table.add_column("Priority", justify="center")
table.add_column("Status", justify="center")
for task in tasks:
status = "[green]✓[/green]" if task["done"] else "[red]○[/red]"
table.add_row(
str(task["id"]),
task["description"],
str(task["priority"]),
status
)
console.print(table)
@app.command()
def done(task_id: int):
"""Mark a task as done."""
tasks = load_tasks()
for task in tasks:
if task["id"] == task_id:
task["done"] = True
save_tasks(tasks)
console.print(f"[green]✓[/green] Marked task {task_id} as done")
return
console.print(f"[red]Task {task_id} not found[/red]")
@app.command()
def delete(task_id: int, force: bool = False):
"""Delete a task."""
if not force:
confirm = typer.confirm(f"Delete task {task_id}?")
if not confirm:
console.print("Cancelled")
return
tasks = load_tasks()
tasks = [t for t in tasks if t["id"] != task_id]
save_tasks(tasks)
console.print(f"[green]✓[/green] Deleted task {task_id}")
if __name__ == "__main__":
app()
📋 CLI Best Practices
- •Always provide --help - argparse/Click/Typer do this automatically
- •Use descriptive names -
--output-filenot-o - •Provide sensible defaults - Most options shouldn't be required
- •Show progress - For long operations, use progress bars
- •Use exit codes - 0 for success, non-zero for errors
- •Support --quiet and --verbose - Control output verbosity
- •Read from stdin, write to stdout - Enable piping
- •Use color sparingly - Make output machine-parseable when needed
🎯 Next Steps
After learning CLI applications, proceed to 27_api_development to learn how to build REST APIs!