Claude
Skill
python-cli-ops
CLI application patterns for Python. Triggers on: cli, command line, typer, click, argparse, terminal, rich, console, terminal ui.
Virus-scanned
Reviewed automatically before listing.
Download
0xdarkmatter-claude-mods-skills_python-cli-ops-3dfaf0b.zip · 11 KB
Install
skills CLI
npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/python-cli-ops
Claude Code
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git
git clone https://github.com/0xDarkMatter/claude-mods.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Python CLI Patterns
Modern CLI development with Typer and Rich.
Basic Typer App
import typer
app = typer.Typer(
name="myapp",
help="My awesome CLI application",
add_completion=True,
)
@app.command()
def hello(
name: str = typer.Argument(..., help="Name to greet"),
count: int = typer.Option(1, "--count", "-c", help="Times to greet"),
loud: bool = typer.Option(False, "--loud", "-l", help="Uppercase"),
):
"""Say hello to someone."""
message = f"Hello, {name}!"
if loud:
message = message.upper()
for _ in range(count):
typer.echo(message)
if __name__ == "__main__":
app()
Command Groups
import typer
app = typer.Typer()
users_app = typer.Typer(help="User management commands")
app.add_typer(users_app, name="users")
@users_app.command("list")
def list_users():
"""List all users."""
typer.echo("Listing users...")
@users_app.command("create")
def create_user(name: str, email: str):
"""Create a new user."""
typer.echo(f"Creating user: {name} <{email}>")
@app.command()
def version():
"""Show version."""
typer.echo("1.0.0")
# Usage: myapp users list
# myapp users create "John" "john@example.com"
# myapp version
Rich Output
from rich.console import Console
from rich.table import Table
from rich.progress import track
from rich.panel import Panel
import typer
console = Console()
@app.command()
def show_users():
"""Display users in a table."""
table = Table(title="Users")
table.add_column("ID", style="cyan")
table.add_column("Name", style="green")
table.add_column("Email")
users = [
(1, "Alice", "alice@example.com"),
(2, "Bob", "bob@example.com"),
]
for id, name, email in users:
table.add_row(str(id), name, email)
console.print(table)
@app.command()
def process():
"""Process items with progress bar."""
items = list(range(100))
for item in track(items, description="Processing..."):
do_something(item)
console.print("[green]Done![/green]")
Error Handling
import typer
from rich.console import Console
console = Console()
def error(message: str, code: int = 1):
"""Print error and exit."""
console.print(f"[red]Error:[/red] {message}")
raise typer.Exit(code)
@app.command()
def process(file: str):
"""Process a file."""
if not os.path.exists(file):
error(f"File not found: {file}")
try:
result = process_file(file)
console.print(f"[green]Success:[/green] {result}")
except ValueError as e:
error(str(e))
Quick Reference
| Feature | Typer Syntax |
|---|---|
| Required arg | name: str |
| Optional arg | name: str = "default" |
| Option | typer.Option(default, "--flag", "-f") |
| Argument | typer.Argument(..., help="...") |
| Boolean flag | verbose: bool = False |
| Enum choice | color: Color = Color.red |
| Rich Feature | Usage |
|---|---|
| Table | Table() + add_column/row |
| Progress | track(items) |
| Colors | [red]text[/red] |
| Panel | Panel("content", title="Title") |
Additional Resources
./references/typer-patterns.md- Advanced Typer patterns./references/rich-output.md- Rich tables, progress, formatting./references/configuration.md- Config files, environment variables
Assets
./assets/cli-template.py- Full CLI application template
See Also
Related Skills:
python-typing-ops- Type hints for CLI argumentspython-observability-ops- Logging for CLI applications
Complementary Skills:
python-env- Package CLI for distribution
Files (claude-mods)
-
assets
-
cli-template.py 6.4 KB
""" CLI Application Template A production-ready CLI application structure. Usage: python cli.py --help python cli.py greet "World" python cli.py config init """ import sys from pathlib import Path from typing import Annotated, Optional import typer from rich.console import Console from rich.table import Table from rich.panel import Panel from rich.progress import track # ============================================================================= # App Setup # ============================================================================= app = typer.Typer( name="myapp", help="My awesome CLI application", no_args_is_help=True, add_completion=True, rich_markup_mode="rich", ) console = Console() err_console = Console(stderr=True) # Sub-applications config_app = typer.Typer(help="Configuration commands") app.add_typer(config_app, name="config") # ============================================================================= # State and Configuration # ============================================================================= class AppState: """Application state shared across commands.""" def __init__(self): self.verbose: bool = False self.config_dir: Path = Path.home() / ".config" / "myapp" self.config_file: Path = self.config_dir / "config.toml" state = AppState() @app.callback() def main( verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"), config: Optional[Path] = typer.Option( None, "--config", "-c", help="Config file path" ), ): """ [bold blue]MyApp[/bold blue] - A sample CLI application. Use [green]--help[/green] on any command for more info. """ state.verbose = verbose if config: state.config_file = config # ============================================================================= # Utility Functions # ============================================================================= def log(message: str, style: str = ""): """Log message if verbose mode is enabled.""" if state.verbose: console.print(f"[dim]{message}[/dim]", style=style) def error(message: str, code: int = 1) -> None: """Print error and exit.""" err_console.print(f"[red]Error:[/red] {message}") raise typer.Exit(code) def success(message: str) -> None: """Print success message.""" console.print(f"[green]✓[/green] {message}") # ============================================================================= # Commands # ============================================================================= @app.command() def greet( name: Annotated[str, typer.Argument(help="Name to greet")], count: Annotated[int, typer.Option("--count", "-n", help="Times to greet")] = 1, loud: Annotated[bool, typer.Option("--loud", "-l", help="Uppercase")] = False, ): """ Say hello to someone. Example: myapp greet World myapp greet World --count 3 --loud """ message = f"Hello, {name}!" if loud: message = message.upper() for _ in range(count): console.print(message) @app.command() def process( files: Annotated[ list[Path], typer.Argument( help="Files to process", exists=True, readable=True, ), ], output: Annotated[ Optional[Path], typer.Option("--output", "-o", help="Output file"), ] = None, ): """ Process one or more files. Example: myapp process file1.txt file2.txt -o output.txt """ log(f"Processing {len(files)} files") results = [] for file in track(files, description="Processing..."): log(f"Processing: {file}") # Simulate processing results.append(f"Processed: {file.name}") if output: output.write_text("\n".join(results)) success(f"Results written to {output}") else: for result in results: console.print(result) @app.command() def status(): """Show application status.""" table = Table(title="Application Status") table.add_column("Setting", style="cyan") table.add_column("Value", style="green") table.add_row("Config Dir", str(state.config_dir)) table.add_row("Config File", str(state.config_file)) table.add_row("Verbose", str(state.verbose)) table.add_row( "Config Exists", "✓" if state.config_file.exists() else "✗" ) console.print(table) # ============================================================================= # Config Subcommands # ============================================================================= @config_app.command("init") def config_init( force: Annotated[ bool, typer.Option("--force", "-f", help="Overwrite existing"), ] = False, ): """Initialize configuration file.""" if state.config_file.exists() and not force: if not typer.confirm(f"Config exists at {state.config_file}. Overwrite?"): raise typer.Abort() state.config_dir.mkdir(parents=True, exist_ok=True) default_config = """ # MyApp Configuration # See documentation for all options [general] verbose = false [server] host = "localhost" port = 8080 """.strip() state.config_file.write_text(default_config) success(f"Created config: {state.config_file}") @config_app.command("show") def config_show(): """Show current configuration.""" if not state.config_file.exists(): error(f"Config not found: {state.config_file}") content = state.config_file.read_text() console.print(Panel(content, title=str(state.config_file), border_style="blue")) @config_app.command("path") def config_path(): """Print config file path.""" typer.echo(state.config_file) # ============================================================================= # Version # ============================================================================= def version_callback(value: bool): if value: console.print("myapp version [bold]1.0.0[/bold]") raise typer.Exit() @app.callback() def version_option( version: Annotated[ bool, typer.Option( "--version", callback=version_callback, is_eager=True, help="Show version", ), ] = False, ): pass # ============================================================================= # Entry Point # ============================================================================= if __name__ == "__main__": app()
-
-
references
-
configuration.md 7.2 KB
# CLI Configuration Patterns Configuration file and environment variable handling. ## Environment Variables ```python import os import typer app = typer.Typer() @app.command() def connect( # Read from env var with fallback host: str = typer.Option( "localhost", envvar="DB_HOST", help="Database host", ), port: int = typer.Option( 5432, envvar="DB_PORT", help="Database port", ), # Multiple envvars (first found wins) password: str = typer.Option( ..., # Required envvar=["DB_PASSWORD", "DATABASE_PASSWORD", "PGPASSWORD"], help="Database password", ), ): """Connect to database.""" typer.echo(f"Connecting to {host}:{port}") ``` ## Configuration File with TOML ```python import tomllib # Python 3.11+ from pathlib import Path from dataclasses import dataclass from typing import Optional @dataclass class Config: host: str = "localhost" port: int = 8080 debug: bool = False log_level: str = "INFO" @classmethod def load(cls, path: Path | None = None) -> "Config": """Load config from TOML file.""" if path is None: # Search default locations for p in [ Path("config.toml"), Path.home() / ".config" / "myapp" / "config.toml", ]: if p.exists(): path = p break if path and path.exists(): with open(path, "rb") as f: data = tomllib.load(f) return cls(**data) return cls() # Usage in CLI @app.callback() def main( ctx: typer.Context, config: Path = typer.Option( None, "--config", "-c", exists=True, help="Config file path", ), ): ctx.obj = Config.load(config) @app.command() def serve(ctx: typer.Context): config = ctx.obj typer.echo(f"Starting on {config.host}:{config.port}") ``` ## Config with Pydantic Settings ```python from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic import Field from pathlib import Path class Settings(BaseSettings): """Application settings from env vars and config file.""" model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", env_prefix="MYAPP_", # MYAPP_HOST, MYAPP_PORT case_sensitive=False, ) host: str = "localhost" port: int = 8080 debug: bool = False database_url: str = Field( default="sqlite:///app.db", validation_alias="DATABASE_URL", # Also check DATABASE_URL without prefix ) api_key: str = Field(default="") # Load once settings = Settings() @app.command() def serve(): typer.echo(f"Host: {settings.host}") typer.echo(f"Debug: {settings.debug}") ``` ## XDG Config Directories ```python from pathlib import Path import os def get_config_dir(app_name: str) -> Path: """Get XDG-compliant config directory.""" if os.name == "nt": # Windows base = Path(os.environ.get("APPDATA", Path.home())) else: # Linux/macOS base = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) config_dir = base / app_name config_dir.mkdir(parents=True, exist_ok=True) return config_dir def get_data_dir(app_name: str) -> Path: """Get XDG-compliant data directory.""" if os.name == "nt": base = Path(os.environ.get("LOCALAPPDATA", Path.home())) else: base = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")) data_dir = base / app_name data_dir.mkdir(parents=True, exist_ok=True) return data_dir def get_cache_dir(app_name: str) -> Path: """Get XDG-compliant cache directory.""" if os.name == "nt": base = Path(os.environ.get("LOCALAPPDATA", Path.home())) / "cache" else: base = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) cache_dir = base / app_name cache_dir.mkdir(parents=True, exist_ok=True) return cache_dir ``` ## Config Init Command ```python import typer from pathlib import Path @app.command() def init( force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing"), ): """Initialize configuration file.""" config_dir = get_config_dir("myapp") config_file = config_dir / "config.toml" if config_file.exists() and not force: typer.echo(f"Config already exists: {config_file}") if not typer.confirm("Overwrite?"): raise typer.Abort() default_config = """ # MyApp Configuration [server] host = "localhost" port = 8080 [logging] level = "INFO" format = "json" [database] url = "sqlite:///app.db" """.strip() config_file.write_text(default_config) typer.echo(f"Created config: {config_file}") ``` ## Layered Configuration ```python from dataclasses import dataclass, field, asdict import tomllib from pathlib import Path import os @dataclass class Config: """Config with layered loading: defaults < file < env vars < CLI.""" host: str = "localhost" port: int = 8080 debug: bool = False @classmethod def load( cls, config_file: Path | None = None, **cli_overrides, ) -> "Config": # Start with defaults config = cls() # Layer 2: Config file if config_file and config_file.exists(): with open(config_file, "rb") as f: file_config = tomllib.load(f) for key, value in file_config.items(): if hasattr(config, key): setattr(config, key, value) # Layer 3: Environment variables env_mapping = { "MYAPP_HOST": "host", "MYAPP_PORT": "port", "MYAPP_DEBUG": "debug", } for env_var, attr in env_mapping.items(): if value := os.environ.get(env_var): if attr == "port": value = int(value) elif attr == "debug": value = value.lower() in ("true", "1", "yes") setattr(config, attr, value) # Layer 4: CLI overrides (highest priority) for key, value in cli_overrides.items(): if value is not None and hasattr(config, key): setattr(config, key, value) return config @app.command() def serve( config: Path = typer.Option(None, "--config", "-c"), host: str = typer.Option(None, "--host", "-h"), port: int = typer.Option(None, "--port", "-p"), debug: bool = typer.Option(None, "--debug", "-d"), ): """Start server with layered config.""" cfg = Config.load( config_file=config, host=host, port=port, debug=debug, ) typer.echo(f"Starting on {cfg.host}:{cfg.port}") ``` ## Quick Reference | Source | Priority | Example | |--------|----------|---------| | Defaults | Lowest | `host="localhost"` | | Config file | Low | `config.toml` | | Env vars | Medium | `MYAPP_HOST=0.0.0.0` | | CLI args | Highest | `--host 0.0.0.0` | | XDG Directory | Purpose | Default | |---------------|---------|---------| | `XDG_CONFIG_HOME` | Config files | `~/.config` | | `XDG_DATA_HOME` | Persistent data | `~/.local/share` | | `XDG_CACHE_HOME` | Cache | `~/.cache` | -
rich-output.md 5.9 KB
# Rich Terminal Output Beautiful CLI output with Rich. ## Console Basics ```python from rich.console import Console from rich.text import Text console = Console() # Basic printing console.print("Hello, World!") # With styling console.print("Hello", style="bold red") console.print("[bold blue]Bold blue[/bold blue] and [green]green[/green]") # Print objects (auto-formatting) console.print({"key": "value", "list": [1, 2, 3]}) # Print to stderr console.print("Error!", style="red", file=sys.stderr) # Width control console.print("Text", width=40, justify="center") ``` ## Tables ```python from rich.table import Table from rich.console import Console console = Console() # Basic table table = Table(title="Users") table.add_column("ID", style="cyan", justify="right") table.add_column("Name", style="green") table.add_column("Email") table.add_column("Active", justify="center") 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) # Table with styling table = Table( title="Report", show_header=True, header_style="bold magenta", border_style="blue", box=box.DOUBLE, ) # Dynamic table from data def print_users(users: list[dict]): table = Table() table.add_column("ID") table.add_column("Name") table.add_column("Status") for user in users: status = "[green]Active[/green]" if user["active"] else "[red]Inactive[/red]" table.add_row(str(user["id"]), user["name"], status) console.print(table) ``` ## Progress Bars ```python from rich.progress import ( Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeRemainingColumn, track, ) from rich.console import Console console = Console() # Simple progress with track() for item in track(items, description="Processing..."): process(item) # Customizable progress with Progress( SpinnerColumn(), TextColumn("[bold blue]{task.description}"), BarColumn(), TaskProgressColumn(), TimeRemainingColumn(), console=console, ) as progress: task = progress.add_task("Downloading...", total=100) for i in range(100): do_work() progress.update(task, advance=1) # Multiple tasks with Progress() as progress: download_task = progress.add_task("Downloading", total=1000) process_task = progress.add_task("Processing", total=500) while not progress.finished: progress.update(download_task, advance=10) progress.update(process_task, advance=5) time.sleep(0.01) # Indeterminate spinner with console.status("[bold green]Working...") as status: while not done: do_something() status.update("[bold green]Still working...") ``` ## Panels and Layout ```python from rich.panel import Panel from rich.layout import Layout from rich.console import Console console = Console() # Basic panel console.print(Panel("Hello, World!", title="Greeting", border_style="green")) # Panel with rich content console.print(Panel( "[bold]Important Message[/bold]\n\n" "This is a [red]warning[/red] message.", title="Alert", subtitle="Action Required", border_style="red", )) # Layout for complex UIs layout = Layout() layout.split( Layout(name="header", size=3), Layout(name="main"), Layout(name="footer", size=3), ) layout["header"].update(Panel("My CLI App", style="bold")) layout["main"].split_row( Layout(name="left"), Layout(name="right"), ) layout["footer"].update(Panel("Press Ctrl+C to exit")) console.print(layout) ``` ## Markdown and Syntax ```python from rich.markdown import Markdown from rich.syntax import Syntax from rich.console import Console console = Console() # Render markdown md = Markdown(""" # Title This is **bold** and *italic*. - Item 1 - Item 2 ```python print("Hello") ``` """) console.print(md) # Syntax highlighting code = ''' def hello(name: str) -> str: """Say hello.""" return f"Hello, {name}!" ''' syntax = Syntax(code, "python", theme="monokai", line_numbers=True) console.print(syntax) # From file syntax = Syntax.from_path("script.py", line_numbers=True) console.print(syntax) ``` ## Trees ```python from rich.tree import Tree from rich.console import Console console = Console() tree = Tree("[bold]Project Structure") src = tree.add("[blue]src/") src.add("main.py") src.add("utils.py") src.add("[blue]models/").add("user.py") tests = tree.add("[blue]tests/") tests.add("test_main.py") console.print(tree) ``` ## Live Display ```python from rich.live import Live from rich.table import Table from rich.console import Console import time console = Console() def generate_table(count: int) -> Table: table = Table() table.add_column("Count") table.add_column("Status") table.add_row(str(count), "Processing...") return table with Live(generate_table(0), console=console, refresh_per_second=4) as live: for i in range(100): time.sleep(0.1) live.update(generate_table(i)) ``` ## Logging Integration ```python from rich.logging import RichHandler import logging logging.basicConfig( level="INFO", format="%(message)s", datefmt="[%X]", handlers=[RichHandler(rich_tracebacks=True)], ) logger = logging.getLogger("my_app") logger.info("Hello, World!") logger.warning("This is a warning") logger.error("Something went wrong") ``` ## Quick Reference | Component | Usage | |-----------|-------| | `console.print()` | Print with styling | | `Table()` | Tabular data | | `track()` | Simple progress bar | | `Progress()` | Custom progress | | `Panel()` | Bordered content | | `Syntax()` | Code highlighting | | `Markdown()` | Render markdown | | `Tree()` | Hierarchical data | | `Live()` | Dynamic updates | | Markup | Effect | |--------|--------| | `[bold]text[/bold]` | Bold | | `[red]text[/red]` | Red color | | `[link=url]text[/link]` | Hyperlink | | `[dim]text[/dim]` | Dimmed | -
typer-patterns.md 6.6 KB
# Advanced Typer Patterns Modern CLI development patterns with Typer. ## Application Structure ```python import typer from typing import Optional from enum import Enum # Create app with metadata app = typer.Typer( name="myapp", help="My CLI application", add_completion=True, no_args_is_help=True, # Show help if no command given rich_markup_mode="rich", # Enable Rich formatting in help ) # State object for shared options class State: def __init__(self): self.verbose: bool = False self.config_path: str = "" state = State() @app.callback() def main( verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"), config: str = typer.Option("config.yaml", "--config", "-c", help="Config file"), ): """ My awesome CLI application. Use --help on any command for more info. """ state.verbose = verbose state.config_path = config ``` ## Type-Safe Arguments ```python from typing import Annotated from enum import Enum from pathlib import Path class OutputFormat(str, Enum): json = "json" yaml = "yaml" table = "table" @app.command() def export( # Required argument query: Annotated[str, typer.Argument(help="Search query")], # Optional argument with default limit: Annotated[int, typer.Argument()] = 10, # Path validation output: Annotated[ Path, typer.Option( "--output", "-o", help="Output file path", exists=False, # Must not exist file_okay=True, dir_okay=False, writable=True, resolve_path=True, ) ] = None, # Input file (must exist) input_file: Annotated[ Path, typer.Option( "--input", "-i", exists=True, # Must exist readable=True, ) ] = None, # Enum choices format: Annotated[ OutputFormat, typer.Option("--format", "-f", case_sensitive=False) ] = OutputFormat.table, # Multiple values tags: Annotated[ list[str], typer.Option("--tag", "-t", help="Tags to filter") ] = None, ): """Export data with various options.""" typer.echo(f"Query: {query}, Format: {format.value}") ``` ## Interactive Prompts ```python import typer @app.command() def create_user(): """Create a new user interactively.""" # Text prompt name = typer.prompt("What's your name?") # With default email = typer.prompt("Email", default=f"{name.lower()}@example.com") # Hidden input (password) password = typer.prompt("Password", hide_input=True) # Confirmation password_confirm = typer.prompt("Confirm password", hide_input=True) if password != password_confirm: typer.echo("Passwords don't match!") raise typer.Abort() # Yes/No confirmation if typer.confirm("Create this user?"): typer.echo(f"Creating user: {name}") else: typer.echo("Cancelled") raise typer.Abort() # Non-interactive with --yes flag @app.command() def delete_all( yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"), ): """Delete all items.""" if not yes: yes = typer.confirm("Are you sure?") if yes: typer.echo("Deleting...") else: raise typer.Abort() ``` ## Context and Dependency Injection ```python import typer from typing import Annotated # Create a context type class Context: def __init__(self, db_url: str, debug: bool): self.db_url = db_url self.debug = debug self.db = None def connect(self): self.db = create_connection(self.db_url) # Store in typer context @app.callback() def main( ctx: typer.Context, db_url: str = typer.Option("sqlite:///app.db", envvar="DATABASE_URL"), debug: bool = typer.Option(False, "--debug"), ): """Initialize application context.""" ctx.obj = Context(db_url=db_url, debug=debug) ctx.obj.connect() @app.command() def query( ctx: typer.Context, sql: str, ): """Run a SQL query.""" result = ctx.obj.db.execute(sql) for row in result: typer.echo(row) ``` ## Subcommands and Nested Apps ```python import typer # Main app app = typer.Typer() # Sub-applications db_app = typer.Typer(help="Database operations") cache_app = typer.Typer(help="Cache operations") # Register sub-apps app.add_typer(db_app, name="db") app.add_typer(cache_app, name="cache") @db_app.command("migrate") def db_migrate(): """Run database migrations.""" typer.echo("Running migrations...") @db_app.command("seed") def db_seed(): """Seed database with test data.""" typer.echo("Seeding database...") @cache_app.command("clear") def cache_clear(): """Clear cache.""" typer.echo("Clearing cache...") # Usage: # myapp db migrate # myapp db seed # myapp cache clear ``` ## Async Commands ```python import typer import asyncio app = typer.Typer() async def async_operation(): await asyncio.sleep(1) return "Done" @app.command() def fetch(): """Fetch data asynchronously.""" result = asyncio.run(async_main()) typer.echo(result) async def async_main(): results = await asyncio.gather( async_operation(), async_operation(), ) return results ``` ## Testing CLI Apps ```python from typer.testing import CliRunner import pytest runner = CliRunner() def test_hello(): result = runner.invoke(app, ["hello", "World"]) assert result.exit_code == 0 assert "Hello, World!" in result.stdout def test_hello_with_options(): result = runner.invoke(app, ["hello", "World", "--count", "3", "--loud"]) assert result.exit_code == 0 assert "HELLO, WORLD!" in result.stdout assert result.stdout.count("HELLO") == 3 def test_invalid_input(): result = runner.invoke(app, ["process", "nonexistent.txt"]) assert result.exit_code == 1 assert "not found" in result.stdout.lower() # With environment variables def test_with_env(): result = runner.invoke( app, ["connect"], env={"DATABASE_URL": "sqlite:///test.db"} ) assert result.exit_code == 0 ``` ## Quick Reference | Pattern | Syntax | |---------|--------| | App callback | `@app.callback()` for global options | | Context | `ctx: typer.Context` + `ctx.obj` | | Envvar | `typer.Option(envvar="VAR_NAME")` | | Prompt | `typer.prompt("Question")` | | Confirm | `typer.confirm("Sure?")` | | Abort | `raise typer.Abort()` | | Exit | `raise typer.Exit(code=1)` | | Progress | Use Rich `track()` | | Decorator | Purpose | |-----------|---------| | `@app.command()` | Define a command | | `@app.callback()` | App initialization | | `@sub_app.command()` | Subcommand |
-
-
scripts
-
.gitkeep 0 B · in bundle
-
-
SKILL.md 4 KB
--- name: python-cli-ops description: "CLI application patterns for Python. Triggers on: cli, command line, typer, click, argparse, terminal, rich, console, terminal ui." license: MIT compatibility: "Python 3.10+. Requires typer and rich for modern CLI development." allowed-tools: "Read Write Bash" metadata: author: claude-mods related-skills: python-typing-ops, python-observability-ops --- # Python CLI Patterns Modern CLI development with Typer and Rich. ## Basic Typer App ```python import typer app = typer.Typer( name="myapp", help="My awesome CLI application", add_completion=True, ) @app.command() def hello( name: str = typer.Argument(..., help="Name to greet"), count: int = typer.Option(1, "--count", "-c", help="Times to greet"), loud: bool = typer.Option(False, "--loud", "-l", help="Uppercase"), ): """Say hello to someone.""" message = f"Hello, {name}!" if loud: message = message.upper() for _ in range(count): typer.echo(message) if __name__ == "__main__": app() ``` ## Command Groups ```python import typer app = typer.Typer() users_app = typer.Typer(help="User management commands") app.add_typer(users_app, name="users") @users_app.command("list") def list_users(): """List all users.""" typer.echo("Listing users...") @users_app.command("create") def create_user(name: str, email: str): """Create a new user.""" typer.echo(f"Creating user: {name} <{email}>") @app.command() def version(): """Show version.""" typer.echo("1.0.0") # Usage: myapp users list # myapp users create "John" "john@example.com" # myapp version ``` ## Rich Output ```python from rich.console import Console from rich.table import Table from rich.progress import track from rich.panel import Panel import typer console = Console() @app.command() def show_users(): """Display users in a table.""" table = Table(title="Users") table.add_column("ID", style="cyan") table.add_column("Name", style="green") table.add_column("Email") users = [ (1, "Alice", "alice@example.com"), (2, "Bob", "bob@example.com"), ] for id, name, email in users: table.add_row(str(id), name, email) console.print(table) @app.command() def process(): """Process items with progress bar.""" items = list(range(100)) for item in track(items, description="Processing..."): do_something(item) console.print("[green]Done![/green]") ``` ## Error Handling ```python import typer from rich.console import Console console = Console() def error(message: str, code: int = 1): """Print error and exit.""" console.print(f"[red]Error:[/red] {message}") raise typer.Exit(code) @app.command() def process(file: str): """Process a file.""" if not os.path.exists(file): error(f"File not found: {file}") try: result = process_file(file) console.print(f"[green]Success:[/green] {result}") except ValueError as e: error(str(e)) ``` ## Quick Reference | Feature | Typer Syntax | |---------|--------------| | Required arg | `name: str` | | Optional arg | `name: str = "default"` | | Option | `typer.Option(default, "--flag", "-f")` | | Argument | `typer.Argument(..., help="...")` | | Boolean flag | `verbose: bool = False` | | Enum choice | `color: Color = Color.red` | | Rich Feature | Usage | |--------------|-------| | Table | `Table()` + `add_column/row` | | Progress | `track(items)` | | Colors | `[red]text[/red]` | | Panel | `Panel("content", title="Title")` | ## Additional Resources - `./references/typer-patterns.md` - Advanced Typer patterns - `./references/rich-output.md` - Rich tables, progress, formatting - `./references/configuration.md` - Config files, environment variables ## Assets - `./assets/cli-template.py` - Full CLI application template --- ## See Also **Related Skills:** - `python-typing-ops` - Type hints for CLI arguments - `python-observability-ops` - Logging for CLI applications **Complementary Skills:** - `python-env` - Package CLI for distribution
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.