Claude Skill

python-cli-ops

CLI application patterns for Python. Triggers on: cli, command line, typer, click, argparse, terminal, rich, console, terminal ui.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_python-cli-ops-3dfaf0b.zip · 11 KB
Part of 0xdarkmatter/claude-mods — 94 skills

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 arguments
  • python-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.

No comments yet.

Reviews (0)

No reviews yet.

Related