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,473 @@
# Advanced argparse Patterns
Complex argument parsing scenarios and advanced techniques.
## Templates Reference
- `templates/custom-actions.py`
- `templates/mutually-exclusive.py`
- `templates/argument-groups.py`
- `templates/variadic-args.py`
## Overview
Advanced patterns:
- Custom action classes
- Mutually exclusive groups
- Argument groups (organization)
- Variadic arguments (nargs)
- Environment variable fallback
- Config file integration
- Subparser inheritance
## 1. Custom Actions
Create custom argument processing logic.
### Simple Custom Action
```python
class UpperCaseAction(argparse.Action):
"""Convert value to uppercase."""
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values.upper())
parser.add_argument('--name', action=UpperCaseAction)
```
### Key-Value Action
```python
class KeyValueAction(argparse.Action):
"""Parse key=value pairs."""
def __call__(self, parser, namespace, values, option_string=None):
if '=' not in values:
parser.error(f"Must be key=value format: {values}")
key, value = values.split('=', 1)
items = getattr(namespace, self.dest, {}) or {}
items[key] = value
setattr(namespace, self.dest, items)
parser.add_argument(
'--env', '-e',
action=KeyValueAction,
help='Environment variable (key=value)'
)
# Usage: --env API_KEY=abc123 --env DB_URL=postgres://...
```
### Load File Action
```python
class LoadFileAction(argparse.Action):
"""Load and parse file content."""
def __call__(self, parser, namespace, values, option_string=None):
try:
with open(values, 'r') as f:
content = f.read()
setattr(namespace, self.dest, content)
except Exception as e:
parser.error(f"Cannot load file {values}: {e}")
parser.add_argument('--config', action=LoadFileAction)
```
## 2. Mutually Exclusive Groups
Ensure only one option from a group is used.
### Basic Exclusivity
```python
group = parser.add_mutually_exclusive_group()
group.add_argument('--json', help='Output as JSON')
group.add_argument('--yaml', help='Output as YAML')
group.add_argument('--xml', help='Output as XML')
# Valid: --json output.json
# Valid: --yaml output.yaml
# Invalid: --json output.json --yaml output.yaml
```
### Required Exclusive Group
```python
mode_group = parser.add_mutually_exclusive_group(required=True)
mode_group.add_argument('--create', metavar='NAME')
mode_group.add_argument('--update', metavar='NAME')
mode_group.add_argument('--delete', metavar='NAME')
mode_group.add_argument('--list', action='store_true')
# Must specify exactly one: create, update, delete, or list
```
### Multiple Exclusive Groups
```python
# Output format group
output = parser.add_mutually_exclusive_group()
output.add_argument('--json', action='store_true')
output.add_argument('--yaml', action='store_true')
# Verbosity group
verbosity = parser.add_mutually_exclusive_group()
verbosity.add_argument('--verbose', action='store_true')
verbosity.add_argument('--quiet', action='store_true')
# Can use one from each group:
# Valid: --json --verbose
# Invalid: --json --yaml
```
## 3. Argument Groups
Organize arguments for better help display.
```python
parser = argparse.ArgumentParser()
# Server configuration
server_group = parser.add_argument_group(
'server configuration',
'Options for configuring the web server'
)
server_group.add_argument('--host', default='127.0.0.1')
server_group.add_argument('--port', type=int, default=8080)
server_group.add_argument('--workers', type=int, default=4)
# Database configuration
db_group = parser.add_argument_group(
'database configuration',
'Options for database connection'
)
db_group.add_argument('--db-host', default='localhost')
db_group.add_argument('--db-port', type=int, default=5432)
db_group.add_argument('--db-name', required=True)
# Logging configuration
log_group = parser.add_argument_group(
'logging configuration',
'Options for logging and monitoring'
)
log_group.add_argument('--log-level',
choices=['debug', 'info', 'warning', 'error'],
default='info')
log_group.add_argument('--log-file', help='Log to file')
```
**Help output groups arguments logically.**
## 4. Variadic Arguments (nargs)
Handle variable number of arguments.
### Optional Single Argument (?)
```python
parser.add_argument(
'--output',
nargs='?',
const='default.json', # Used if flag present, no value
default=None, # Used if flag not present
help='Output file'
)
# --output → 'default.json'
# --output file.json → 'file.json'
# (no flag) → None
```
### Zero or More (*)
```python
parser.add_argument(
'--include',
nargs='*',
default=[],
help='Include patterns'
)
# --include → []
# --include *.py → ['*.py']
# --include *.py *.md → ['*.py', '*.md']
```
### One or More (+)
```python
parser.add_argument(
'files',
nargs='+',
help='Input files (at least one required)'
)
# file1.txt → ['file1.txt']
# file1.txt file2.txt → ['file1.txt', 'file2.txt']
# (no files) → Error: required
```
### Exact Number
```python
parser.add_argument(
'--range',
nargs=2,
type=int,
metavar=('START', 'END'),
help='Range as start end'
)
# --range 1 10 → [1, 10]
# --range 1 → Error: expected 2
```
### Remainder
```python
parser.add_argument(
'--command',
nargs=argparse.REMAINDER,
help='Pass-through command and args'
)
# mycli --command python script.py --arg1 --arg2
# → command = ['python', 'script.py', '--arg1', '--arg2']
```
## 5. Environment Variable Fallback
```python
import os
parser = argparse.ArgumentParser()
parser.add_argument(
'--api-key',
default=os.environ.get('API_KEY'),
help='API key (default: $API_KEY)'
)
parser.add_argument(
'--db-url',
default=os.environ.get('DATABASE_URL'),
help='Database URL (default: $DATABASE_URL)'
)
# Precedence: CLI arg > Environment variable > Default
```
## 6. Config File Integration
```python
import configparser
def load_config(config_file):
"""Load configuration from INI file."""
config = configparser.ConfigParser()
config.read(config_file)
return config
parser = argparse.ArgumentParser()
parser.add_argument('--config', help='Config file')
parser.add_argument('--host', help='Server host')
parser.add_argument('--port', type=int, help='Server port')
args = parser.parse_args()
# Load config file if specified
if args.config:
config = load_config(args.config)
# Use config values as defaults if not specified on CLI
if not args.host:
args.host = config.get('server', 'host', fallback='127.0.0.1')
if not args.port:
args.port = config.getint('server', 'port', fallback=8080)
```
## 7. Parent Parsers (Inheritance)
Share common arguments across subcommands.
```python
# Parent parser with common arguments
parent_parser = argparse.ArgumentParser(add_help=False)
parent_parser.add_argument('--verbose', action='store_true')
parent_parser.add_argument('--config', help='Config file')
# Main parser
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='command')
# Subcommands inherit from parent
deploy_parser = subparsers.add_parser(
'deploy',
parents=[parent_parser],
help='Deploy application'
)
deploy_parser.add_argument('environment')
build_parser = subparsers.add_parser(
'build',
parents=[parent_parser],
help='Build application'
)
build_parser.add_argument('--target')
# Both subcommands have --verbose and --config
```
## 8. Argument Defaults from Dict
```python
defaults = {
'host': '127.0.0.1',
'port': 8080,
'workers': 4,
'timeout': 30.0
}
parser = argparse.ArgumentParser()
parser.add_argument('--host')
parser.add_argument('--port', type=int)
parser.add_argument('--workers', type=int)
parser.add_argument('--timeout', type=float)
# Set all defaults at once
parser.set_defaults(**defaults)
```
## 9. Namespace Manipulation
```python
# Pre-populate namespace
defaults = argparse.Namespace(
host='127.0.0.1',
port=8080,
debug=False
)
args = parser.parse_args(namespace=defaults)
# Or modify after parsing
args = parser.parse_args()
args.computed_value = args.value1 + args.value2
```
## 10. Conditional Arguments
```python
args = parser.parse_args()
# Add conditional validation
if args.ssl and not (args.cert and args.key):
parser.error("--ssl requires both --cert and --key")
# Add computed values
if args.workers == 'auto':
import os
args.workers = os.cpu_count()
```
## Complete Advanced Example
```python
#!/usr/bin/env python3
import argparse
import os
import sys
class KeyValueAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
key, value = values.split('=', 1)
items = getattr(namespace, self.dest, {}) or {}
items[key] = value
setattr(namespace, self.dest, items)
def main():
# Parent parser for common args
parent = argparse.ArgumentParser(add_help=False)
parent.add_argument('--verbose', action='store_true')
parent.add_argument('--config', help='Config file')
# Main parser
parser = argparse.ArgumentParser(
description='Advanced argparse patterns'
)
subparsers = parser.add_subparsers(dest='command', required=True)
# Deploy command
deploy = subparsers.add_parser(
'deploy',
parents=[parent],
help='Deploy application'
)
# Mutually exclusive group
format_group = deploy.add_mutually_exclusive_group()
format_group.add_argument('--json', action='store_true')
format_group.add_argument('--yaml', action='store_true')
# Custom action
deploy.add_argument(
'--env', '-e',
action=KeyValueAction,
help='Environment variable'
)
# Variadic arguments
deploy.add_argument(
'targets',
nargs='+',
help='Deployment targets'
)
# Environment fallback
deploy.add_argument(
'--api-key',
default=os.environ.get('API_KEY'),
help='API key'
)
args = parser.parse_args()
# Post-parse validation
if args.command == 'deploy':
if not args.api_key:
parser.error("API key required (use --api-key or $API_KEY)")
return 0
if __name__ == '__main__':
sys.exit(main())
```
## Best Practices
1. **Use parent parsers** for shared arguments
2. **Use argument groups** for organization
3. **Use mutually exclusive groups** when appropriate
4. **Validate after parsing** for complex logic
5. **Provide environment fallbacks** for sensitive data
6. **Use custom actions** for complex transformations
7. **Document nargs behavior** in help text
## Next Steps
- Review template files for complete implementations
- Test patterns with `scripts/test-parser.sh`
- Compare with Click/Typer alternatives

