Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:04:14 +08:00
commit 70c36b5eff
248 changed files with 47482 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
---
name: click-patterns
description: Click framework examples and templates - decorators, nested commands, parameter validation. Use when building Python CLI with Click, implementing command groups, adding CLI options/arguments, validating CLI parameters, creating nested subcommands, or when user mentions Click framework, @click decorators, command-line interface.
allowed-tools: Read, Write, Bash
---
# Click Framework Patterns
This skill provides comprehensive Click framework patterns, templates, and examples for building production-ready Python CLIs.
## Instructions
### When Building a Click CLI
1. Read the appropriate template based on complexity:
- Simple CLI: `templates/basic-cli.py`
- Nested commands: `templates/nested-commands.py`
- Custom validators: `templates/validators.py`
2. Generate new Click project:
```bash
bash scripts/generate-click-cli.sh <project-name> <cli-type>
```
Where cli-type is: basic, nested, or advanced
3. Study complete examples:
- `examples/complete-example.md` - Full-featured CLI
- `examples/patterns.md` - Common patterns and best practices
4. Validate your Click setup:
```bash
bash scripts/validate-click.sh <cli-file.py>
```
### Core Click Patterns
**Command Groups:**
```python
@click.group()
def cli():
"""Main CLI entry point"""
pass
@cli.command()
def subcommand():
"""A subcommand"""
pass
```
**Options and Arguments:**
```python
@click.option('--template', '-t', default='basic', help='Template name')
@click.argument('environment')
def deploy(template, environment):
pass
```
**Nested Groups:**
```python
@cli.group()
def config():
"""Configuration management"""
pass
@config.command()
def get():
"""Get config value"""
pass
```
**Parameter Validation:**
```python
@click.option('--mode', type=click.Choice(['fast', 'safe', 'rollback']))
@click.option('--count', type=click.IntRange(1, 100))
def command(mode, count):
pass
```
### Available Templates
1. **basic-cli.py** - Simple single-command CLI
2. **nested-commands.py** - Command groups and subcommands
3. **validators.py** - Custom parameter validators
4. **advanced-cli.py** - Advanced patterns with plugins and chaining
### Available Scripts
1. **generate-click-cli.sh** - Creates Click project structure
2. **validate-click.sh** - Validates Click CLI implementation
3. **setup-click-project.sh** - Setup dependencies and environment
### Available Examples
1. **complete-example.md** - Production-ready Click CLI
2. **patterns.md** - Best practices and common patterns
3. **edge-cases.md** - Edge cases and solutions
## Requirements
- Python 3.8+
- Click 8.0+ (`pip install click`)
- Rich for colored output (`pip install rich`)
## Best Practices
1. **Use command groups** for organizing related commands
2. **Add help text** to all commands and options
3. **Validate parameters** using Click's built-in validators
4. **Use context** (@click.pass_context) for sharing state
5. **Handle errors gracefully** with try-except blocks
6. **Add version info** with @click.version_option()
7. **Use Rich** for beautiful colored output
## Common Use Cases
- Building CLI tools with multiple commands
- Creating deployment scripts with options
- Implementing configuration management CLIs
- Building database migration tools
- Creating API testing CLIs
- Implementing project scaffolding tools
---
**Purpose:** Provide Click framework templates and patterns for Python CLI development
**Load when:** Building Click CLIs, implementing command groups, or validating CLI parameters

View File

