""" Argument parsing utilities for terminal client commands. Provides robust parsing for both REPL and CLI commands using shlex to handle quoted strings, spaces, and complex arguments. Author: Claude Date: 2025-10-27 """ import shlex import logging from typing import Dict, Any, List, Optional, Tuple logger = logging.getLogger(f'{__name__}.arg_parser') class ArgumentParseError(Exception): """Raised when argument parsing fails.""" pass class CommandArgumentParser: """Parse command-line style arguments for terminal client.""" @staticmethod def parse_args(arg_string: str, schema: Dict[str, Any]) -> Dict[str, Any]: """ Parse argument string according to schema. Args: arg_string: Raw argument string from command schema: Dictionary defining expected arguments { 'league': {'type': str, 'default': 'sba'}, 'home_team': {'type': int, 'default': 1}, 'count': {'type': int, 'default': 1, 'positional': True}, 'verbose': {'type': bool, 'flag': True} } Returns: Dictionary of parsed arguments with defaults applied Raises: ArgumentParseError: If parsing fails or validation fails """ try: # Use shlex for robust parsing tokens = shlex.split(arg_string) if arg_string.strip() else [] except ValueError as e: raise ArgumentParseError(f"Invalid argument syntax: {e}") # Initialize result with defaults result = {} for key, spec in schema.items(): if 'default' in spec: result[key] = spec['default'] # Track which positional arg we're on positional_keys = [k for k, v in schema.items() if v.get('positional', False)] positional_index = 0 i = 0 while i < len(tokens): token = tokens[i] # Handle flags (--option or -o) if token.startswith('--'): option_name = token[2:] # Convert hyphen to underscore for Python compatibility option_key = option_name.replace('-', '_') if option_key not in schema: raise ArgumentParseError(f"Unknown option: {token}") spec = schema[option_key] # Boolean flags don't need a value if spec.get('flag', False): result[option_key] = True i += 1 continue # Option requires a value if i + 1 >= len(tokens): raise ArgumentParseError(f"Option {token} requires a value") value_str = tokens[i + 1] # Type conversion try: if spec['type'] == int: result[option_key] = int(value_str) elif spec['type'] == float: result[option_key] = float(value_str) elif spec['type'] == list: # Parse comma-separated list result[option_key] = [item.strip() for item in value_str.split(',')] elif spec['type'] == 'int_list': # Parse comma-separated integers result[option_key] = [int(item.strip()) for item in value_str.split(',')] else: result[option_key] = value_str except ValueError as e: raise ArgumentParseError( f"Invalid value for {token}: expected {spec['type'].__name__ if hasattr(spec['type'], '__name__') else spec['type']}, got '{value_str}'" ) i += 2 # Handle positional arguments else: if positional_index >= len(positional_keys): raise ArgumentParseError(f"Unexpected positional argument: {token}") key = positional_keys[positional_index] spec = schema[key] try: if spec['type'] == int: result[key] = int(token) elif spec['type'] == float: result[key] = float(token) else: result[key] = token except ValueError as e: raise ArgumentParseError( f"Invalid value for {key}: expected {spec['type'].__name__}, got '{token}'" ) positional_index += 1 i += 1 return result @staticmethod def parse_game_id(arg_string: str) -> Optional[str]: """ Parse a game ID from argument string. Args: arg_string: Raw argument string Returns: Game ID string or None """ try: tokens = shlex.split(arg_string) if arg_string.strip() else [] # Look for --game-id option for i, token in enumerate(tokens): if token == '--game-id' and i + 1 < len(tokens): return tokens[i + 1] # If no option, check if there's a positional UUID-like argument if tokens and len(tokens[0]) == 36: # UUID length return tokens[0] return None except ValueError: return None # Predefined schemas for common commands NEW_GAME_SCHEMA = { 'league': {'type': str, 'default': 'sba'}, 'home_team': {'type': int, 'default': 1}, 'away_team': {'type': int, 'default': 2} } DEFENSIVE_SCHEMA = { 'alignment': {'type': str, 'default': 'normal'}, 'infield': {'type': str, 'default': 'normal'}, 'outfield': {'type': str, 'default': 'normal'}, 'hold': {'type': 'int_list', 'default': []} } OFFENSIVE_SCHEMA = { 'action': {'type': str, 'default': 'swing_away'}, # Session 2: changed from approach 'steal': {'type': 'int_list', 'default': []} } QUICK_PLAY_SCHEMA = { 'count': {'type': int, 'default': 1, 'positional': True} } USE_GAME_SCHEMA = { 'game_id': {'type': str, 'positional': True} } def parse_new_game_args(arg_string: str) -> Dict[str, Any]: """Parse arguments for new_game command.""" return CommandArgumentParser.parse_args(arg_string, NEW_GAME_SCHEMA) def parse_defensive_args(arg_string: str) -> Dict[str, Any]: """Parse arguments for defensive command.""" return CommandArgumentParser.parse_args(arg_string, DEFENSIVE_SCHEMA) def parse_offensive_args(arg_string: str) -> Dict[str, Any]: """Parse arguments for offensive command.""" return CommandArgumentParser.parse_args(arg_string, OFFENSIVE_SCHEMA) def parse_quick_play_args(arg_string: str) -> Dict[str, Any]: """Parse arguments for quick_play command.""" return CommandArgumentParser.parse_args(arg_string, QUICK_PLAY_SCHEMA) def parse_use_game_args(arg_string: str) -> Dict[str, Any]: """Parse arguments for use_game command.""" return CommandArgumentParser.parse_args(arg_string, USE_GAME_SCHEMA)