View File

@@ -0,0 +1,230 @@
# Basic argparse Usage
Simple CLI with positional and optional arguments using Python's standard library.
## Template Reference
`templates/basic-parser.py`
## Overview
Demonstrates fundamental argparse patterns:
- Positional arguments (required)
- Optional arguments with flags
- Boolean flags
- Type coercion
- Default values
- Help text generation
## Quick Start
```bash
# View help
python basic-parser.py --help
# Basic usage
python basic-parser.py deploy my-app
# With optional arguments
python basic-parser.py deploy my-app --env staging --timeout 60
# Boolean flags
python basic-parser.py deploy my-app --force
# Verbose mode (count occurrences)
python basic-parser.py deploy my-app -vvv
```
## Key Patterns
### 1. Create Parser
```python
import argparse
parser = argparse.ArgumentParser(
description='Deploy application to specified environment',
formatter_class=argparse.RawDescriptionHelpFormatter
)
```
**Why `RawDescriptionHelpFormatter`?**
- Preserves formatting in epilog (usage examples)
- Better control over help text layout
### 2. Add Version
```python
parser.add_argument(
'--version',
action='version',
version='%(prog)s 1.0.0'
)
```
**Usage:** `python mycli.py --version`
### 3. Positional Arguments
```python
parser.add_argument(
'app_name',
help='Name of the application to deploy'
)
```
**Required by default** - no flag needed, just the value.
### 4. Optional Arguments
```python
parser.add_argument(
'--env', '-e',
default='development',
help='Deployment environment (default: %(default)s)'
)
```
**Note:** `%(default)s` automatically shows default value in help.
### 5. Type Coercion
```python
parser.add_argument(
'--timeout', '-t',
type=int,
default=30,
help='Timeout in seconds'
)
```
**Automatic validation** - argparse will error if non-integer provided.
### 6. Boolean Flags
```python
parser.add_argument(
'--force', '-f',
action='store_true',
help='Force deployment without confirmation'
)
```
**Result:**
- Present: `args.force = True`
- Absent: `args.force = False`
### 7. Count Action
```python
parser.add_argument(
'--verbose', '-v',
action='count',
default=0,
help='Increase verbosity (-v, -vv, -vvv)'
)
```
**Usage:**
- `-v`: verbosity = 1
- `-vv`: verbosity = 2
- `-vvv`: verbosity = 3
## Complete Example
```python
#!/usr/bin/env python3
import argparse
import sys
def main():
parser = argparse.ArgumentParser(
description='Simple deployment tool'
)
parser.add_argument('--version', action='version', version='1.0.0')
parser.add_argument('app_name', help='Application name')
parser.add_argument('--env', default='dev', help='Environment')
parser.add_argument('--timeout', type=int, default=30, help='Timeout')
parser.add_argument('--force', action='store_true', help='Force')
args = parser.parse_args()
print(f"Deploying {args.app_name} to {args.env}")
print(f"Timeout: {args.timeout}s")
print(f"Force: {args.force}")
return 0
if __name__ == '__main__':
sys.exit(main())
```
## Help Output
```
usage: basic-parser.py [-h] [--version] [--env ENV] [--timeout TIMEOUT]
[--force] [--verbose]
action app_name
Deploy application to specified environment
positional arguments:
action Action to perform
app_name Name of the application to deploy
optional arguments:
-h, --help show this help message and exit
--version show program's version number and exit
--env ENV, -e ENV Deployment environment (default: development)
--timeout TIMEOUT, -t TIMEOUT
Timeout in seconds (default: 30)
--force, -f Force deployment without confirmation
--verbose, -v Increase verbosity (-v, -vv, -vvv)
```
## Common Mistakes
### ❌ Wrong: Accessing before parsing
```python
args = parser.parse_args()
print(args.env) # ✓ Correct
```
```python
print(args.env) # ✗ Wrong - args doesn't exist yet
args = parser.parse_args()
```
### ❌ Wrong: Not checking boolean flags
```python
if args.force: # ✓ Correct
print("Force mode")
```
```python
if args.force == True: # ✗ Unnecessary comparison
print("Force mode")
```
### ❌ Wrong: Manual type conversion
```python
parser.add_argument('--port', type=int) # ✓ Let argparse handle it
```
```python
parser.add_argument('--port')
port = int(args.port) # ✗ Manual conversion (error-prone)
```
## Next Steps
- **Subcommands:** See `subcommands.md`
- **Validation:** See `validation-patterns.md`
- **Advanced:** See `advanced-parsing.md`