@@ -0,0 +1,405 @@
# Complete Click CLI Example
A production-ready Click CLI demonstrating all major patterns and best practices.
## Full Implementation
```python
#!/usr/bin/env python3
"""
Production-ready Click CLI with all major patterns.
Features:
- Command groups and nested subcommands
- Options and arguments with validation
- Context sharing across commands
- Error handling and colored output
- Configuration management
- Environment-specific commands
"""
import click
from rich.console import Console
from pathlib import Path
import json
console = Console()
# Custom validators
def validate_email(ctx, param, value):
"""Validate email format"""
import re
if value and not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', value):
raise click.BadParameter('Invalid email format')
return value
# Main CLI group
@click.group()
@click.version_option(version='1.0.0')
@click.option('--config', type=click.Path(), default='config.json',
help='Configuration file path')
@click.option('--verbose', '-v', is_flag=True, help='Verbose output')
@click.pass_context
def cli(ctx, config, verbose):
"""
A powerful CLI tool for project management.
Examples:
cli init --template basic
cli deploy production --mode safe
cli config get api-key
cli database migrate --create-tables
"""
ctx.ensure_object(dict)
ctx.obj['console'] = console
ctx.obj['verbose'] = verbose
ctx.obj['config_file'] = config
if verbose:
console.print(f"[dim]Config file: {config}[/dim]")
# Initialize command
@cli.command()
@click.option('--template', '-t',
type=click.Choice(['basic', 'advanced', 'minimal']),
default='basic',
help='Project template')
@click.option('--name', prompt=True, help='Project name')
@click.option('--description', prompt=True, help='Project description')
@click.pass_context
def init(ctx, template, name, description):
"""Initialize a new project"""
console = ctx.obj['console']
verbose = ctx.obj['verbose']
console.print(f"[cyan]Initializing project: {name}[/cyan]")
console.print(f"[dim]Template: {template}[/dim]")
console.print(f"[dim]Description: {description}[/dim]")
# Create project structure
project_dir = Path(name)
if project_dir.exists():
console.print(f"[red]✗[/red] Directory already exists: {name}")
raise click.Abort()
try:
project_dir.mkdir(parents=True)
(project_dir / 'src').mkdir()
(project_dir / 'tests').mkdir()
(project_dir / 'docs').mkdir()
# Create config file
config = {
'name': name,
'description': description,
'template': template,
'version': '1.0.0'
}
with open(project_dir / 'config.json', 'w') as f:
json.dump(config, f, indent=2)
console.print(f"[green]✓[/green] Project initialized successfully!")
if verbose:
console.print(f"[dim]Created directories: src/, tests/, docs/[/dim]")
console.print(f"[dim]Created config.json[/dim]")
except Exception as e:
console.print(f"[red]✗[/red] Error: {e}")
raise click.Abort()
# Deploy command
@cli.command()
@click.argument('environment',
type=click.Choice(['dev', 'staging', 'production']))
@click.option('--force', '-f', is_flag=True, help='Force deployment')
@click.option('--mode', '-m',
type=click.Choice(['fast', 'safe', 'rollback']),
default='safe',
help='Deployment mode')
@click.option('--skip-tests', is_flag=True, help='Skip test execution')
@click.pass_context
def deploy(ctx, environment, force, mode, skip_tests):
"""Deploy to specified environment"""
console = ctx.obj['console']
verbose = ctx.obj['verbose']
console.print(f"[cyan]Deploying to {environment} in {mode} mode[/cyan]")
if force:
console.print("[yellow]⚠ Force mode enabled - skipping safety checks[/yellow]")
# Pre-deployment checks
if not skip_tests and not force:
console.print("[dim]Running tests...[/dim]")
# Simulate test execution
if verbose:
console.print("[green]✓[/green] All tests passed")
# Deployment simulation
steps = [
"Building artifacts",
"Uploading to server",
"Running migrations",
"Restarting services",
"Verifying deployment"
]
for step in steps:
console.print(f"[dim]- {step}...[/dim]")
console.print(f"[green]✓[/green] Deployment completed successfully!")
if mode == 'safe':
console.print("[dim]Rollback available for 24 hours[/dim]")
# Config group
@cli.group()
def config():
"""Manage configuration settings"""
pass
@config.command()
@click.argument('key')
@click.pass_context
def get(ctx, key):
"""Get configuration value"""
console = ctx.obj['console']
config_file = ctx.obj['config_file']
try:
if Path(config_file).exists():
with open(config_file) as f:
config_data = json.load(f)
if key in config_data:
console.print(f"[dim]Config[/dim] {key}: [green]{config_data[key]}[/green]")
else:
console.print(f"[yellow]Key not found: {key}[/yellow]")
else:
console.print(f"[red]Config file not found: {config_file}[/red]")
except Exception as e:
console.print(f"[red]Error reading config: {e}[/red]")
@config.command()
@click.argument('key')
@click.argument('value')
@click.pass_context
def set(ctx, key, value):
"""Set configuration value"""
console = ctx.obj['console']
config_file = ctx.obj['config_file']
try:
config_data = {}
if Path(config_file).exists():
with open(config_file) as f:
config_data = json.load(f)
config_data[key] = value
with open(config_file, 'w') as f:
json.dump(config_data, f, indent=2)
console.print(f"[green]✓[/green] Set {key} = {value}")
except Exception as e:
console.print(f"[red]Error writing config: {e}[/red]")
@config.command()
@click.pass_context
def list(ctx):
"""List all configuration settings"""
console = ctx.obj['console']
config_file = ctx.obj['config_file']
try:
if Path(config_file).exists():
with open(config_file) as f:
config_data = json.load(f)
console.print("[cyan]Configuration Settings:[/cyan]")
for key, value in config_data.items():
console.print(f" {key}: [green]{value}[/green]")
else:
console.print("[yellow]No configuration file found[/yellow]")
except Exception as e:
console.print(f"[red]Error reading config: {e}[/red]")
# Database group
@cli.group()
def database():
"""Database management commands"""
pass
@database.command()
@click.option('--create-tables', is_flag=True, help='Create tables')
@click.option('--seed-data', is_flag=True, help='Seed initial data')
@click.pass_context
def migrate(ctx, create_tables, seed_data):
"""Run database migrations"""
console = ctx.obj['console']
console.print("[cyan]Running migrations...[/cyan]")
if create_tables:
console.print("[dim]- Creating tables...[/dim]")
console.print("[green]✓[/green] Tables created")
if seed_data:
console.print("[dim]- Seeding data...[/dim]")
console.print("[green]✓[/green] Data seeded")
console.print("[green]✓[/green] Migrations completed")
@database.command()
@click.option('--confirm', is_flag=True,
prompt='This will delete all data. Continue?',
help='Confirm reset')
@click.pass_context
def reset(ctx, confirm):
"""Reset database (destructive operation)"""
console = ctx.obj['console']
if not confirm:
console.print("[yellow]Operation cancelled[/yellow]")
raise click.Abort()
console.print("[red]Resetting database...[/red]")
console.print("[dim]- Dropping tables...[/dim]")
console.print("[dim]- Clearing cache...[/dim]")
console.print("[green]✓[/green] Database reset completed")
# User management group
@cli.group()
def user():
"""User management commands"""
pass
@user.command()
@click.option('--email', callback=validate_email, prompt=True,
help='User email address')
@click.option('--name', prompt=True, help='User full name')
@click.option('--role',
type=click.Choice(['admin', 'user', 'guest']),
default='user',
help='User role')
@click.pass_context
def create(ctx, email, name, role):
"""Create a new user"""
console = ctx.obj['console']
console.print(f"[cyan]Creating user: {name}[/cyan]")
console.print(f"[dim]Email: {email}[/dim]")
console.print(f"[dim]Role: {role}[/dim]")
console.print(f"[green]✓[/green] User created successfully")
@user.command()
@click.argument('email')
@click.pass_context
def delete(ctx, email):
"""Delete a user"""
console = ctx.obj['console']
if not click.confirm(f"Delete user {email}?"):
console.print("[yellow]Operation cancelled[/yellow]")
return
console.print(f"[cyan]Deleting user: {email}[/cyan]")
console.print(f"[green]✓[/green] User deleted")
# Error handling wrapper
def main():
"""Main entry point with error handling"""
try:
cli(obj={})
except click.Abort:
console.print("[yellow]Operation aborted[/yellow]")
except Exception as e:
console.print(f"[red]Unexpected error: {e}[/red]")
raise
if __name__ == '__main__':
main()
```
## Usage Examples
### Initialize a project
```bash
python cli.py init --template advanced --name myproject --description "My awesome project"
```
### Deploy to production
```bash
python cli.py deploy production --mode safe
python cli.py deploy staging --force --skip-tests
```
### Configuration management
```bash
python cli.py config set api-key abc123
python cli.py config get api-key
python cli.py config list
```
### Database operations
```bash
python cli.py database migrate --create-tables --seed-data
python cli.py database reset --confirm
```
### User management
```bash
python cli.py user create --email user@example.com --name "John Doe" --role admin
python cli.py user delete user@example.com
```
### With verbose output
```bash
python cli.py --verbose deploy production
```
### With custom config file
```bash
python cli.py --config /path/to/config.json config list
```
## Key Features Demonstrated
1. **Command Groups**: Organized commands into logical groups (config, database, user)
2. **Context Sharing**: Using @click.pass_context to share state
3. **Input Validation**: Custom validators for email, built-in validators for choices
4. **Colored Output**: Using Rich console for beautiful output
5. **Error Handling**: Graceful error handling and user feedback
6. **Interactive Prompts**: Using prompt=True for interactive input
7. **Confirmation Dialogs**: Using click.confirm() for dangerous operations
8. **File Operations**: Reading/writing JSON configuration files
9. **Flags and Options**: Boolean flags, default values, short flags
10. **Version Information**: @click.version_option() decorator
## Best Practices Applied
- Clear help text for all commands and options
- Sensible defaults for options
- Validation for user inputs
- Colored output for better UX
- Verbose mode for debugging
- Confirmation for destructive operations
- Proper error handling and messages
- Clean separation of concerns with command groups
- Context object for sharing state

View File

