返回 Skill 列表
extension
分类: 开发与工程无需 API Key

python-modern-cli

使用pyfiglet(ASCII横幅)、typer(命令)、questionary(交互)和rich(格式化)等现代用户体验模式构建专业的Python命令行界面。适用于创建命令行工具、具有用户交互的自动化脚本、带有CLI接口的数据处理流水线,或将现有的Python脚本升级为专业级CLI。非常适合ETL工作流、GIS工具、数据分析实用程序以及需要可重复、可脚本化的界面的公民技术项目。

person作者: jakexiaohubgithub

Python Modern CLI

Build professional command-line interfaces in Python combining visual appeal, interactive UX, and clear output formatting.

Quick Start

Copy the base template from assets/cli_template.py as starting point. Install dependencies:

pip install typer[all] rich questionary pyfiglet --break-system-packages

Basic structure:

import typer
from rich.console import Console
from pyfiglet import figlet_format
import questionary

app = typer.Typer()
console = Console()

def show_banner(title: str):
    console.print(figlet_format(title, font="slant"), style="bold cyan")

@app.command()
def process(
    input_file: str = typer.Argument(..., help="Input file path"),
    output: str = typer.Option("output.csv", "--output", "-o"),
    verbose: bool = typer.Option(False, "--verbose", "-v")
):
    """Process data with clear feedback"""
    if verbose:
        show_banner("My Tool")
    
    console.print(f"[green]Processing {input_file}...[/green]")
    # ... processing logic ...
    console.print("[bold green]✓ Done![/bold green]")

if __name__ == "__main__":
    app()

Core Components

1. Pyfiglet - Visual Identity

Create memorable ASCII art banners. Use sparingly for main commands or when --verbose.

from pyfiglet import figlet_format
from rich.console import Console

console = Console()

# Simple banner
banner = figlet_format("Tool Name", font="slant")
console.print(banner, style="bold cyan")

# Common fonts: slant, banner, digital, standard

2. Typer - Command Structure

Define commands, arguments, options with type hints and validation.

import typer
from pathlib import Path
from typing import Optional

app = typer.Typer()

@app.command()
def convert(
    input_file: Path = typer.Argument(..., exists=True, help="Input CSV"),
    format: str = typer.Option("geojson", "--format", "-f", 
                               help="Output format"),
    dry_run: bool = typer.Option(False, "--dry-run")
):
    """Convert data to specified format"""
    pass

# Multiple commands
data_app = typer.Typer()
app.add_typer(data_app, name="data")

@data_app.command("import")
def import_data(source: str):
    """Import from source"""
    pass

3. Questionary - User Interaction

Interactive prompts when flags/args are insufficient or for confirmations.

import questionary

# Text input with validation
def validate_url(text):
    return text.startswith("http") or "URL must start with http"

url = questionary.text(
    "API endpoint:",
    validate=validate_url
).ask()

# Selection
operation = questionary.select(
    "Choose operation:",
    choices=["Extract", "Transform", "Load"]
).ask()

# Autocomplete for known values
dataset = questionary.autocomplete(
    "Select dataset:",
    choices=["istat_pop", "anac_contracts", "geo_comuni"]
).ask()

# Confirmation before destructive operations
if questionary.confirm("Delete all data?").ask():
    # proceed

# Multiple selection
features = questionary.checkbox(
    'Enable features:',
    choices=['Cache', 'Logging', 'Validation']
).ask()

4. Rich - Output Formatting

Tables, progress bars, colored messages, panels for structured output.

from rich.console import Console
from rich.table import Table
from rich.progress import Progress
from rich.panel import Panel

console = Console()

# Tables for data display
table = Table(title="Processing Results")
table.add_column("File", style="cyan")
table.add_column("Records", justify="right", style="green")
table.add_column("Status", justify="center")

for file in results:
    table.add_row(file.name, str(file.count), "✓")
console.print(table)

# Progress for long operations
with Progress() as progress:
    task = progress.add_task("[cyan]Processing files...", total=len(files))
    for file in files:
        process_file(file)
        progress.update(task, advance=1)

# Panels for important messages
console.print(Panel(
    "[bold]Configuration saved[/bold]\nLocation: ~/.myapp/config.json",
    title="Success",
    border_style="green"
))

