# 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