@@ -0,0 +1,482 @@
# Click Framework Edge Cases and Solutions
Common edge cases, gotchas, and their solutions when working with Click.
## Table of Contents
1. [Parameter Handling Edge Cases](#parameter-handling-edge-cases)
2. [Context and State Edge Cases](#context-and-state-edge-cases)
3. [Error Handling Edge Cases](#error-handling-edge-cases)
4. [Testing Edge Cases](#testing-edge-cases)
5. [Platform-Specific Edge Cases](#platform-specific-edge-cases)
---
## Parameter Handling Edge Cases
### Case 1: Multiple Values with Same Option
**Problem**: User specifies the same option multiple times
```bash
cli --tag python --tag docker --tag kubernetes
```
**Solution**: Use `multiple=True`
```python
@click.option('--tag', multiple=True)
def command(tag):
"""Handle multiple values"""
for t in tag:
click.echo(t)
```
### Case 2: Option vs Argument Ambiguity
**Problem**: Argument that looks like an option
```bash
cli process --file=-myfile.txt # -myfile.txt looks like option
```
**Solution**: Use `--` separator or quotes
```python
@click.command()
@click.argument('filename')
def process(filename):
pass
# Usage:
# cli process -- -myfile.txt
# cli process "-myfile.txt"
```
### Case 3: Empty String vs None
**Problem**: Distinguishing between no value and empty string
```python
@click.option('--name')
def command(name):
# name is None when not provided
# name is '' when provided as empty
if name is None:
click.echo('Not provided')
elif name == '':
click.echo('Empty string provided')
```
**Solution**: Use callback for custom handling
```python
def handle_empty(ctx, param, value):
if value == '':
return None # Treat empty as None
return value
@click.option('--name', callback=handle_empty)
def command(name):
pass
```
### Case 4: Boolean Flag with Default True
**Problem**: Need a flag that's True by default, but can be disabled
```python
# Wrong approach:
@click.option('--enable', is_flag=True, default=True) # Doesn't work as expected
# Correct approach:
@click.option('--disable', is_flag=True)
def command(disable):
enabled = not disable
```
**Better Solution**: Use flag_value
```python
@click.option('--ssl/--no-ssl', default=True)
def command(ssl):
"""SSL is enabled by default, use --no-ssl to disable"""
pass
```
### Case 5: Required Option with Environment Variable
**Problem**: Make option required unless env var is set
```python
def require_if_no_env(ctx, param, value):
"""Require option if environment variable not set"""
if value is None:
import os
env_value = os.getenv('API_KEY')
if env_value:
return env_value
raise click.MissingParameter(param=param)
return value
@click.option('--api-key', callback=require_if_no_env)
def command(api_key):
pass
```
---
## Context and State Edge Cases
### Case 6: Context Not Available in Callbacks
**Problem**: Need context in parameter callback
```python
# This doesn't work - context not yet initialized:
def my_callback(ctx, param, value):
config = ctx.obj['config'] # Error: ctx.obj is None
return value
```
**Solution**: Use command decorator to set up context first
```python
@click.command()
@click.pass_context
def cli(ctx):
ctx.ensure_object(dict)
ctx.obj['config'] = load_config()
@cli.command()
@click.option('--value', callback=validate_with_config)
@click.pass_context
def subcommand(ctx, value):
# Now ctx.obj is available
pass
```
### Case 7: Sharing State Between Command Groups
**Problem**: State not persisting across nested groups
```python
@click.group()
@click.pass_context
def cli(ctx):
ctx.ensure_object(dict)
ctx.obj['data'] = 'test'
@cli.group()
@click.pass_context
def subgroup(ctx):
# ctx.obj is still available here
assert ctx.obj['data'] == 'test'
@subgroup.command()
@click.pass_context
def command(ctx):
# ctx.obj is still available here too
assert ctx.obj['data'] == 'test'
```
### Case 8: Mutating Context Objects
**Problem**: Changes to context not persisting
```python
# This works:
ctx.obj['key'] = 'value' # Modifying dict
# This doesn't persist:
ctx.obj = {'key': 'value'} # Replacing dict
```
---
## Error Handling Edge Cases
### Case 9: Graceful Handling of Ctrl+C
**Problem**: Ugly traceback on keyboard interrupt
```python
def main():
try:
cli()
except KeyboardInterrupt:
click.echo('\n\nOperation cancelled by user')
raise SystemExit(130) # Standard exit code for Ctrl+C
if __name__ == '__main__':
main()
```
### Case 10: Custom Error Messages for Validation
**Problem**: Default error messages aren't user-friendly
```python
# Default error:
@click.option('--port', type=click.IntRange(1, 65535))
# Error: Invalid value for '--port': 70000 is not in the range 1<=x<=65535
# Custom error:
def validate_port(ctx, param, value):
if not 1 <= value <= 65535:
raise click.BadParameter(
f'Port {value} is out of range. Please use a port between 1 and 65535.'
)
return value
@click.option('--port', type=int, callback=validate_port)
```
### Case 11: Handling Mutually Exclusive Options
**Problem**: Options that can't be used together
```python
def validate_exclusive(ctx, param, value):
"""Ensure mutually exclusive options"""
if value and ctx.params.get('other_option'):
raise click.UsageError(
'Cannot use --option and --other-option together'
)
return value
@click.command()
@click.option('--option', callback=validate_exclusive)
@click.option('--other-option')
def command(option, other_option):
pass
```
### Case 12: Dependent Options
**Problem**: One option requires another
```python
@click.command()
@click.option('--ssl', is_flag=True)
@click.option('--cert', type=click.Path(exists=True))
@click.option('--key', type=click.Path(exists=True))
def server(ssl, cert, key):
"""Validate dependent options"""
if ssl:
if not cert or not key:
raise click.UsageError(
'--ssl requires both --cert and --key'
)
```
---
## Testing Edge Cases
### Case 13: Testing with Environment Variables
**Problem**: Tests failing due to environment pollution
```python
def test_with_clean_env():
"""Test with isolated environment"""
runner = CliRunner()
# This isolates environment variables:
result = runner.invoke(
cli,
['command'],
env={'API_KEY': 'test'},
catch_exceptions=False
)
assert result.exit_code == 0
```
### Case 14: Testing Interactive Prompts with Validation
**Problem**: Prompts with retry logic
```python
def test_interactive_retry():
"""Test prompt with retry on invalid input"""
runner = CliRunner()
# Provide multiple inputs (first invalid, second valid)
result = runner.invoke(
cli,
['create'],
input='invalid-email\nvalid@email.com\n'
)
assert 'Invalid email' in result.output
assert result.exit_code == 0
```
### Case 15: Testing File Operations
**Problem**: Tests creating actual files
```python
def test_file_operations():
"""Test with isolated filesystem"""
runner = CliRunner()
with runner.isolated_filesystem():
# Create test file
with open('input.txt', 'w') as f:
f.write('test data')
result = runner.invoke(cli, ['process', 'input.txt'])
# Verify output file
assert Path('output.txt').exists()
```
---
## Platform-Specific Edge Cases
### Case 16: Windows Path Handling
**Problem**: Backslashes in Windows paths
```python
@click.option('--path', type=click.Path())
def command(path):
# Use pathlib for cross-platform compatibility
from pathlib import Path
p = Path(path) # Handles Windows/Unix paths
```
### Case 17: Unicode in Command Line Arguments
**Problem**: Non-ASCII characters in arguments
```python
@click.command()
@click.argument('name')
def greet(name):
"""Handle unicode properly"""
# Click handles unicode automatically on Python 3
click.echo(f'Hello, {name}!')
# This works:
# cli greet "José"
# cli greet "北京"
```
### Case 18: Terminal Width Detection
**Problem**: Output formatting for different terminal sizes
```python
@click.command()
def status():
"""Adapt to terminal width"""
terminal_width = click.get_terminal_size()[0]
if terminal_width < 80:
# Compact output for narrow terminals
click.echo('Status: OK')
else:
# Detailed output for wide terminals
click.echo('=' * terminal_width)
click.echo('Detailed Status Information')
click.echo('=' * terminal_width)
```
---
## Advanced Edge Cases
### Case 19: Dynamic Command Registration
**Problem**: Register commands at runtime
```python
class DynamicGroup(click.Group):
"""Group that discovers commands dynamically"""
def list_commands(self, ctx):
"""List available commands"""
# Dynamically discover commands
return ['cmd1', 'cmd2', 'cmd3']
def get_command(self, ctx, name):
"""Load command on demand"""
if name in self.list_commands(ctx):
# Import and return command
module = __import__(f'commands.{name}')
return getattr(module, name).cli
return None
@click.command(cls=DynamicGroup)
def cli():
pass
```
### Case 20: Command Aliases
**Problem**: Support command aliases
```python
class AliasedGroup(click.Group):
"""Group that supports command aliases"""
def get_command(self, ctx, cmd_name):
"""Resolve aliases"""
aliases = {
'ls': 'list',
'rm': 'remove',
'cp': 'copy'
}
# Resolve alias
resolved = aliases.get(cmd_name, cmd_name)
return super().get_command(ctx, resolved)
@click.group(cls=AliasedGroup)
def cli():
pass
@cli.command()
def list():
"""List items (alias: ls)"""
pass
```
### Case 21: Progress Bar with Unknown Length
**Problem**: Show progress when total is unknown
```python
@click.command()
def process():
"""Process with indeterminate progress"""
import time
# For unknown length, use length=None
with click.progressbar(
range(100),
length=None,
label='Processing'
) as bar:
for _ in bar:
time.sleep(0.1)
```
---
## Summary
Key takeaways for handling edge cases:
1. **Parameters**: Use callbacks and custom types for complex validation
2. **Context**: Ensure context is initialized before accessing ctx.obj
3. **Errors**: Provide clear, actionable error messages
4. **Testing**: Use CliRunner's isolation features
5. **Platform**: Use pathlib and Click's built-in utilities for portability
For more edge cases, consult the [Click documentation](https://click.palletsprojects.com/) and [GitHub issues](https://github.com/pallets/click/issues).

View File

@@ -0,0 +1,521 @@
# Click Framework Common Patterns
Best practices and common patterns for building production-ready Click CLIs.
## Table of Contents
1. [Command Structure Patterns](#command-structure-patterns)
2. [Parameter Patterns](#parameter-patterns)
3. [Validation Patterns](#validation-patterns)
4. [Error Handling Patterns](#error-handling-patterns)
5. [Output Patterns](#output-patterns)
6. [Configuration Patterns](#configuration-patterns)
7. [Testing Patterns](#testing-patterns)
---
## Command Structure Patterns
### Single Command CLI
For simple tools with one main function:
```python
@click.command()
@click.option('--name', default='World')
def hello(name):
"""Simple greeting CLI"""
click.echo(f'Hello, {name}!')
```
### Command Group Pattern
For CLIs with multiple related commands:
```python
@click.group()
def cli():
"""Main CLI entry point"""
pass
@cli.command()
def cmd1():
"""First command"""
pass
@cli.command()
def cmd2():
"""Second command"""
pass
```
### Nested Command Groups
For complex CLIs with logical grouping:
```python
@click.group()
def cli():
"""Main CLI"""
pass
@cli.group()
def database():
"""Database commands"""
pass
@database.command()
def migrate():
"""Run migrations"""
pass
@database.command()
def reset():
"""Reset database"""
pass
```
### Context-Aware Commands
Share state across commands:
```python
@click.group()
@click.pass_context
def cli(ctx):
"""Main CLI with shared context"""
ctx.ensure_object(dict)
ctx.obj['config'] = load_config()
@cli.command()
@click.pass_context
def deploy(ctx):
"""Use shared config"""
config = ctx.obj['config']
```
---
## Parameter Patterns
### Options vs Arguments
**Options** (optional, named):
```python
@click.option('--name', '-n', default='World', help='Name to greet')
@click.option('--count', '-c', default=1, type=int)
```
**Arguments** (required, positional):
```python
@click.argument('filename')
@click.argument('output', type=click.Path())
```
### Required Options
```python
@click.option('--api-key', required=True, help='API key (required)')
@click.option('--config', required=True, type=click.Path(exists=True))
```
### Multiple Values
```python
# Multiple option values
@click.option('--tag', multiple=True, help='Tags (can specify multiple times)')
def command(tag):
for t in tag:
click.echo(t)
# Variable arguments
@click.argument('files', nargs=-1, type=click.Path(exists=True))
def process(files):
for f in files:
click.echo(f)
```
### Environment Variables
```python
@click.option('--api-key', envvar='API_KEY', help='API key (from env: API_KEY)')
@click.option('--debug', envvar='DEBUG', is_flag=True)
```
### Interactive Prompts
```python
@click.option('--name', prompt=True, help='Your name')
@click.option('--password', prompt=True, hide_input=True)
@click.option('--confirm', prompt='Continue?', confirmation_prompt=True)
```
---
## Validation Patterns
### Built-in Validators
```python
# Choice validation
@click.option('--env', type=click.Choice(['dev', 'staging', 'prod']))
# Range validation
@click.option('--port', type=click.IntRange(1, 65535))
@click.option('--rate', type=click.FloatRange(0.0, 1.0))
# Path validation
@click.option('--input', type=click.Path(exists=True, dir_okay=False))
@click.option('--output', type=click.Path(writable=True))
@click.option('--dir', type=click.Path(exists=True, file_okay=False))
```
### Custom Validators with Callbacks
```python
def validate_email(ctx, param, value):
"""Validate email format"""
import re
if value and not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', value):
raise click.BadParameter('Invalid email format')
return value
@click.option('--email', callback=validate_email)
def command(email):
pass
```
### Custom Click Types
```python
class EmailType(click.ParamType):
"""Custom email type"""
name = 'email'
def convert(self, value, param, ctx):
import re
if not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', value):
self.fail(f'{value} is not a valid email', param, ctx)
return value
@click.option('--email', type=EmailType())
def command(email):
pass
```
### Conditional Validation
```python
@click.command()
@click.option('--ssl', is_flag=True)
@click.option('--cert', type=click.Path(exists=True))
@click.option('--key', type=click.Path(exists=True))
def server(ssl, cert, key):
"""Start server with SSL validation"""
if ssl and (not cert or not key):
raise click.UsageError('SSL requires --cert and --key')
```
---
## Error Handling Patterns
### Graceful Error Handling
```python
@click.command()
def command():
"""Command with error handling"""
try:
# Operation that might fail
result = risky_operation()
except FileNotFoundError as e:
raise click.FileError(str(e))
except Exception as e:
raise click.ClickException(f'Operation failed: {e}')
```
### Custom Exit Codes
```python
@click.command()
def deploy():
"""Deploy with custom exit codes"""
if not check_prerequisites():
ctx = click.get_current_context()
ctx.exit(1)
if not deploy_application():
ctx = click.get_current_context()
ctx.exit(2)
click.echo('Deployment successful')
ctx = click.get_current_context()
ctx.exit(0)
```
### Confirmation for Dangerous Operations
```python
@click.command()
@click.option('--force', is_flag=True, help='Skip confirmation')
def delete(force):
"""Delete with confirmation"""
if not force:
click.confirm('This will delete all data. Continue?', abort=True)
# Proceed with deletion
click.echo('Deleting...')
```
---
## Output Patterns
### Colored Output with Click
```python
@click.command()
def status():
"""Show status with colors"""
click.secho('Success!', fg='green', bold=True)
click.secho('Warning!', fg='yellow')
click.secho('Error!', fg='red', bold=True)
click.echo(click.style('Info', fg='cyan'))
```
### Rich Console Integration
```python
from rich.console import Console
console = Console()
@click.command()
def deploy():
"""Deploy with Rich output"""
console.print('[cyan]Starting deployment...[/cyan]')
console.print('[green]✓[/green] Build successful')
console.print('[yellow]⚠[/yellow] Warning: Cache cleared')
```
### Progress Bars
```python
@click.command()
@click.argument('files', nargs=-1)
def process(files):
"""Process with progress bar"""
with click.progressbar(files, label='Processing files') as bar:
for file in bar:
# Process file
time.sleep(0.1)
```
### Verbose Mode Pattern
```python
@click.command()
@click.option('--verbose', '-v', is_flag=True)
def command(verbose):
"""Command with verbose output"""
click.echo('Starting operation...')
if verbose:
click.echo('Debug: Loading configuration')
click.echo('Debug: Connecting to database')
# Main operation
click.echo('Operation completed')
```
---
## Configuration Patterns
### Configuration File Loading
```python
import json
from pathlib import Path
@click.group()
@click.option('--config', type=click.Path(), default='config.json')
@click.pass_context
def cli(ctx, config):
"""CLI with config file"""
ctx.ensure_object(dict)
if Path(config).exists():
with open(config) as f:
ctx.obj['config'] = json.load(f)
else:
ctx.obj['config'] = {}
@cli.command()
@click.pass_context
def deploy(ctx):
"""Use config"""
config = ctx.obj['config']
api_key = config.get('api_key')
```
### Environment-Based Configuration
```python
@click.command()
@click.option('--env', type=click.Choice(['dev', 'staging', 'prod']), default='dev')
def deploy(env):
"""Deploy with environment config"""
config_file = f'config.{env}.json'
with open(config_file) as f:
config = json.load(f)
# Use environment-specific config
```
### Configuration Priority
```python
def get_config_value(ctx, param_value, env_var, config_key, default):
"""Get value with priority: param > env > config > default"""
if param_value:
return param_value
if env_var in os.environ:
return os.environ[env_var]
if config_key in ctx.obj['config']:
return ctx.obj['config'][config_key]
return default
```
---
## Testing Patterns
### Basic Testing with CliRunner
```python
from click.testing import CliRunner
import pytest
def test_command():
"""Test Click command"""
runner = CliRunner()
result = runner.invoke(cli, ['--help'])
assert result.exit_code == 0
assert 'Usage:' in result.output
def test_command_with_args():
"""Test with arguments"""
runner = CliRunner()
result = runner.invoke(cli, ['deploy', 'production'])
assert result.exit_code == 0
assert 'Deploying to production' in result.output
```
### Testing with Temporary Files
```python
def test_with_file():
"""Test with temporary file"""
runner = CliRunner()
with runner.isolated_filesystem():
with open('test.txt', 'w') as f:
f.write('test content')
result = runner.invoke(cli, ['process', 'test.txt'])
assert result.exit_code == 0
```
### Testing Interactive Prompts
```python
def test_interactive():
"""Test interactive prompts"""
runner = CliRunner()
result = runner.invoke(cli, ['create'], input='username\npassword\n')
assert result.exit_code == 0
assert 'User created' in result.output
```
### Testing Environment Variables
```python
def test_with_env():
"""Test with environment variables"""
runner = CliRunner()
result = runner.invoke(cli, ['deploy'], env={'API_KEY': 'test123'})
assert result.exit_code == 0
```
---
## Advanced Patterns
### Plugin System
```python
@click.group()
def cli():
"""CLI with plugin support"""
pass
# Allow plugins to register commands
def register_plugin(group, plugin_name):
"""Register plugin commands"""
plugin_module = importlib.import_module(f'plugins.{plugin_name}')
for name, cmd in plugin_module.commands.items():
group.add_command(cmd, name)
```
### Lazy Loading
```python
class LazyGroup(click.Group):
"""Lazy load commands"""
def get_command(self, ctx, cmd_name):
"""Load command on demand"""
module = importlib.import_module(f'commands.{cmd_name}')
return module.cli
@click.command(cls=LazyGroup)
def cli():
"""CLI with lazy loading"""
pass
```
### Middleware Pattern
```python
def with_database(f):
"""Decorator to inject database connection"""
@click.pass_context
def wrapper(ctx, *args, **kwargs):
ctx.obj['db'] = connect_database()
try:
return f(*args, **kwargs)
finally:
ctx.obj['db'].close()
return wrapper
@cli.command()
@with_database
@click.pass_context
def query(ctx):
"""Command with database"""
db = ctx.obj['db']
```
---
## Summary
These patterns cover the most common use cases for Click CLIs:
1. **Structure**: Choose between single command, command group, or nested groups
2. **Parameters**: Use options for named parameters, arguments for positional
3. **Validation**: Leverage built-in validators or create custom ones
4. **Errors**: Handle errors gracefully with proper messages
5. **Output**: Use colored output and progress bars for better UX
6. **Config**: Load configuration from files with proper priority
7. **Testing**: Test thoroughly with CliRunner
For more patterns and advanced usage, see the [Click documentation](https://click.palletsprojects.com/).

View File

@@ -0,0 +1,334 @@
#!/bin/bash
#
# generate-click-cli.sh - Generate Click CLI project structure
#
# Usage: generate-click-cli.sh <project-name> [cli-type]
# cli-type: basic, nested, or advanced (default: basic)
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Function to print colored output
print_info() { echo -e "${CYAN}${NC} $1"; }
print_success() { echo -e "${GREEN}${NC} $1"; }
print_error() { echo -e "${RED}${NC} $1" >&2; }
print_warning() { echo -e "${YELLOW}${NC} $1"; }
# Validate arguments
if [ $# -lt 1 ]; then
print_error "Usage: $0 <project-name> [cli-type]"
echo " cli-type: basic, nested, or advanced (default: basic)"
exit 1
fi
PROJECT_NAME="$1"
CLI_TYPE="${2:-basic}"
# Validate CLI type
if [[ ! "$CLI_TYPE" =~ ^(basic|nested|advanced)$ ]]; then
print_error "Invalid CLI type: $CLI_TYPE"
echo " Valid types: basic, nested, advanced"
exit 1
fi
# Validate project name
if [[ ! "$PROJECT_NAME" =~ ^[a-z0-9_-]+$ ]]; then
print_error "Invalid project name: $PROJECT_NAME"
echo " Must contain only lowercase letters, numbers, hyphens, and underscores"
exit 1
fi
# Create project directory
if [ -d "$PROJECT_NAME" ]; then
print_error "Directory already exists: $PROJECT_NAME"
exit 1
fi
print_info "Creating Click CLI project: $PROJECT_NAME (type: $CLI_TYPE)"
# Create directory structure
mkdir -p "$PROJECT_NAME"/{src,tests,docs}
# Determine which template to use
SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
TEMPLATE_FILE=""
case "$CLI_TYPE" in
basic)
TEMPLATE_FILE="$SKILL_DIR/templates/basic-cli.py"
;;
nested)
TEMPLATE_FILE="$SKILL_DIR/templates/nested-commands.py"
;;
advanced)
# For advanced, use nested as base with validators
TEMPLATE_FILE="$SKILL_DIR/templates/nested-commands.py"
;;
esac
# Copy template
if [ ! -f "$TEMPLATE_FILE" ]; then
print_error "Template file not found: $TEMPLATE_FILE"
exit 1
fi
cp "$TEMPLATE_FILE" "$PROJECT_NAME/src/cli.py"
print_success "Created src/cli.py from template"
# Copy validators if advanced type
if [ "$CLI_TYPE" = "advanced" ]; then
VALIDATORS_FILE="$SKILL_DIR/templates/validators.py"
if [ -f "$VALIDATORS_FILE" ]; then
cp "$VALIDATORS_FILE" "$PROJECT_NAME/src/validators.py"
print_success "Created src/validators.py"
fi
fi
# Create __init__.py
cat > "$PROJECT_NAME/src/__init__.py" <<'EOF'
"""
CLI application package
"""
from .cli import cli
__version__ = "1.0.0"
__all__ = ["cli"]
EOF
print_success "Created src/__init__.py"
# Create requirements.txt
cat > "$PROJECT_NAME/requirements.txt" <<'EOF'
click>=8.0.0
rich>=13.0.0
EOF
print_success "Created requirements.txt"
# Create setup.py
cat > "$PROJECT_NAME/setup.py" <<EOF
from setuptools import setup, find_packages
setup(
name="${PROJECT_NAME}",
version="1.0.0",
packages=find_packages(),
install_requires=[
"click>=8.0.0",
"rich>=13.0.0",
],
entry_points={
"console_scripts": [
"${PROJECT_NAME}=src.cli:cli",
],
},
python_requires=">=3.8",
)
EOF
print_success "Created setup.py"
# Create pyproject.toml
cat > "$PROJECT_NAME/pyproject.toml" <<EOF
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "${PROJECT_NAME}"
version = "1.0.0"
description = "A Click-based CLI tool"
requires-python = ">=3.8"
dependencies = [
"click>=8.0.0",
"rich>=13.0.0",
]
[project.scripts]
${PROJECT_NAME} = "src.cli:cli"
EOF
print_success "Created pyproject.toml"
# Create README.md
cat > "$PROJECT_NAME/README.md" <<EOF
# ${PROJECT_NAME}
A CLI tool built with Click framework.
## Installation
\`\`\`bash
pip install -e .
\`\`\`
## Usage
\`\`\`bash
# Show help
${PROJECT_NAME} --help
# Run command
${PROJECT_NAME} <command>
\`\`\`
## Development
\`\`\`bash
# Install in development mode
pip install -e .
# Run tests
pytest tests/
# Format code
black src/ tests/
# Lint code
pylint src/ tests/
\`\`\`
## Project Structure
\`\`\`
${PROJECT_NAME}/
├── src/
│ ├── __init__.py
│ └── cli.py # Main CLI implementation
├── tests/
│ └── test_cli.py # Unit tests
├── docs/
│ └── usage.md # Usage documentation
├── requirements.txt # Dependencies
├── setup.py # Setup configuration
└── README.md # This file
\`\`\`
## License
MIT
EOF
print_success "Created README.md"
# Create basic test file
cat > "$PROJECT_NAME/tests/test_cli.py" <<'EOF'
import pytest
from click.testing import CliRunner
from src.cli import cli
def test_cli_help():
"""Test CLI help output"""
runner = CliRunner()
result = runner.invoke(cli, ['--help'])
assert result.exit_code == 0
assert 'Usage:' in result.output
def test_cli_version():
"""Test CLI version output"""
runner = CliRunner()
result = runner.invoke(cli, ['--version'])
assert result.exit_code == 0
assert '1.0.0' in result.output
EOF
print_success "Created tests/test_cli.py"
# Create .gitignore
cat > "$PROJECT_NAME/.gitignore" <<'EOF'
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
venv/
ENV/
env/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Testing
.pytest_cache/
.coverage
htmlcov/
# Environment
.env
.env.local
EOF
print_success "Created .gitignore"
# Create usage documentation
cat > "$PROJECT_NAME/docs/usage.md" <<EOF
# ${PROJECT_NAME} Usage Guide
## Installation
Install the CLI tool:
\`\`\`bash
pip install -e .
\`\`\`
## Commands
### Help
Show available commands:
\`\`\`bash
${PROJECT_NAME} --help
\`\`\`
### Version
Show version information:
\`\`\`bash
${PROJECT_NAME} --version
\`\`\`
## Examples
Add specific examples for your CLI commands here.
EOF
print_success "Created docs/usage.md"
# Print summary
echo ""
print_success "Click CLI project created successfully!"
echo ""
print_info "Next steps:"
echo " 1. cd $PROJECT_NAME"
echo " 2. python -m venv venv"
echo " 3. source venv/bin/activate"
echo " 4. pip install -e ."
echo " 5. $PROJECT_NAME --help"
echo ""
print_info "Project type: $CLI_TYPE"
print_info "Location: $(pwd)/$PROJECT_NAME"

View File

@@ -0,0 +1,108 @@
#!/bin/bash
#
# setup-click-project.sh - Setup Click project dependencies and environment
#
# Usage: setup-click-project.sh [project-directory]
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Function to print colored output
print_info() { echo -e "${CYAN}${NC} $1"; }
print_success() { echo -e "${GREEN}${NC} $1"; }
print_error() { echo -e "${RED}${NC} $1" >&2; }
print_warning() { echo -e "${YELLOW}${NC} $1"; }
PROJECT_DIR="${1:-.}"
print_info "Setting up Click project in: $PROJECT_DIR"
# Check if Python is installed
if ! command -v python3 &> /dev/null; then
print_error "Python 3 is not installed"
exit 1
fi
PYTHON_VERSION=$(python3 --version | cut -d' ' -f2)
print_success "Python $PYTHON_VERSION detected"
# Navigate to project directory
cd "$PROJECT_DIR"
# Check if virtual environment exists
if [ ! -d "venv" ]; then
print_info "Creating virtual environment..."
python3 -m venv venv
print_success "Virtual environment created"
else
print_info "Virtual environment already exists"
fi
# Activate virtual environment
print_info "Activating virtual environment..."
source venv/bin/activate
# Upgrade pip
print_info "Upgrading pip..."
pip install --upgrade pip > /dev/null 2>&1
print_success "pip upgraded"
# Install Click and dependencies
print_info "Installing Click and dependencies..."
if [ -f "requirements.txt" ]; then
pip install -r requirements.txt
print_success "Installed from requirements.txt"
else
pip install click rich
print_success "Installed click and rich"
fi
# Install development dependencies
print_info "Installing development dependencies..."
pip install pytest pytest-cov black pylint mypy
print_success "Development dependencies installed"
# Create .env.example if it doesn't exist
if [ ! -f ".env.example" ]; then
cat > .env.example <<'EOF'
# Environment variables for CLI
API_KEY=your_api_key_here
DEBUG=false
LOG_LEVEL=info
EOF
print_success "Created .env.example"
fi
# Setup pre-commit hooks if git repo
if [ -d ".git" ]; then
print_info "Setting up git hooks..."
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/bash
# Run tests before commit
source venv/bin/activate
black src/ tests/ --check || exit 1
pylint src/ || exit 1
pytest tests/ || exit 1
EOF
chmod +x .git/hooks/pre-commit
print_success "Git hooks configured"
fi
# Verify installation
print_info "Verifying installation..."
python3 -c "import click; print(f'Click version: {click.__version__}')"
print_success "Click is properly installed"
echo ""
print_success "Setup completed successfully!"
echo ""
print_info "Next steps:"
echo " 1. source venv/bin/activate"
echo " 2. python src/cli.py --help"
echo " 3. pytest tests/"

View File

@@ -0,0 +1,162 @@
#!/bin/bash
#
# validate-click.sh - Validate Click CLI implementation
#
# Usage: validate-click.sh <cli-file.py>
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Function to print colored output
print_info() { echo -e "${CYAN}${NC} $1"; }
print_success() { echo -e "${GREEN}${NC} $1"; }
print_error() { echo -e "${RED}${NC} $1" >&2; }
print_warning() { echo -e "${YELLOW}${NC} $1"; }
# Validate arguments
if [ $# -lt 1 ]; then
print_error "Usage: $0 <cli-file.py>"
exit 1
fi
CLI_FILE="$1"
# Check if file exists
if [ ! -f "$CLI_FILE" ]; then
print_error "File not found: $CLI_FILE"
exit 1
fi
print_info "Validating Click CLI: $CLI_FILE"
echo ""
VALIDATION_PASSED=true
# Check 1: File is a Python file
if [[ ! "$CLI_FILE" =~ \.py$ ]]; then
print_error "File must be a Python file (.py)"
VALIDATION_PASSED=false
else
print_success "File extension is valid (.py)"
fi
# Check 2: File imports Click
if grep -q "import click" "$CLI_FILE"; then
print_success "Click module is imported"
else
print_error "Click module is not imported"
VALIDATION_PASSED=false
fi
# Check 3: Has at least one Click decorator
DECORATOR_COUNT=$(grep -c "@click\." "$CLI_FILE" || true)
if [ "$DECORATOR_COUNT" -gt 0 ]; then
print_success "Found $DECORATOR_COUNT Click decorator(s)"
else
print_error "No Click decorators found"
VALIDATION_PASSED=false
fi
# Check 4: Has main entry point or group
if grep -q "@click.command()\|@click.group()" "$CLI_FILE"; then
print_success "Has Click command or group decorator"
else
print_error "Missing @click.command() or @click.group()"
VALIDATION_PASSED=false
fi
# Check 5: Has if __name__ == '__main__' block
if grep -q "if __name__ == '__main__':" "$CLI_FILE"; then
print_success "Has main execution block"
else
print_warning "Missing main execution block (if __name__ == '__main__':)"
fi
# Check 6: Python syntax is valid
if python3 -m py_compile "$CLI_FILE" 2>/dev/null; then
print_success "Python syntax is valid"
else
print_error "Python syntax errors detected"
VALIDATION_PASSED=false
fi
# Check 7: Has help text
if grep -q '"""' "$CLI_FILE"; then
print_success "Contains docstrings/help text"
else
print_warning "No docstrings found (recommended for help text)"
fi
# Check 8: Has option or argument decorators
if grep -q "@click.option\|@click.argument" "$CLI_FILE"; then
print_success "Has options or arguments defined"
else
print_warning "No options or arguments defined"
fi
# Check 9: Uses recommended patterns
echo ""
print_info "Checking best practices..."
# Check for version option
if grep -q "@click.version_option" "$CLI_FILE"; then
print_success "Has version option"
else
print_warning "Consider adding @click.version_option()"
fi
# Check for help parameter
if grep -q "help=" "$CLI_FILE"; then
print_success "Uses help parameters"
else
print_warning "Consider adding help text to options"
fi
# Check for context usage
if grep -q "@click.pass_context" "$CLI_FILE"; then
print_success "Uses context for state sharing"
else
print_info "No context usage detected (optional)"
fi
# Check for command groups
if grep -q "@click.group()" "$CLI_FILE"; then
print_success "Uses command groups"
# Check for subcommands
SUBCOMMAND_COUNT=$(grep -c "\.command()" "$CLI_FILE" || true)
if [ "$SUBCOMMAND_COUNT" -gt 0 ]; then
print_success "Has $SUBCOMMAND_COUNT subcommand(s)"
fi
fi
# Check for validation
if grep -q "click.Choice\|click.IntRange\|click.FloatRange\|click.Path" "$CLI_FILE"; then
print_success "Uses Click's built-in validators"
else
print_info "No built-in validators detected (optional)"
fi
# Check for colored output (Rich or Click's styling)
if grep -q "from rich\|click.style\|click.echo.*fg=" "$CLI_FILE"; then
print_success "Uses colored output"
else
print_info "No colored output detected (optional)"
fi
# Summary
echo ""
if [ "$VALIDATION_PASSED" = true ]; then
print_success "All critical validations passed!"
echo ""
print_info "Try running: python3 $CLI_FILE --help"
exit 0
else
print_error "Validation failed. Please fix the errors above."
exit 1
fi

View File

@@ -0,0 +1,310 @@
#!/usr/bin/env python3
"""
Advanced Click CLI Template
Demonstrates advanced patterns including:
- Custom parameter types
- Command chaining
- Plugin architecture
- Configuration management
- Logging integration
"""
import click
import logging
from rich.console import Console
from pathlib import Path
import json
from typing import Optional
console = Console()
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Custom parameter types
class JsonType(click.ParamType):
"""Custom type for JSON parsing"""
name = 'json'
def convert(self, value, param, ctx):
try:
return json.loads(value)
except json.JSONDecodeError as e:
self.fail(f'Invalid JSON: {e}', param, ctx)
class PathListType(click.ParamType):
"""Custom type for comma-separated paths"""
name = 'pathlist'
def convert(self, value, param, ctx):
paths = [Path(p.strip()) for p in value.split(',')]
for path in paths:
if not path.exists():
self.fail(f'Path does not exist: {path}', param, ctx)
return paths
# Configuration class
class Config:
"""Application configuration"""
def __init__(self):
self.debug = False
self.log_level = 'INFO'
self.config_file = 'config.json'
self._data = {}
def load(self, config_file: Optional[str] = None):
"""Load configuration from file"""
file_path = Path(config_file or self.config_file)
if file_path.exists():
with open(file_path) as f:
self._data = json.load(f)
logger.info(f"Loaded config from {file_path}")
def get(self, key: str, default=None):
"""Get configuration value"""
return self._data.get(key, default)
def set(self, key: str, value):
"""Set configuration value"""
self._data[key] = value
def save(self):
"""Save configuration to file"""
file_path = Path(self.config_file)
with open(file_path, 'w') as f:
json.dump(self._data, f, indent=2)
logger.info(f"Saved config to {file_path}")
# Pass config between commands
pass_config = click.make_pass_decorator(Config, ensure=True)
# Main CLI group
@click.group(chain=True)
@click.option('--debug', is_flag=True, help='Enable debug mode')
@click.option('--config', type=click.Path(), default='config.json',
help='Configuration file')
@click.option('--log-level',
type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'ERROR']),
default='INFO',
help='Logging level')
@click.version_option(version='2.0.0')
@pass_config
def cli(config: Config, debug: bool, config: str, log_level: str):
"""
Advanced CLI with chaining and plugin support.
Commands can be chained together:
cli init process deploy
cli config set key=value process --validate
"""
config.debug = debug
config.log_level = log_level
config.config_file = config
config.load()
# Set logging level
logger.setLevel(getattr(logging, log_level))
if debug:
console.print("[dim]Debug mode enabled[/dim]")
# Pipeline commands (chainable)
@cli.command()
@click.option('--template', type=click.Choice(['basic', 'advanced', 'api']),
default='basic')
@pass_config
def init(config: Config, template: str):
"""Initialize project (chainable)"""
console.print(f"[cyan]Initializing with {template} template...[/cyan]")
config.set('template', template)
return config
@cli.command()
@click.option('--validate', is_flag=True, help='Validate before processing')
@click.option('--parallel', is_flag=True, help='Process in parallel')
@pass_config
def process(config: Config, validate: bool, parallel: bool):
"""Process data (chainable)"""
console.print("[cyan]Processing data...[/cyan]")
if validate:
console.print("[dim]Validating input...[/dim]")
mode = "parallel" if parallel else "sequential"
console.print(f"[dim]Processing mode: {mode}[/dim]")
return config
@cli.command()
@click.argument('environment', type=click.Choice(['dev', 'staging', 'prod']))
@click.option('--dry-run', is_flag=True, help='Simulate deployment')
@pass_config
def deploy(config: Config, environment: str, dry_run: bool):
"""Deploy to environment (chainable)"""
prefix = "[yellow][DRY RUN][/yellow] " if dry_run else ""
console.print(f"{prefix}[cyan]Deploying to {environment}...[/cyan]")
template = config.get('template', 'unknown')
console.print(f"[dim]Template: {template}[/dim]")
return config
# Advanced configuration commands
@cli.group()
def config():
"""Advanced configuration management"""
pass
@config.command()
@click.argument('key')
@pass_config
def get(config: Config, key: str):
"""Get configuration value"""
value = config.get(key)
if value is not None:
console.print(f"{key}: [green]{value}[/green]")
else:
console.print(f"[yellow]Key not found: {key}[/yellow]")
@config.command()
@click.argument('pair')
@pass_config
def set(config: Config, pair: str):
"""Set configuration (format: key=value)"""
if '=' not in pair:
raise click.BadParameter('Format must be key=value')
key, value = pair.split('=', 1)
config.set(key, value)
config.save()
console.print(f"[green]✓[/green] Set {key} = {value}")
@config.command()
@click.option('--format', type=click.Choice(['json', 'yaml', 'env']),
default='json')
@pass_config
def export(config: Config, format: str):
"""Export configuration in different formats"""
console.print(f"[cyan]Exporting config as {format}...[/cyan]")
if format == 'json':
output = json.dumps(config._data, indent=2)
elif format == 'yaml':
# Simplified YAML output
output = '\n'.join(f"{k}: {v}" for k, v in config._data.items())
else: # env
output = '\n'.join(f"{k.upper()}={v}" for k, v in config._data.items())
console.print(output)
# Advanced data operations
@cli.group()
def data():
"""Data operations with advanced types"""
pass
@data.command()
@click.option('--json-data', type=JsonType(), help='JSON data to import')
@click.option('--paths', type=PathListType(), help='Comma-separated paths')
@pass_config
def import_data(config: Config, json_data: Optional[dict], paths: Optional[list]):
"""Import data from various sources"""
console.print("[cyan]Importing data...[/cyan]")
if json_data:
console.print(f"[dim]JSON data: {json_data}[/dim]")
if paths:
console.print(f"[dim]Processing {len(paths)} path(s)[/dim]")
for path in paths:
console.print(f" - {path}")
@data.command()
@click.option('--input', type=click.File('r'), help='Input file')
@click.option('--output', type=click.File('w'), help='Output file')
@click.option('--format',
type=click.Choice(['json', 'csv', 'xml']),
default='json')
def transform(input, output, format):
"""Transform data between formats"""
console.print(f"[cyan]Transforming data to {format}...[/cyan]")
if input:
data = input.read()
console.print(f"[dim]Read {len(data)} bytes[/dim]")
if output:
# Would write transformed data here
output.write('{}') # Placeholder
console.print("[green]✓[/green] Transformation complete")
# Plugin system
@cli.group()
def plugin():
"""Plugin management"""
pass
@plugin.command()
@click.argument('plugin_name')
@click.option('--version', help='Plugin version')
def install(plugin_name: str, version: Optional[str]):
"""Install a plugin"""
version_str = f"@{version}" if version else "@latest"
console.print(f"[cyan]Installing plugin: {plugin_name}{version_str}...[/cyan]")
console.print("[green]✓[/green] Plugin installed successfully")
@plugin.command()
def list():
"""List installed plugins"""
console.print("[cyan]Installed Plugins:[/cyan]")
# Placeholder plugin list
plugins = [
{"name": "auth-plugin", "version": "1.0.0", "status": "active"},
{"name": "database-plugin", "version": "2.1.0", "status": "active"},
]
for p in plugins:
status_color = "green" if p["status"] == "active" else "yellow"
console.print(f" - {p['name']} ({p['version']}) [{status_color}]{p['status']}[/{status_color}]")
# Batch operations
@cli.command()
@click.argument('commands', nargs=-1, required=True)
@pass_config
def batch(config: Config, commands: tuple):
"""Execute multiple commands in batch"""
console.print(f"[cyan]Executing {len(commands)} command(s)...[/cyan]")
for i, cmd in enumerate(commands, 1):
console.print(f"[dim]{i}. {cmd}[/dim]")
# Would execute actual commands here
console.print("[green]✓[/green] Batch execution completed")
if __name__ == '__main__':
cli()

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env python3
"""
Basic Click CLI Template
A simple single-command CLI using Click framework.
"""
import click
from rich.console import Console
console = Console()
@click.command()
@click.version_option(version='1.0.0')
@click.option('--name', '-n', default='World', help='Name to greet')
@click.option('--count', '-c', default=1, type=int, help='Number of greetings')
@click.option('--verbose', '-v', is_flag=True, help='Verbose output')
def cli(name, count, verbose):
"""
A simple greeting CLI tool.
Example:
python cli.py --name Alice --count 3
"""
if verbose:
console.print(f"[dim]Running with name={name}, count={count}[/dim]")
for i in range(count):
console.print(f"[green]Hello, {name}![/green]")
if verbose:
console.print(f"[dim]Completed {count} greeting(s)[/dim]")
if __name__ == '__main__':
cli()

View File

@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""
Nested Commands Click Template
Demonstrates command groups, nested subcommands, and context sharing.
"""
import click
from rich.console import Console
console = Console()
@click.group()
@click.version_option(version='1.0.0')
@click.pass_context
def cli(ctx):
"""
A powerful CLI tool with nested commands.
Example:
python cli.py init --template basic
python cli.py deploy production --mode safe
python cli.py config get api-key
"""
ctx.ensure_object(dict)
ctx.obj['console'] = console
@cli.command()
@click.option('--template', '-t', default='basic',
type=click.Choice(['basic', 'advanced', 'minimal']),
help='Project template')
@click.pass_context
def init(ctx, template):
"""Initialize a new project"""
console = ctx.obj['console']
console.print(f"[green]✓[/green] Initializing project with {template} template...")
@cli.command()
@click.argument('environment', type=click.Choice(['dev', 'staging', 'production']))
@click.option('--force', '-f', is_flag=True, help='Force deployment')
@click.option('--mode', '-m',
type=click.Choice(['fast', 'safe', 'rollback']),
default='safe',
help='Deployment mode')
@click.pass_context
def deploy(ctx, environment, force, mode):
"""Deploy to specified environment"""
console = ctx.obj['console']
console.print(f"[cyan]Deploying to {environment} in {mode} mode[/cyan]")
if force:
console.print("[yellow]⚠ Force mode enabled[/yellow]")
@cli.group()
def config():
"""Manage configuration settings"""
pass
@config.command()
@click.argument('key')
@click.pass_context
def get(ctx, key):
"""Get configuration value"""
console = ctx.obj['console']
# Placeholder for actual config retrieval
value = "example_value"
console.print(f"[dim]Config[/dim] {key}: [green]{value}[/green]")
@config.command()
@click.argument('key')
@click.argument('value')
@click.pass_context
def set(ctx, key, value):
"""Set configuration value"""
console = ctx.obj['console']
# Placeholder for actual config storage
console.print(f"[green]✓[/green] Set {key} = {value}")
@config.command()
@click.pass_context
def list(ctx):
"""List all configuration settings"""
console = ctx.obj['console']
console.print("[cyan]Configuration Settings:[/cyan]")
# Placeholder for actual config listing
console.print(" api-key: [dim]***hidden***[/dim]")
console.print(" debug: [green]true[/green]")
@cli.group()
def database():
"""Database management commands"""
pass
@database.command()
@click.option('--create-tables', is_flag=True, help='Create tables')
@click.pass_context
def migrate(ctx, create_tables):
"""Run database migrations"""
console = ctx.obj['console']
console.print("[cyan]Running migrations...[/cyan]")
if create_tables:
console.print("[green]✓[/green] Tables created")
@database.command()
@click.option('--confirm', is_flag=True, help='Confirm reset')
@click.pass_context
def reset(ctx, confirm):
"""Reset database (destructive)"""
console = ctx.obj['console']
if not confirm:
console.print("[yellow]⚠ Use --confirm to proceed[/yellow]")
return
console.print("[red]Resetting database...[/red]")
if __name__ == '__main__':
cli(obj={})

View File

@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""
Click Custom Validators Template
Demonstrates custom parameter validation, callbacks, and type conversion.
"""
import click
import re
from pathlib import Path
from rich.console import Console
console = Console()
# Custom validator callbacks
def validate_email(ctx, param, value):
"""Validate email format"""
if value and not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', value):
raise click.BadParameter('Invalid email format')
return value
def validate_port(ctx, param, value):
"""Validate port number"""
if value < 1 or value > 65535:
raise click.BadParameter('Port must be between 1 and 65535')
return value
def validate_path_exists(ctx, param, value):
"""Validate that path exists"""
if value and not Path(value).exists():
raise click.BadParameter(f'Path does not exist: {value}')
return value
def validate_url(ctx, param, value):
"""Validate URL format"""
if value and not re.match(r'^https?://[^\s]+$', value):
raise click.BadParameter('Invalid URL format (must start with http:// or https://)')
return value
# Custom Click types
class CommaSeparatedList(click.ParamType):
"""Custom type for comma-separated lists"""
name = 'comma-list'
def convert(self, value, param, ctx):
if isinstance(value, list):
return value
try:
return [item.strip() for item in value.split(',') if item.strip()]
except Exception:
self.fail(f'{value} is not a valid comma-separated list', param, ctx)
class EnvironmentVariable(click.ParamType):
"""Custom type for environment variables"""
name = 'env-var'
def convert(self, value, param, ctx):
if not re.match(r'^[A-Z_][A-Z0-9_]*$', value):
self.fail(f'{value} is not a valid environment variable name', param, ctx)
return value
@click.group()
def cli():
"""CLI with custom validators"""
pass
@cli.command()
@click.option('--email', callback=validate_email, required=True, help='User email address')
@click.option('--age', type=click.IntRange(0, 150), required=True, help='User age')
@click.option('--username', type=click.STRING, required=True,
help='Username (3-20 characters)',
callback=lambda ctx, param, value: value if 3 <= len(value) <= 20
else ctx.fail('Username must be 3-20 characters'))
def create_user(email, age, username):
"""Create a new user with validation"""
console.print(f"[green]✓[/green] User created: {username} ({email}), age {age}")
@cli.command()
@click.option('--port', type=int, callback=validate_port, default=8080, help='Server port')
@click.option('--host', default='localhost', help='Server host')
@click.option('--workers', type=click.IntRange(1, 32), default=4, help='Number of workers')
@click.option('--ssl', is_flag=True, help='Enable SSL')
def start_server(port, host, workers, ssl):
"""Start server with validated parameters"""
protocol = 'https' if ssl else 'http'
console.print(f"[cyan]Starting server at {protocol}://{host}:{port}[/cyan]")
console.print(f"[dim]Workers: {workers}[/dim]")
@cli.command()
@click.option('--config', type=click.Path(exists=True, dir_okay=False),
callback=validate_path_exists, required=True, help='Config file path')
@click.option('--output', type=click.Path(dir_okay=False), required=True, help='Output file path')
@click.option('--format', type=click.Choice(['json', 'yaml', 'toml']), default='json',
help='Output format')
def convert_config(config, output, format):
"""Convert configuration file"""
console.print(f"[cyan]Converting {config} to {format} format[/cyan]")
console.print(f"[green]✓[/green] Output: {output}")
@cli.command()
@click.option('--url', callback=validate_url, required=True, help='API URL')
@click.option('--method', type=click.Choice(['GET', 'POST', 'PUT', 'DELETE']),
default='GET', help='HTTP method')
@click.option('--headers', type=CommaSeparatedList(), help='Headers (comma-separated key:value)')
@click.option('--timeout', type=click.FloatRange(0.1, 300.0), default=30.0,
help='Request timeout in seconds')
def api_call(url, method, headers, timeout):
"""Make API call with validation"""
console.print(f"[cyan]{method} {url}[/cyan]")
console.print(f"[dim]Timeout: {timeout}s[/dim]")
if headers:
console.print(f"[dim]Headers: {headers}[/dim]")
@cli.command()
@click.option('--env-var', type=EnvironmentVariable(), required=True,
help='Environment variable name')
@click.option('--value', required=True, help='Environment variable value')
@click.option('--scope', type=click.Choice(['user', 'system', 'project']),
default='user', help='Variable scope')
def set_env(env_var, value, scope):
"""Set environment variable with validation"""
console.print(f"[green]✓[/green] Set {env_var}={value} (scope: {scope})")
@cli.command()
@click.option('--min', type=float, required=True, help='Minimum value')
@click.option('--max', type=float, required=True, help='Maximum value')
@click.option('--step', type=click.FloatRange(0.01, None), default=1.0, help='Step size')
def generate_range(min, max, step):
"""Generate numeric range with validation"""
if min >= max:
raise click.BadParameter('min must be less than max')
count = int((max - min) / step) + 1
console.print(f"[cyan]Generating range from {min} to {max} (step: {step})[/cyan]")
console.print(f"[dim]Total values: {count}[/dim]")
# Example combining multiple validators
@cli.command()
@click.option('--name', required=True, help='Project name',
callback=lambda ctx, param, value: value.lower().replace(' ', '-'))
@click.option('--tags', type=CommaSeparatedList(), help='Project tags (comma-separated)')
@click.option('--priority', type=click.IntRange(1, 10), default=5, help='Priority (1-10)')
@click.option('--template', type=click.Path(exists=True), help='Template directory')
def create_project(name, tags, priority, template):
"""Create project with multiple validators"""
console.print(f"[green]✓[/green] Project created: {name}")
console.print(f"[dim]Priority: {priority}[/dim]")
if tags:
console.print(f"[dim]Tags: {', '.join(tags)}[/dim]")
if template:
console.print(f"[dim]Template: {template}[/dim]")
if __name__ == '__main__':
cli()