← Back to Course Index
Lesson 18 of 20

CLI Tools

~20 min · sys.argv, argparse, Click, Typer — building command-line programs

Ref
Primary Source
Official Docs — argparse

stdlib's built-in CLI parser. For real projects, also see Typer — the modern, type-hint-driven CLI framework.

1 — The Ecosystem Map

JavaScript
// Raw
process.argv  // ["node", "script.js", "arg1"]

// Libraries
// Commander.js — full-featured
// yargs — flexible
// minimist — minimal
Python
# Raw
import sys
sys.argv  # ["script.py", "arg1", "arg2"]

# stdlib
import argparse  # batteries-included, no install

# Third-party — better APIs
# Click       — decorator-based, widely used
# Typer       — type-hint-driven (Click under the hood)
# rich        — beautiful terminal output

2 — argparse — stdlib (no install needed)

# greet.py
import argparse

def main():
    parser = argparse.ArgumentParser(
        description="A friendly greeting tool"
    )

    # Positional argument (required)
    parser.add_argument("name", help="Name to greet")

    # Optional argument with flag
    parser.add_argument(
        "--count", "-c",
        type=int,
        default=1,
        help="Number of times to greet (default: 1)"
    )

    # Boolean flag
    parser.add_argument(
        "--shout",
        action="store_true",   # True if flag present, False otherwise
        help="Output in uppercase"
    )

    # Choice from a set
    parser.add_argument(
        "--language",
        choices=["en", "es", "fr"],
        default="en"
    )

    args = parser.parse_args()

    msg = f"Hello, {args.name}!"
    if args.shout:
        msg = msg.upper()

    for _ in range(args.count):
        print(msg)

if __name__ == "__main__":
    main()

# Usage:
# python greet.py Alice
# python greet.py Alice --count 3 --shout
# python greet.py --help  ← auto-generated help!

3 — Subcommands (like git add, git commit)

# git-clone.py — multi-command CLI
import argparse

def cmd_add(args):
    print(f"Adding {args.files}")

def cmd_commit(args):
    print(f"Committing: {args.message}")

def main():
    parser = argparse.ArgumentParser(description="Mini VCS")
    subparsers = parser.add_subparsers(dest="command", required=True)

    # 'add' subcommand
    add_parser = subparsers.add_parser("add", help="Stage files")
    add_parser.add_argument("files", nargs="+")   # one or more

    # 'commit' subcommand
    commit_parser = subparsers.add_parser("commit", help="Commit changes")
    commit_parser.add_argument("-m", "--message", required=True)

    args = parser.parse_args()

    match args.command:
        case "add":    cmd_add(args)
        case "commit": cmd_commit(args)

# python git-clone.py add file1.txt file2.txt
# python git-clone.py commit -m "Initial commit"

4 — Typer — Modern CLI with Type Hints

Typer generates the entire CLI from your function's type hints. No parser setup — just annotate your function.

# pip install typer[all]
import typer
from typing import Annotated

app = typer.Typer()

@app.command()
def greet(
    name: str,
    count: Annotated[int, typer.Option(help="Times to greet")] = 1,
    shout: bool = False,
):
    """A friendly greeting tool."""
    msg = f"Hello, {name}!"
    if shout:
        msg = msg.upper()
    for _ in range(count):
        typer.echo(msg)

@app.command()
def goodbye(name: str):
    """Say goodbye."""
    typer.echo(f"Goodbye, {name}!")

if __name__ == "__main__":
    app()

# python greet.py greet Alice --count 3 --shout
# python greet.py goodbye Bob
# python greet.py --help  ← auto-generated, beautiful help
💡 Typer vs argparse vs Click

Use argparse when you can't add dependencies. Use Typer for new projects — it's the most ergonomic. Click is the battle-tested foundation that Typer builds on — worth knowing if you read others' code.

5 — Rich — Beautiful Terminal Output

rich adds colors, tables, progress bars, and syntax highlighting to your CLI — like chalk + ora + cli-table in JS.

# pip install rich
from rich import print
from rich.console import Console
from rich.table import Table
from rich.progress import track

console = Console()

# Styled print (works with [markup])
print("[bold green]✅ Success![/bold green]")
print("[red]Error:[/red] File not found")
console.log("Debug info", log_locals=True)

# Table
table = Table(title="Users")
table.add_column("Name", style="cyan")
table.add_column("Role", style="magenta")
table.add_row("Alice", "admin")
table.add_row("Bob",   "editor")
console.print(table)

# Progress bar
for item in track(large_list, description="Processing..."):
    process(item)

# Syntax highlighting
from rich.syntax import Syntax
code = Syntax('print("hello")', "python", theme="monokai")
console.print(code)

6 — Making a Script Executable

# Make directly executable on Unix
#!/usr/bin/env python3
# (add this as the first line of your script)

# In terminal:
chmod +x my_script.py
./my_script.py

# Install as a CLI tool via pyproject.toml
[project.scripts]
my-tool = "mypackage.cli:app"   # module:function

# After pip install -e .
my-tool --help   # works from anywhere in the terminal

🧠 Quiz

1. In argparse, what does action="store_true" do?

2. What is the main advantage of Typer over argparse?

3. To make a script installable as a CLI command via pip, you define it in:

0/3

Questions answered correctly.