Initial commit
This commit is contained in:
473
skills/argparse-patterns/examples/advanced-parsing.md
Normal file
473
skills/argparse-patterns/examples/advanced-parsing.md
Normal 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
|
||||
230
skills/argparse-patterns/examples/basic-usage.md
Normal file
230
skills/argparse-patterns/examples/basic-usage.md
Normal 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`
|
||||
370
skills/argparse-patterns/examples/nested-commands.md
Normal file
370
skills/argparse-patterns/examples/nested-commands.md
Normal 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
|
||||
283
skills/argparse-patterns/examples/subcommands.md
Normal file
283
skills/argparse-patterns/examples/subcommands.md
Normal 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`
|
||||
424
skills/argparse-patterns/examples/validation-patterns.md
Normal file
424
skills/argparse-patterns/examples/validation-patterns.md
Normal 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`
|
||||
Reference in New Issue
Block a user