View File

@@ -0,0 +1,370 @@
# Nested Subcommands
Multi-level command hierarchies like `git config get` or `kubectl config view`.
## Template Reference
`templates/nested-subparser.py`
## Overview
Create deep command structures:
- `mycli config get key`
- `mycli config set key value`
- `mycli deploy start production`
- `mycli deploy stop production`
## Quick Start
```bash
# Two-level commands
python nested-subparser.py config get database_url
python nested-subparser.py config set api_key abc123
python nested-subparser.py config list
# Deploy subcommands
python nested-subparser.py deploy start production --replicas 3
python nested-subparser.py deploy stop staging
```
## Architecture
```
mycli
├── config
│ ├── get <key>
│ ├── set <key> <value>
│ ├── list
│ └── delete <key>
└── deploy
├── start <environment>
├── stop <environment>
└── restart <environment>
```
## Implementation Pattern
### 1. Main Parser
```python
parser = argparse.ArgumentParser(description='Multi-level CLI')
subparsers = parser.add_subparsers(dest='command', required=True)
```
### 2. First-Level Subcommand
```python
# Create 'config' command group
config_parser = subparsers.add_parser(
'config',
help='Manage configuration'
)
# Create second-level subparsers under 'config'
config_subparsers = config_parser.add_subparsers(
dest='config_command',
required=True
)
```
### 3. Second-Level Subcommands
```python
# config get
config_get = config_subparsers.add_parser('get', help='Get value')
config_get.add_argument('key', help='Configuration key')
config_get.set_defaults(func=config_get_handler)
# config set
config_set = config_subparsers.add_parser('set', help='Set value')
config_set.add_argument('key', help='Configuration key')
config_set.add_argument('value', help='Configuration value')
config_set.add_argument('--force', action='store_true')
config_set.set_defaults(func=config_set_handler)
```
## Complete Example
```python
#!/usr/bin/env python3
import argparse
import sys
# Config handlers
def config_get(args):
print(f"Getting: {args.key}")
return 0
def config_set(args):
print(f"Setting: {args.key} = {args.value}")
return 0
# Deploy handlers
def deploy_start(args):
print(f"Starting deployment to {args.environment}")
return 0
def main():
parser = argparse.ArgumentParser(description='Nested CLI')
subparsers = parser.add_subparsers(dest='command', required=True)
# === Config group ===
config_parser = subparsers.add_parser('config', help='Configuration')
config_subs = config_parser.add_subparsers(
dest='config_command',
required=True
)
# config get
get_parser = config_subs.add_parser('get')
get_parser.add_argument('key')
get_parser.set_defaults(func=config_get)
# config set
set_parser = config_subs.add_parser('set')
set_parser.add_argument('key')
set_parser.add_argument('value')
set_parser.set_defaults(func=config_set)
# === Deploy group ===
deploy_parser = subparsers.add_parser('deploy', help='Deployment')
deploy_subs = deploy_parser.add_subparsers(
dest='deploy_command',
required=True
)
# deploy start
start_parser = deploy_subs.add_parser('start')
start_parser.add_argument('environment')
start_parser.set_defaults(func=deploy_start)
# Parse and dispatch
args = parser.parse_args()
return args.func(args)
if __name__ == '__main__':
sys.exit(main())
```
## Accessing Nested Commands
```python
args = parser.parse_args()
# Top-level command
print(args.command) # 'config' or 'deploy'
# Second-level command
if args.command == 'config':
print(args.config_command) # 'get', 'set', 'list', 'delete'
elif args.command == 'deploy':
print(args.deploy_command) # 'start', 'stop', 'restart'
```
## Help Output
### Top-Level Help
```
usage: mycli [-h] {config,deploy} ...
positional arguments:
{config,deploy}
config Manage configuration
deploy Manage deployments
```
### Second-Level Help
```bash
$ python mycli.py config --help
usage: mycli config [-h] {get,set,list,delete} ...
positional arguments:
{get,set,list,delete}
get Get configuration value
set Set configuration value
list List all configuration
delete Delete configuration value
```
### Third-Level Help
```bash
$ python mycli.py config set --help
usage: mycli config set [-h] [-f] key value
positional arguments:
key Configuration key
value Configuration value
optional arguments:
-h, --help show this help message and exit
-f, --force Overwrite existing value
```
## Dispatch Pattern
### Option 1: Manual Switch
```python
args = parser.parse_args()
if args.command == 'config':
if args.config_command == 'get':
config_get(args)
elif args.config_command == 'set':
config_set(args)
elif args.command == 'deploy':
if args.deploy_command == 'start':
deploy_start(args)
```
### Option 2: Function Dispatch (Recommended)
```python
# Set handlers when creating parsers
config_get.set_defaults(func=config_get_handler)
config_set.set_defaults(func=config_set_handler)
deploy_start.set_defaults(func=deploy_start_handler)
# Simple dispatch
args = parser.parse_args()
return args.func(args)
```
## Best Practices
### 1. Consistent Naming
```python
# ✓ Good - consistent dest naming
config_parser.add_subparsers(dest='config_command')
deploy_parser.add_subparsers(dest='deploy_command')
```
### 2. Set Required
```python
# ✓ Good - require subcommand
config_subs = config_parser.add_subparsers(
dest='config_command',
required=True
)
```
### 3. Provide Help
```python
# ✓ Good - descriptive help at each level
config_parser = subparsers.add_parser(
'config',
help='Manage configuration',
description='Configuration management commands'
)
```
### 4. Use set_defaults
```python
# ✓ Good - easy dispatch
get_parser.set_defaults(func=config_get)
```
## How Deep Should You Go?
### ✓ Good: 2-3 Levels
```
mycli config get key
mycli deploy start production
```
### ⚠️ Consider alternatives: 4+ Levels
```
mycli server database config get key # Too deep
```
**Alternatives:**
- Flatten: `mycli db-config-get key`
- Split: Separate CLI tools
- Use flags: `mycli config get key --scope=server --type=database`
## Common Mistakes
### ❌ Wrong: Same dest name
```python
# Both use 'command' - second overwrites first
config_subs = config_parser.add_subparsers(dest='command')
deploy_subs = deploy_parser.add_subparsers(dest='command')
```
```python
# ✓ Correct - unique dest names
config_subs = config_parser.add_subparsers(dest='config_command')
deploy_subs = deploy_parser.add_subparsers(dest='deploy_command')
```
### ❌ Wrong: Accessing wrong level
```python
args = parser.parse_args(['config', 'get', 'key'])
print(args.command) # ✓ 'config'
print(args.config_command) # ✓ 'get'
print(args.deploy_command) # ✗ Error - not set
```
### ❌ Wrong: Not checking hierarchy
```python
# ✗ Assumes deploy command
print(args.deploy_command)
```
```python
# ✓ Check first
if args.command == 'deploy':
print(args.deploy_command)
```
## Real-World Examples
### Git-style
```
git config --global user.name "Name"
git remote add origin url
git branch --list
```
### Kubectl-style
```
kubectl config view
kubectl get pods --namespace default
kubectl logs pod-name --follow
```
### Docker-style
```
docker container ls
docker image build -t name .
docker network create name
```
## Next Steps
- **Validation:** See `validation-patterns.md`
- **Advanced:** See `advanced-parsing.md`
- **Compare frameworks:** See templates for Click/Typer equivalents

