This commit includes cleanup from model refactoring and terminal client
modularization for better code organization and maintainability.
## Game Models Refactor
**Removed RunnerState class:**
- Eliminated separate RunnerState model (was redundant)
- Replaced runners: List[RunnerState] with direct base references:
- on_first: Optional[LineupPlayerState]
- on_second: Optional[LineupPlayerState]
- on_third: Optional[LineupPlayerState]
- Updated helper methods:
- get_runner_at_base() now returns LineupPlayerState directly
- get_all_runners() returns List[Tuple[int, LineupPlayerState]]
- is_runner_on_X() simplified to direct None checks
**Benefits:**
- Matches database structure (plays table has on_first_id, etc.)
- Simpler state management (direct references vs list management)
- Better type safety (LineupPlayerState vs generic runner)
- Easier to work with in game engine logic
**Updated files:**
- app/models/game_models.py - Removed RunnerState, updated GameState
- app/core/play_resolver.py - Use get_all_runners() instead of state.runners
- app/core/validators.py - Updated runner access patterns
- tests/unit/models/test_game_models.py - Updated test assertions
- tests/unit/core/test_play_resolver.py - Updated test data
- tests/unit/core/test_validators.py - Updated test data
## Terminal Client Refactor
**Modularization (DRY principle):**
Created separate modules for better code organization:
1. **terminal_client/commands.py** (10,243 bytes)
- Shared command functions for game operations
- Used by both CLI (main.py) and REPL (repl.py)
- Functions: submit_defensive_decision, submit_offensive_decision,
resolve_play, quick_play_sequence
- Single source of truth for command logic
2. **terminal_client/arg_parser.py** (7,280 bytes)
- Centralized argument parsing and validation
- Handles defensive/offensive decision arguments
- Validates formats (alignment, depths, hold runners, steal attempts)
3. **terminal_client/completions.py** (10,357 bytes)
- TAB completion support for REPL mode
- Command completions, option completions, dynamic completions
- Game ID completions, defensive/offensive option suggestions
4. **terminal_client/help_text.py** (10,839 bytes)
- Centralized help text and command documentation
- Detailed command descriptions
- Usage examples for all commands
**Updated main modules:**
- terminal_client/main.py - Simplified by using shared commands module
- terminal_client/repl.py - Cleaner with shared functions and completions
**Benefits:**
- DRY: Behavior consistent between CLI and REPL modes
- Maintainability: Changes in one place affect both interfaces
- Testability: Can test commands module independently
- Organization: Clear separation of concerns
## Documentation
**New files:**
- app/models/visual_model_relationships.md
- Visual documentation of model relationships
- Helps understand data flow between models
- terminal_client/update_docs/ (6 phase documentation files)
- Phased documentation for terminal client evolution
- Historical context for implementation decisions
## Tests
**New test files:**
- tests/unit/terminal_client/__init__.py
- tests/unit/terminal_client/test_arg_parser.py
- tests/unit/terminal_client/test_commands.py
- tests/unit/terminal_client/test_completions.py
- tests/unit/terminal_client/test_help_text.py
**Updated tests:**
- Integration tests updated for new runner model
- Unit tests updated for model changes
- All tests passing with new structure
## Summary
- ✅ Simplified game state model (removed RunnerState)
- ✅ Better alignment with database structure
- ✅ Modularized terminal client (DRY principle)
- ✅ Shared command logic between CLI and REPL
- ✅ Comprehensive test coverage
- ✅ Improved documentation
Total changes: 26 files modified/created
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
601 lines
20 KiB
Markdown
601 lines
20 KiB
Markdown
● Terminal Client Improvement Plan - Part 2: Robust Argument Parsing
|
|
|
|
Overview
|
|
|
|
Replace manual string splitting with shlex for robust argument parsing that handles quoted strings, edge cases, and
|
|
complex arguments properly.
|
|
|
|
Files to Create
|
|
|
|
1. Create backend/terminal_client/arg_parser.py
|
|
|
|
"""
|
|
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__}, 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 = {
|
|
'approach': {'type': str, 'default': 'normal'},
|
|
'steal': {'type': 'int_list', 'default': []},
|
|
'hit_run': {'type': bool, 'flag': True, 'default': False},
|
|
'bunt': {'type': bool, 'flag': True, 'default': False}
|
|
}
|
|
|
|
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)
|
|
|
|
Files to Update
|
|
|
|
2. Update backend/terminal_client/repl.py
|
|
|
|
# Add import at top
|
|
from terminal_client.arg_parser import (
|
|
parse_new_game_args,
|
|
parse_defensive_args,
|
|
parse_offensive_args,
|
|
parse_quick_play_args,
|
|
parse_use_game_args,
|
|
ArgumentParseError
|
|
)
|
|
|
|
# Replace do_new_game method:
|
|
def do_new_game(self, arg):
|
|
"""
|
|
Create a new game with lineups and start it.
|
|
|
|
Usage: new_game [--league sba|pd] [--home-team N] [--away-team N]
|
|
|
|
Examples:
|
|
new_game
|
|
new_game --league pd
|
|
new_game --home-team 5 --away-team 3
|
|
"""
|
|
async def _new_game():
|
|
try:
|
|
# Parse arguments with robust parser
|
|
args = parse_new_game_args(arg)
|
|
|
|
# Use shared command
|
|
gid, success = await game_commands.create_new_game(
|
|
league=args['league'],
|
|
home_team=args['home_team'],
|
|
away_team=args['away_team'],
|
|
set_current=True
|
|
)
|
|
|
|
if success:
|
|
self.current_game_id = gid
|
|
|
|
except ArgumentParseError as e:
|
|
display.print_error(f"Invalid arguments: {e}")
|
|
except Exception as e:
|
|
display.print_error(f"Failed to create game: {e}")
|
|
logger.exception("New game error")
|
|
|
|
self._run_async(_new_game())
|
|
|
|
# Replace do_defensive method:
|
|
def do_defensive(self, arg):
|
|
"""
|
|
Submit defensive decision.
|
|
|
|
Usage: defensive [--alignment TYPE] [--infield DEPTH] [--outfield DEPTH] [--hold BASES]
|
|
|
|
Options:
|
|
--alignment normal, shifted_left, shifted_right, extreme_shift
|
|
--infield in, normal, back, double_play
|
|
--outfield in, normal, back
|
|
--hold Comma-separated bases (e.g., 1,3)
|
|
|
|
Examples:
|
|
defensive
|
|
defensive --alignment shifted_left
|
|
defensive --infield double_play --hold 1,3
|
|
"""
|
|
async def _defensive():
|
|
try:
|
|
gid = self._ensure_game()
|
|
await self._ensure_game_loaded(gid)
|
|
|
|
# Parse arguments
|
|
args = parse_defensive_args(arg)
|
|
|
|
# Submit decision
|
|
await game_commands.submit_defensive_decision(
|
|
game_id=gid,
|
|
alignment=args['alignment'],
|
|
infield=args['infield'],
|
|
outfield=args['outfield'],
|
|
hold_runners=args['hold']
|
|
)
|
|
|
|
except ArgumentParseError as e:
|
|
display.print_error(f"Invalid arguments: {e}")
|
|
except ValueError:
|
|
pass # Already printed error
|
|
except Exception as e:
|
|
display.print_error(f"Failed: {e}")
|
|
logger.exception("Defensive error")
|
|
|
|
self._run_async(_defensive())
|
|
|
|
# Replace do_offensive method:
|
|
def do_offensive(self, arg):
|
|
"""
|
|
Submit offensive decision.
|
|
|
|
Usage: offensive [--approach TYPE] [--steal BASES] [--hit-run] [--bunt]
|
|
|
|
Options:
|
|
--approach normal, contact, power, patient
|
|
--steal Comma-separated bases (e.g., 2,3)
|
|
--hit-run Enable hit-and-run (flag)
|
|
--bunt Attempt bunt (flag)
|
|
|
|
Examples:
|
|
offensive
|
|
offensive --approach power
|
|
offensive --steal 2 --hit-run
|
|
"""
|
|
async def _offensive():
|
|
try:
|
|
gid = self._ensure_game()
|
|
await self._ensure_game_loaded(gid)
|
|
|
|
# Parse arguments
|
|
args = parse_offensive_args(arg)
|
|
|
|
# Submit decision
|
|
await game_commands.submit_offensive_decision(
|
|
game_id=gid,
|
|
approach=args['approach'],
|
|
steal_attempts=args['steal'],
|
|
hit_and_run=args['hit_run'],
|
|
bunt_attempt=args['bunt']
|
|
)
|
|
|
|
except ArgumentParseError as e:
|
|
display.print_error(f"Invalid arguments: {e}")
|
|
except ValueError:
|
|
pass
|
|
except Exception as e:
|
|
display.print_error(f"Failed: {e}")
|
|
logger.exception("Offensive error")
|
|
|
|
self._run_async(_offensive())
|
|
|
|
# Replace do_quick_play method:
|
|
def do_quick_play(self, arg):
|
|
"""
|
|
Auto-play multiple plays with default decisions.
|
|
|
|
Usage: quick_play [COUNT]
|
|
|
|
Examples:
|
|
quick_play # Play 1 play
|
|
quick_play 10 # Play 10 plays
|
|
quick_play 27 # Play ~3 innings
|
|
"""
|
|
async def _quick_play():
|
|
try:
|
|
gid = self._ensure_game()
|
|
await self._ensure_game_loaded(gid)
|
|
|
|
# Parse arguments
|
|
args = parse_quick_play_args(arg)
|
|
|
|
# Execute quick play
|
|
plays_completed = await game_commands.quick_play_rounds(
|
|
game_id=gid,
|
|
count=args['count']
|
|
)
|
|
|
|
display.print_success(f"Completed {plays_completed} plays")
|
|
|
|
except ArgumentParseError as e:
|
|
display.print_error(f"Invalid arguments: {e}")
|
|
except ValueError:
|
|
pass
|
|
except Exception as e:
|
|
display.print_error(f"Failed: {e}")
|
|
logger.exception("Quick play error")
|
|
|
|
self._run_async(_quick_play())
|
|
|
|
# Replace do_use_game method:
|
|
def do_use_game(self, arg):
|
|
"""
|
|
Switch to a different game.
|
|
|
|
Usage: use_game <game_id>
|
|
|
|
Example:
|
|
use_game a1b2c3d4-e5f6-7890-abcd-ef1234567890
|
|
"""
|
|
try:
|
|
# Parse arguments
|
|
args = parse_use_game_args(arg)
|
|
|
|
gid = UUID(args['game_id'])
|
|
self.current_game_id = gid
|
|
Config.set_current_game(gid)
|
|
display.print_success(f"Switched to game: {gid}")
|
|
|
|
except ArgumentParseError as e:
|
|
display.print_error(f"Invalid arguments: {e}")
|
|
except ValueError:
|
|
display.print_error(f"Invalid UUID: {arg}")
|
|
|
|
Testing Plan
|
|
|
|
3. Create backend/tests/unit/terminal_client/test_arg_parser.py
|
|
|
|
"""
|
|
Unit tests for argument parser.
|
|
"""
|
|
import pytest
|
|
|
|
from terminal_client.arg_parser import (
|
|
CommandArgumentParser,
|
|
ArgumentParseError,
|
|
parse_new_game_args,
|
|
parse_defensive_args,
|
|
parse_offensive_args
|
|
)
|
|
|
|
|
|
class TestCommandArgumentParser:
|
|
"""Tests for CommandArgumentParser."""
|
|
|
|
def test_parse_simple_string_arg(self):
|
|
"""Test parsing simple string argument."""
|
|
schema = {'name': {'type': str, 'default': 'default'}}
|
|
result = CommandArgumentParser.parse_args('--name test', schema)
|
|
assert result['name'] == 'test'
|
|
|
|
def test_parse_integer_arg(self):
|
|
"""Test parsing integer argument."""
|
|
schema = {'count': {'type': int, 'default': 1}}
|
|
result = CommandArgumentParser.parse_args('--count 42', schema)
|
|
assert result['count'] == 42
|
|
|
|
def test_parse_flag_arg(self):
|
|
"""Test parsing boolean flag."""
|
|
schema = {
|
|
'verbose': {'type': bool, 'flag': True, 'default': False}
|
|
}
|
|
result = CommandArgumentParser.parse_args('--verbose', schema)
|
|
assert result['verbose'] is True
|
|
|
|
def test_parse_int_list(self):
|
|
"""Test parsing comma-separated integer list."""
|
|
schema = {'bases': {'type': 'int_list', 'default': []}}
|
|
result = CommandArgumentParser.parse_args('--bases 1,2,3', schema)
|
|
assert result['bases'] == [1, 2, 3]
|
|
|
|
def test_parse_quoted_string(self):
|
|
"""Test parsing quoted string with spaces."""
|
|
schema = {'message': {'type': str, 'default': ''}}
|
|
result = CommandArgumentParser.parse_args('--message "hello world"', schema)
|
|
assert result['message'] == 'hello world'
|
|
|
|
def test_parse_positional_arg(self):
|
|
"""Test parsing positional argument."""
|
|
schema = {
|
|
'count': {'type': int, 'positional': True, 'default': 1}
|
|
}
|
|
result = CommandArgumentParser.parse_args('10', schema)
|
|
assert result['count'] == 10
|
|
|
|
def test_parse_mixed_args(self):
|
|
"""Test parsing mix of options and positional."""
|
|
schema = {
|
|
'count': {'type': int, 'positional': True, 'default': 1},
|
|
'league': {'type': str, 'default': 'sba'}
|
|
}
|
|
result = CommandArgumentParser.parse_args('5 --league pd', schema)
|
|
assert result['count'] == 5
|
|
assert result['league'] == 'pd'
|
|
|
|
def test_parse_unknown_option_raises(self):
|
|
"""Test that unknown option raises error."""
|
|
schema = {'name': {'type': str, 'default': 'default'}}
|
|
with pytest.raises(ArgumentParseError, match="Unknown option"):
|
|
CommandArgumentParser.parse_args('--invalid test', schema)
|
|
|
|
def test_parse_missing_value_raises(self):
|
|
"""Test that missing value for option raises error."""
|
|
schema = {'name': {'type': str, 'default': 'default'}}
|
|
with pytest.raises(ArgumentParseError, match="requires a value"):
|
|
CommandArgumentParser.parse_args('--name', schema)
|
|
|
|
def test_parse_invalid_type_raises(self):
|
|
"""Test that invalid type conversion raises error."""
|
|
schema = {'count': {'type': int, 'default': 1}}
|
|
with pytest.raises(ArgumentParseError, match="expected int"):
|
|
CommandArgumentParser.parse_args('--count abc', schema)
|
|
|
|
def test_parse_empty_string(self):
|
|
"""Test parsing empty string returns defaults."""
|
|
schema = {
|
|
'name': {'type': str, 'default': 'default'},
|
|
'count': {'type': int, 'default': 1}
|
|
}
|
|
result = CommandArgumentParser.parse_args('', schema)
|
|
assert result['name'] == 'default'
|
|
assert result['count'] == 1
|
|
|
|
def test_parse_hyphen_to_underscore(self):
|
|
"""Test that hyphens in options convert to underscores."""
|
|
schema = {'home_team': {'type': int, 'default': 1}}
|
|
result = CommandArgumentParser.parse_args('--home-team 5', schema)
|
|
assert result['home_team'] == 5
|
|
|
|
|
|
class TestPrebuiltParsers:
|
|
"""Tests for pre-built parser functions."""
|
|
|
|
def test_parse_new_game_args_defaults(self):
|
|
"""Test new_game parser with defaults."""
|
|
result = parse_new_game_args('')
|
|
assert result['league'] == 'sba'
|
|
assert result['home_team'] == 1
|
|
assert result['away_team'] == 2
|
|
|
|
def test_parse_new_game_args_custom(self):
|
|
"""Test new_game parser with custom values."""
|
|
result = parse_new_game_args('--league pd --home-team 5 --away-team 3')
|
|
assert result['league'] == 'pd'
|
|
assert result['home_team'] == 5
|
|
assert result['away_team'] == 3
|
|
|
|
def test_parse_defensive_args_defaults(self):
|
|
"""Test defensive parser with defaults."""
|
|
result = parse_defensive_args('')
|
|
assert result['alignment'] == 'normal'
|
|
assert result['infield'] == 'normal'
|
|
assert result['outfield'] == 'normal'
|
|
assert result['hold'] == []
|
|
|
|
def test_parse_defensive_args_with_hold(self):
|
|
"""Test defensive parser with hold runners."""
|
|
result = parse_defensive_args('--alignment shifted_left --hold 1,3')
|
|
assert result['alignment'] == 'shifted_left'
|
|
assert result['hold'] == [1, 3]
|
|
|
|
def test_parse_offensive_args_flags(self):
|
|
"""Test offensive parser with flags."""
|
|
result = parse_offensive_args('--approach power --hit-run --bunt')
|
|
assert result['approach'] == 'power'
|
|
assert result['hit_run'] is True
|
|
assert result['bunt'] is True
|
|
|
|
def test_parse_offensive_args_steal(self):
|
|
"""Test offensive parser with steal attempts."""
|
|
result = parse_offensive_args('--steal 2,3')
|
|
assert result['steal'] == [2, 3]
|
|
|
|
Benefits of Improved Parsing
|
|
|
|
1. Handles quoted strings: --message "double steal attempt" works correctly
|
|
2. Better error messages: Clear feedback on what went wrong
|
|
3. Type validation: Automatic conversion with helpful errors
|
|
4. Consistent behavior: Same parsing logic for REPL and CLI
|
|
5. Extensible: Easy to add new argument types
|
|
6. Edge case handling: Properly handles empty strings, trailing spaces, etc.
|
|
|
|
Example Usage
|
|
|
|
# Before (manual parsing, breaks on spaces):
|
|
⚾ > defensive --hold 1,3 # Works
|
|
⚾ > defensive --alignment shifted left # Breaks! (sees 'shifted' and 'left' separately)
|
|
|
|
# After (shlex parsing, handles correctly):
|
|
⚾ > defensive --hold 1,3 # Still works
|
|
⚾ > defensive --alignment "shifted left" # Now works with quotes!
|
|
⚾ > quick_play 10 # Positional arg works
|
|
⚾ > offensive --hit-run --bunt # Multiple flags work
|