# Errors
console.print(Panel(
    f"[bold red]Error:[/bold red] Invalid file format",
    border_style="red"
))

Design Patterns

ETL Pipeline CLI

@app.command()
def pipeline(
    source: str = typer.Argument(..., help="Data source URL"),
    output_dir: Path = typer.Option("./output", "--output", "-o"),
    validate: bool = typer.Option(True, "--validate/--no-validate")
):
    """Run complete ETL pipeline"""
    show_banner("ETL Pipeline")
    
    steps = ["Extract", "Transform", "Validate", "Load"]
    with Progress() as progress:
        task = progress.add_task("[cyan]Pipeline", total=len(steps))
        
        # Extract
        data = extract_data(source)
        progress.update(task, advance=1, description="[cyan]Extracting...")
        
        # Transform
        transformed = transform(data)
        progress.update(task, advance=1, description="[cyan]Transforming...")
        
        if validate:
            errors = validate_data(transformed)
            progress.update(task, advance=1)
            if errors:
                console.print(f"[yellow]⚠ {len(errors)} validation errors[/yellow]")
        
        # Load
        save_data(transformed, output_dir)
        progress.update(task, advance=1)
    
    console.print("[bold green]✓ Pipeline completed[/bold green]")

Interactive Configuration

@app.command()
def configure():
    """Interactive configuration setup"""
    show_banner("Configuration")
    
    config = {
        "api_url": questionary.text(
            "API endpoint:",
            default="https://api.example.com"
        ).ask(),
        
        "format": questionary.select(
            "Default output format:",
            choices=["csv", "geojson", "parquet"]
        ).ask(),
        
        "cache": questionary.confirm(
            "Enable caching?",
            default=True
        ).ask()
    }
    
    # Display configuration
    table = Table(title="Configuration")
    table.add_column("Setting", style="cyan")
    table.add_column("Value", style="green")
    
    for key, value in config.items():
        table.add_row(key, str(value))
    
    console.print(table)
    
    if questionary.confirm("Save configuration?").ask():
        save_config(config)
        console.print("[green]✓ Configuration saved[/green]")

Batch Processing with Feedback

@app.command()
def batch(
    pattern: str = typer.Argument(..., help="File pattern (e.g., '*.csv')"),
    operation: str = typer.Option("validate", help="Operation to perform")
):
    """Process multiple files with detailed feedback"""
    files = list(Path(".").glob(pattern))
    
    if not files:
        console.print(f"[yellow]No files matching '{pattern}'[/yellow]")
        raise typer.Exit(1)
    
    console.print(f"Found {len(files)} files")
    
    results = []
    with Progress() as progress:
        task = progress.add_task("[cyan]Processing", total=len(files))
        
        for file in files:
            try:
                result = process_file(file, operation)
                results.append(("✓", file.name, result))
            except Exception as e:
                results.append(("✗", file.name, str(e)))
            progress.update(task, advance=1)
    
    # Summary table
    table = Table(title="Results")
    table.add_column("Status", justify="center")
    table.add_column("File", style="cyan")
    table.add_column("Result")
    
    for status, name, result in results:
        style = "green" if status == "✓" else "red"
        table.add_row(f"[{style}]{status}[/{style}]", name, result)
    
    console.print(table)

Key Principles

  1. Progressive verbosity: Use banners/formatting only when useful. Support --verbose/--quiet flags.
  2. Clear feedback: Always show progress for operations >2 seconds. Use colors consistently.
  3. Fail gracefully: Validate inputs early. Show actionable error messages with suggestions.
  4. Scriptable: Support non-interactive mode with all flags. Output machine-readable formats on request.
  5. Reproducible: Log commands with full parameters. Support config files for repeated workflows.

Advanced Patterns

For complex scenarios (multi-command apps, context state, custom styling, testing), see references/advanced_patterns.md.

Common advanced needs:

  • Subcommands and command groups
  • Shared state across commands
  • Custom questionary styling
  • Configuration file management
  • Progress for streaming data
  • CLI testing with CliRunner

Template Usage

The assets/cli_template.py provides a working example demonstrating all four components. Copy and adapt:

cp assets/cli_template.py my_tool.py
chmod +x my_tool.py
./my_tool.py --help

Modify the commands, add your logic, keep the UX patterns.

Source

Based on the approach shared by Gaël PENESSOT: LinkedIn post