View File

@@ -0,0 +1,283 @@
# Subcommands with argparse
Multi-command CLI like `git`, `docker`, or `kubectl` using subparsers.
## Template Reference
`templates/subparser-pattern.py`
## Overview
Create CLIs with multiple commands:
- `mycli init` - Initialize project
- `mycli deploy production` - Deploy to environment
- `mycli status` - Show status
Each subcommand has its own arguments and options.
## Quick Start
```bash
# View main help
python subparser-pattern.py --help
# View subcommand help
python subparser-pattern.py init --help
python subparser-pattern.py deploy --help
# Execute subcommands
python subparser-pattern.py init --template react
python subparser-pattern.py deploy production --force
python subparser-pattern.py status --format json
```
## Key Patterns
### 1. Create Subparsers
```python
parser = argparse.ArgumentParser(description='Multi-command CLI')
subparsers = parser.add_subparsers(
dest='command', # Store command name in args.command
help='Available commands',
required=True # At least one command required (Python 3.7+)
)
```
**Important:** Set `dest='command'` to access which command was used.
### 2. Add Subcommand
```python
init_parser = subparsers.add_parser(
'init',
help='Initialize a new project',
description='Initialize a new project with specified template'
)
init_parser.add_argument('--template', default='basic')
init_parser.add_argument('--path', default='.')
```
Each subcommand is a separate parser with its own arguments.
### 3. Set Command Handler
```python
def cmd_init(args):
"""Initialize project."""
print(f"Initializing with {args.template} template...")
init_parser.set_defaults(func=cmd_init)
```
**Dispatch pattern:**
```python
args = parser.parse_args()
return args.func(args) # Call the appropriate handler
```
### 4. Subcommand with Choices
```python
deploy_parser = subparsers.add_parser('deploy')
deploy_parser.add_argument(
'environment',
choices=['development', 'staging', 'production'],
help='Target environment'
)
deploy_parser.add_argument(
'--mode',
choices=['fast', 'safe', 'rollback'],
default='safe'
)
```
## Complete Example
```python
#!/usr/bin/env python3
import argparse
import sys
def cmd_init(args):
print(f"Initializing with {args.template} template")
return 0
def cmd_deploy(args):
print(f"Deploying to {args.environment}")
return 0
def main():
parser = argparse.ArgumentParser(description='My CLI Tool')
parser.add_argument('--version', action='version', version='1.0.0')
subparsers = parser.add_subparsers(dest='command', required=True)
# Init command
init_parser = subparsers.add_parser('init', help='Initialize project')
init_parser.add_argument('--template', default='basic')
init_parser.set_defaults(func=cmd_init)
# Deploy command
deploy_parser = subparsers.add_parser('deploy', help='Deploy app')
deploy_parser.add_argument(
'environment',
choices=['dev', 'staging', 'prod']
)
deploy_parser.set_defaults(func=cmd_deploy)
# Parse and dispatch
args = parser.parse_args()
return args.func(args)
if __name__ == '__main__':
sys.exit(main())
```
## Help Output
### Main Help
```
usage: mycli [-h] [--version] {init,deploy,status} ...
Multi-command CLI tool
positional arguments:
{init,deploy,status} Available commands
init Initialize a new project
deploy Deploy application to environment
status Show deployment status
optional arguments:
-h, --help show this help message and exit
--version show program's version number and exit
```
### Subcommand Help
```bash
$ python mycli.py deploy --help
usage: mycli deploy [-h] [-f] [-m {fast,safe,rollback}]
{development,staging,production}
positional arguments:
{development,staging,production}
Target environment
optional arguments:
-h, --help show this help message and exit
-f, --force Force deployment without confirmation
-m {fast,safe,rollback}, --mode {fast,safe,rollback}
Deployment mode (default: safe)
```
## Accessing Parsed Values
```python
args = parser.parse_args()
# Which command was used?
print(args.command) # 'init', 'deploy', or 'status'
# Command-specific arguments
if args.command == 'deploy':
print(args.environment) # 'production'
print(args.force) # True/False
print(args.mode) # 'safe'
```
## Common Patterns
### Pattern 1: Switch on Command
```python
args = parser.parse_args()
if args.command == 'init':
init_project(args)
elif args.command == 'deploy':
deploy_app(args)
elif args.command == 'status':
show_status(args)
```
### Pattern 2: Function Dispatch (Better)
```python
# Set handlers
init_parser.set_defaults(func=cmd_init)
deploy_parser.set_defaults(func=cmd_deploy)
# Dispatch
args = parser.parse_args()
return args.func(args)
```
### Pattern 3: Check if Command Provided
```python
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
```
**Note:** Use `required=True` in `add_subparsers()` to make this automatic.
## Common Mistakes
### ❌ Wrong: Forgetting dest
```python
subparsers = parser.add_subparsers(dest='command') # ✓ Can check args.command
```
```python
subparsers = parser.add_subparsers() # ✗ Can't access which command
```
### ❌ Wrong: Accessing wrong argument
```python
# deploy_parser defines 'environment'
# init_parser defines 'template'
args = parser.parse_args(['deploy', 'prod'])
print(args.environment) # ✓ Correct
print(args.template) # ✗ Error - not defined for deploy
```
### ❌ Wrong: No required=True (Python 3.7+)
```python
subparsers = parser.add_subparsers(dest='command', required=True) # ✓
```
```python
subparsers = parser.add_subparsers(dest='command') # ✗ Command optional
# User can run: python mycli.py (no command)
```
## Nested Subcommands
For multi-level commands like `git config get`, see:
- `nested-commands.md`
- `templates/nested-subparser.py`
## Next Steps
- **Nested Commands:** See `nested-commands.md`
- **Validation:** See `validation-patterns.md`
- **Complex CLIs:** See `advanced-parsing.md`

