Problem
When a user provides a value that doesn't match a parameterized type (e.g., OptionsType), the command is not recognized at all, and the user sees a generic NoMatchingCommandFoundError ("Unknown command").
This happens because match() serves two purposes simultaneously:
- Command identification (routing) — determining which command the user intended
- Value validation — checking that parameter values are acceptable
Since OptionsType.match() returns False for values not in the valid list, the entire command fails to match. The interpreter treats it as if the command doesn't exist, rather than as a recognized command with an invalid parameter.
Example
Given a command registered as:
["environment", "maintenance", "on", OptionsType(["dev", "stg"])]
Input: /environment maintenance on production
Current behavior: NoMatchingCommandFoundError → "Unknown command"
Expected behavior: Command is recognized, but a clear validation error is returned → "Invalid option 'production'. Valid options: dev, stg"
This affects all parameterized types (OptionsType, IntegerType, RegexType, etc.), not just OptionsType.
Proposal: Separate matching from validation
Add a validation phase to the interpreter that runs after structural matching fails strict matching. This keeps backward compatibility while providing actionable error messages.
1. Add validate() to BaseType
class BaseType:
# existing methods...
def validate(self, word: str, context: Context, partial_line: list[str] | None = None) -> str | None:
"""Return None if valid, or an error message string if invalid."""
return None # default: always valid (backward compatible)
Each concrete type overrides with its own validation logic and message:
OptionsType.validate() → "Invalid option 'foo'. Valid options: dev, stg"
IntegerType.validate() → "Expected integer between 1 and 10, got 'abc'"
RegexType.validate() → "Value 'xyz' doesn't match expected pattern: <regex>"
StringType.validate() → None (any non-empty string is valid)
2. Add structural_match() to Command
A new method that matches only the command structure: keyword positions must match exactly, but parameterized positions accept any non-empty string.
def structural_match(self, tokens: list[str], context: Context) -> bool:
if not self.context_match(context):
return False
if len(tokens) != len(self.keywords):
return False
for index, word in enumerate(tokens):
definition = self.definitions[index]
if isinstance(definition, KeywordType):
if not definition.match(word, context, partial_line=tokens):
return False
else:
if not word: # parameterized position: accept any non-empty string
return False
return True
3. Add validate_parameters() to Command
Collects validation errors from all parameterized positions:
def validate_parameters(self, tokens: list[str], context: Context) -> list[str]:
errors = []
for index, word in enumerate(tokens):
definition = self.definitions[index]
if not isinstance(definition, KeywordType):
error = definition.validate(word, context, partial_line=tokens)
if error:
errors.append(error)
return errors
4. Add CommandValidationError exception
class CommandValidationError(EvalError):
def __init__(self, command: Command, errors: list[str]) -> None:
self.command = command
self.errors = errors
5. Two-phase matching in Interpreter._matching_command()
def _matching_command(self, tokens: list[str], line_text: str) -> Command:
# Phase 1: strict match (current behavior)
matching = self._select_matching_commands(tokens)
if len(matching) == 1:
return matching[0]
if len(matching) > 1:
raise AmbiguousCommandError(matching)
# Phase 2: structural match + validation
structural = [cmd for cmd in self._commands
if cmd.structural_match(tokens, self.actual_context())]
if len(structural) == 1:
errors = structural[0].validate_parameters(tokens, self.actual_context())
if errors:
raise CommandValidationError(structural[0], errors)
# Phase 3: no match at all
raise NoMatchingCommandFoundError(line_text)
Why this approach
- Backward compatible: strict matching runs first; existing behavior is preserved for valid inputs
- All types benefit: not limited to
OptionsType — any parameterized type can provide meaningful validation messages
- Clean separation:
match() handles routing, validate() handles error reporting. Each type already knows its constraints (valid options, integer bounds, regex pattern), so the validation messages are natural
- Autocompletion unaffected:
complete() and partial_match() remain unchanged
- Minimal surface area: one new method on
BaseType, one new method on Command, one new exception, and a small change in the interpreter's matching flow
Alternatives considered
strict parameter on types
Adding strict=False to make match() permissive. Rejected because it pushes validation responsibility to the caller (who would need to re-validate), and doesn't provide a channel for error messages.
Lenient wrapper type
Lenient(OptionsType(["dev", "stg"])) — a decorator that makes match permissive. Rejected because it adds complexity at the registration site and still needs a validation mechanism to report errors.
🤖 Generated with Claude Code
Problem
When a user provides a value that doesn't match a parameterized type (e.g.,
OptionsType), the command is not recognized at all, and the user sees a genericNoMatchingCommandFoundError("Unknown command").This happens because
match()serves two purposes simultaneously:Since
OptionsType.match()returnsFalsefor values not in the valid list, the entire command fails to match. The interpreter treats it as if the command doesn't exist, rather than as a recognized command with an invalid parameter.Example
Given a command registered as:
Input:
/environment maintenance on productionCurrent behavior:
NoMatchingCommandFoundError→ "Unknown command"Expected behavior: Command is recognized, but a clear validation error is returned →
"Invalid option 'production'. Valid options: dev, stg"This affects all parameterized types (
OptionsType,IntegerType,RegexType, etc.), not justOptionsType.Proposal: Separate matching from validation
Add a validation phase to the interpreter that runs after structural matching fails strict matching. This keeps backward compatibility while providing actionable error messages.
1. Add
validate()toBaseTypeEach concrete type overrides with its own validation logic and message:
OptionsType.validate()→"Invalid option 'foo'. Valid options: dev, stg"IntegerType.validate()→"Expected integer between 1 and 10, got 'abc'"RegexType.validate()→"Value 'xyz' doesn't match expected pattern: <regex>"StringType.validate()→None(any non-empty string is valid)2. Add
structural_match()toCommandA new method that matches only the command structure: keyword positions must match exactly, but parameterized positions accept any non-empty string.
3. Add
validate_parameters()toCommandCollects validation errors from all parameterized positions:
4. Add
CommandValidationErrorexception5. Two-phase matching in
Interpreter._matching_command()Why this approach
OptionsType— any parameterized type can provide meaningful validation messagesmatch()handles routing,validate()handles error reporting. Each type already knows its constraints (valid options, integer bounds, regex pattern), so the validation messages are naturalcomplete()andpartial_match()remain unchangedBaseType, one new method onCommand, one new exception, and a small change in the interpreter's matching flowAlternatives considered
strictparameter on typesAdding
strict=Falseto makematch()permissive. Rejected because it pushes validation responsibility to the caller (who would need to re-validate), and doesn't provide a channel for error messages.Lenient wrapper type
Lenient(OptionsType(["dev", "stg"]))— a decorator that makes match permissive. Rejected because it adds complexity at the registration site and still needs a validation mechanism to report errors.🤖 Generated with Claude Code