View File

@@ -0,0 +1,424 @@
# Validation Patterns with argparse
Custom validators, type checking, and error handling.
## Template Reference
`templates/choices-validation.py`
## Overview
Robust argument validation:
- Built-in choices validation
- Custom type validators
- Range validation
- Pattern matching (regex)
- File/path validation
## Quick Start
```bash
# Valid inputs
python choices-validation.py --log-level debug --port 8080
python choices-validation.py --region us-east-1 --email user@example.com
# Invalid inputs (will error)
python choices-validation.py --log-level invalid # Not in choices
python choices-validation.py --port 99999 # Out of range
python choices-validation.py --email invalid # Invalid format
```
## Validation Methods
### 1. Choices (Built-in)
```python
parser.add_argument(
'--log-level',
choices=['debug', 'info', 'warning', 'error', 'critical'],
default='info',
help='Logging level'
)
```
**Automatic validation** - argparse rejects invalid values.
### 2. Custom Type Validator
```python
def validate_port(value):
"""Validate port number is 1-65535."""
try:
ivalue = int(value)
except ValueError:
raise argparse.ArgumentTypeError(f"{value} is not a valid integer")
if ivalue < 1 or ivalue > 65535:
raise argparse.ArgumentTypeError(
f"{value} is not a valid port (must be 1-65535)"
)
return ivalue
parser.add_argument(
'--port',
type=validate_port,
default=8080,
help='Server port (1-65535)'
)
```
### 3. Regex Pattern Validation
```python
import re
def validate_email(value):
"""Validate email address format."""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(pattern, value):
raise argparse.ArgumentTypeError(
f"{value} is not a valid email address"
)
return value
parser.add_argument('--email', type=validate_email)
```
### 4. IP Address Validation
```python
def validate_ip(value):
"""Validate IPv4 address."""
pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
if not re.match(pattern, value):
raise argparse.ArgumentTypeError(
f"{value} is not a valid IP address"
)
# Check each octet is 0-255
octets = [int(x) for x in value.split('.')]
if any(o < 0 or o > 255 for o in octets):
raise argparse.ArgumentTypeError(
f"{value} contains invalid octets"
)
return value
parser.add_argument('--host', type=validate_ip)
```
### 5. Path Validation
```python
from pathlib import Path
def validate_path_exists(value):
"""Validate path exists."""
path = Path(value)
if not path.exists():
raise argparse.ArgumentTypeError(
f"Path does not exist: {value}"
)
return path
parser.add_argument('--config', type=validate_path_exists)
```
### 6. Range Validation Factory
```python
def validate_range(min_val, max_val):
"""Factory function for range validators."""
def validator(value):
try:
ivalue = int(value)
except ValueError:
raise argparse.ArgumentTypeError(
f"{value} is not a valid integer"
)
if ivalue < min_val or ivalue > max_val:
raise argparse.ArgumentTypeError(
f"{value} must be between {min_val} and {max_val}"
)
return ivalue
return validator
# Usage
parser.add_argument(
'--workers',
type=validate_range(1, 32),
default=4,
help='Number of workers (1-32)'
)
```
## Complete Validation Example
```python
#!/usr/bin/env python3
import argparse
import re
import sys
from pathlib import Path
def validate_port(value):
ivalue = int(value)
if not (1 <= ivalue <= 65535):
raise argparse.ArgumentTypeError(
f"Port must be 1-65535, got {value}"
)
return ivalue
def validate_email(value):
if not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', value):
raise argparse.ArgumentTypeError(
f"Invalid email: {value}"
)
return value
def main():
parser = argparse.ArgumentParser(
description='Validation examples'
)
# Choices
parser.add_argument(
'--env',
choices=['dev', 'staging', 'prod'],
required=True,
help='Environment (required)'
)
# Custom validators
parser.add_argument('--port', type=validate_port, default=8080)
parser.add_argument('--email', type=validate_email)
# Path validation
parser.add_argument(
'--config',
type=lambda x: Path(x) if Path(x).exists() else
parser.error(f"File not found: {x}"),
help='Config file (must exist)'
)
args = parser.parse_args()
print(f"Environment: {args.env}")
print(f"Port: {args.port}")
if args.email:
print(f"Email: {args.email}")
return 0
if __name__ == '__main__':
sys.exit(main())
```
## Post-Parse Validation
Sometimes you need to validate relationships between arguments:
```python
args = parser.parse_args()
# Validate argument combinations
if args.ssl and not (args.cert and args.key):
parser.error("--ssl requires both --cert and --key")
if args.output and args.output.exists() and not args.force:
parser.error(f"Output file exists: {args.output}. Use --force to overwrite")
# Validate argument ranges
if args.start_date > args.end_date:
parser.error("Start date must be before end date")
```
## Error Messages
### Built-in Error Format
```bash
$ python mycli.py --env invalid
usage: mycli.py [-h] --env {dev,staging,prod}
mycli.py: error: argument --env: invalid choice: 'invalid'
(choose from 'dev', 'staging', 'prod')
```
### Custom Error Format
```python
def validate_port(value):
try:
ivalue = int(value)
except ValueError:
raise argparse.ArgumentTypeError(
f"Port must be an integer (got '{value}')"
)
if ivalue < 1 or ivalue > 65535:
raise argparse.ArgumentTypeError(
f"Port {ivalue} is out of range (valid: 1-65535)"
)
return ivalue
```
```bash
$ python mycli.py --port 99999
usage: mycli.py [-h] [--port PORT]
mycli.py: error: argument --port: Port 99999 is out of range (valid: 1-65535)
```
## Common Validation Patterns
### URL Validation
```python
import re
def validate_url(value):
pattern = r'^https?://[\w\.-]+\.\w+(:\d+)?(/.*)?$'
if not re.match(pattern, value):
raise argparse.ArgumentTypeError(f"Invalid URL: {value}")
return value
```
### Date Validation
```python
from datetime import datetime
def validate_date(value):
try:
return datetime.strptime(value, '%Y-%m-%d').date()
except ValueError:
raise argparse.ArgumentTypeError(
f"Invalid date: {value} (expected YYYY-MM-DD)"
)
```
### File Extension Validation
```python
def validate_json_file(value):
path = Path(value)
if path.suffix != '.json':
raise argparse.ArgumentTypeError(
f"File must have .json extension: {value}"
)
return path
```
### Percentage Validation
```python
def validate_percentage(value):
try:
pct = float(value.rstrip('%'))
except ValueError:
raise argparse.ArgumentTypeError(f"Invalid percentage: {value}")
if not (0 <= pct <= 100):
raise argparse.ArgumentTypeError(
f"Percentage must be 0-100: {value}"
)
return pct
```
## Best Practices
### ✓ Do: Fail Early
```python
# Validate during parsing
parser.add_argument('--port', type=validate_port)
# Not after parsing
args = parser.parse_args()
if not valid_port(args.port): # ✗ Too late
sys.exit(1)
```
### ✓ Do: Provide Clear Messages
```python
# ✓ Clear, actionable error
raise argparse.ArgumentTypeError(
f"Port {value} is out of range (valid: 1-65535)"
)
# ✗ Vague error
raise argparse.ArgumentTypeError("Invalid port")
```
### ✓ Do: Use Choices When Possible
```python
# ✓ Let argparse handle it
parser.add_argument('--env', choices=['dev', 'staging', 'prod'])
# ✗ Manual validation
parser.add_argument('--env')
if args.env not in ['dev', 'staging', 'prod']:
parser.error("Invalid environment")
```
### ✓ Do: Validate Type Before Range
```python
def validate_port(value):
# First ensure it's an integer
try:
ivalue = int(value)
except ValueError:
raise argparse.ArgumentTypeError(f"Not an integer: {value}")
# Then check range
if not (1 <= ivalue <= 65535):
raise argparse.ArgumentTypeError(f"Out of range: {ivalue}")
return ivalue
```
## Testing Validation
```python
import pytest
from io import StringIO
import sys
def test_valid_port():
"""Test valid port number."""
parser = create_parser()
args = parser.parse_args(['--port', '8080'])
assert args.port == 8080
def test_invalid_port():
"""Test invalid port number."""
parser = create_parser()
with pytest.raises(SystemExit):
parser.parse_args(['--port', '99999'])
def test_invalid_choice():
"""Test invalid choice."""
parser = create_parser()
with pytest.raises(SystemExit):
parser.parse_args(['--env', 'invalid'])
```
## Next Steps
- **Advanced Patterns:** See `advanced-parsing.md`
- **Type Coercion:** See `templates/type-coercion.py`
- **Custom Actions:** See `templates/custom-actions.py`