tessl install tessl/maven-io-cucumber--cucumber-expressions@19.0.0Cucumber Expressions are simple patterns for matching Step Definitions with Gherkin steps
The matching system extracts typed arguments from matched text with position information and group hierarchies. It provides detailed information about matched values, their types, and their locations in the source text.
Represents a matched argument with value and type information.
package io.cucumber.cucumberexpressions;
import java.lang.reflect.Type;
import org.jspecify.annotations.Nullable;
/**
* Matched argument with value and type information
* Immutable value object - thread-safe
*/
public final class Argument<T> {
/**
* Get the matched group
* Contains position and text information about the match
* @return Group representing the matched text
*/
public Group getGroup();
/**
* Get the transformed value
* Returns the result of applying the parameter type's transformer
* May be null if the transformer returns null or the match was empty
*
* IMPORTANT: Transformer exceptions are thrown from this method
* If the transformer throws an exception, calling getValue() will re-throw it
*
* @return Transformed value of type T
* @throws RuntimeException if transformer threw an exception during transformation
*/
@Nullable
public T getValue();
/**
* Get the Java type of the transformed value
* @return Java Type (e.g., Integer.class, String.class)
*/
public Type getType();
/**
* Get the parameter type definition
* @return ParameterType used for this argument
*/
public ParameterType<T> getParameterType();
}Usage Examples:
import io.cucumber.cucumberexpressions.*;
import java.util.Locale;
import java.util.List;
import java.util.Optional;
// Setup
ParameterTypeRegistry registry = new ParameterTypeRegistry(Locale.ENGLISH);
ExpressionFactory factory = new ExpressionFactory(registry);
// Match and extract arguments
Expression expr = factory.createExpression("I have {int} cucumbers");
Optional<List<Argument<?>>> match = expr.match("I have 42 cucumbers");
if (match.isPresent()) {
List<Argument<?>> arguments = match.get();
Argument<?> arg = arguments.get(0);
// Get the transformed value
Integer value = (Integer) arg.getValue();
// value = 42
// Get the matched group (text and position)
Group group = arg.getGroup();
String matchedText = group.getValue();
// matchedText = "42"
int start = group.getStart();
// start = 7
int end = group.getEnd();
// end = 9
// Get type information
Type type = arg.getType();
// type = Integer.class
ParameterType<?> paramType = arg.getParameterType();
String paramName = paramType.getName();
// paramName = "int"
}Multiple Arguments:
Expression expr = factory.createExpression("Transfer {int} from account {int} to account {int}");
Optional<List<Argument<?>>> match = expr.match("Transfer 100 from account 12345 to account 67890");
if (match.isPresent()) {
List<Argument<?>> arguments = match.get();
Integer amount = (Integer) arguments.get(0).getValue();
// amount = 100
Integer fromAccount = (Integer) arguments.get(1).getValue();
// fromAccount = 12345
Integer toAccount = (Integer) arguments.get(2).getValue();
// toAccount = 67890
// Access all argument positions
for (int i = 0; i < arguments.size(); i++) {
Argument<?> arg = arguments.get(i);
Group group = arg.getGroup();
System.out.printf("Argument %d: value=%s, position=%d-%d%n",
i, arg.getValue(), group.getStart(), group.getEnd());
}
}Custom Types:
// Define custom parameter type
ParameterType<Color> colorType = new ParameterType<>(
"color",
"red|blue|green",
Color.class,
(String s) -> new Color(s)
);
registry.defineParameterType(colorType);
Expression expr = factory.createExpression("I have a {color} ball");
Optional<List<Argument<?>>> match = expr.match("I have a red ball");
if (match.isPresent()) {
Argument<?> arg = match.get().get(0);
// Get typed value
Color color = (Color) arg.getValue();
// color = Color.RED
// Get parameter type info
ParameterType<?> paramType = arg.getParameterType();
// paramType.getName() = "color"
// paramType.getType() = Color.class
// Get raw matched text
String rawText = arg.getGroup().getValue();
// rawText = "red"
}Transformer Exception Handling:
// Transformer that validates and may throw exceptions
ParameterType<Age> ageType = new ParameterType<>(
"age",
"\\d+",
Age.class,
(String s) -> {
int value = Integer.parseInt(s);
if (value < 0 || value > 150) {
throw new IllegalArgumentException("Age must be 0-150, got: " + value);
}
return new Age(value);
}
);
registry.defineParameterType(ageType);
Expression expr = factory.createExpression("Person is {age} years old");
Optional<List<Argument<?>>> match = expr.match("Person is 200 years old");
if (match.isPresent()) {
Argument<?> arg = match.get().get(0);
try {
Age age = (Age) arg.getValue();
// This throws IllegalArgumentException from transformer
} catch (IllegalArgumentException e) {
System.err.println("Validation error: " + e.getMessage());
// Handle validation error...
}
}Represents a matched text group with position information and hierarchical structure.
package io.cucumber.cucumberexpressions;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;
/**
* Matched text group with position information
* Immutable value object with hierarchical structure
*/
public final class Group {
/**
* Parse a Pattern into Groups
* Useful for analyzing regular expression structure
* Static factory method for regex pattern analysis
* @param expression - Pattern to parse
* @return Collection of root-level groups
*/
public static Collection<Group> parse(Pattern expression);
/**
* Get the matched value
* Returns null for non-capturing groups
* @return Matched text value
*/
@Nullable
public String getValue();
/**
* Get start index of match in source text
* @return Zero-based start index (inclusive)
*/
public int getStart();
/**
* Get end index of match in source text
* End index is exclusive (points to character after last matched character)
* @return Zero-based end index (exclusive)
*/
public int getEnd();
/**
* Get child groups (capture groups within this group)
* Returns empty Optional if no children
* @return Optional containing list of child groups
*/
public Optional<List<Group>> getChildren();
/**
* Get all values from children or self
* Flattens the group hierarchy into a list
* First element is always the complete match, followed by capture groups
* @return List of all matched values (nulls preserved for non-capturing groups)
*/
public List<@Nullable String> getValues();
}Usage Examples:
import io.cucumber.cucumberexpressions.*;
import java.util.List;
import java.util.Optional;
// Basic group information
Expression expr = factory.createExpression("I have {int} cucumbers");
Optional<List<Argument<?>>> match = expr.match("I have 42 cucumbers");
if (match.isPresent()) {
Argument<?> arg = match.get().get(0);
Group group = arg.getGroup();
// Get matched text
String value = group.getValue();
// value = "42"
// Get position
int start = group.getStart();
int end = group.getEnd();
// Text from index 7 to 9: "42"
// Extract from source text
String sourceText = "I have 42 cucumbers";
String extracted = sourceText.substring(start, end);
// extracted = "42"
}Group Hierarchy:
import java.util.regex.Pattern;
import java.util.Collection;
// Parse a pattern with capture groups
Pattern pattern = Pattern.compile("(\\d+)/(\\d+)/(\\d+)");
Collection<Group> groups = Group.parse(pattern);
// Groups contain capture group structure
for (Group group : groups) {
System.out.println("Group: " + group.getValue());
Optional<List<Group>> children = group.getChildren();
if (children.isPresent()) {
for (Group child : children.get()) {
System.out.println(" Child: " + child.getValue());
}
}
}Values Extraction:
// Define parameter type with multiple capture groups
ParameterType<Point> pointType = new ParameterType<>(
"point",
"(\\d+),(\\d+)",
Point.class,
(String[] args) -> new Point(
Integer.parseInt(args[0]),
Integer.parseInt(args[1])
)
);
registry.defineParameterType(pointType);
Expression expr = factory.createExpression("Point at {point}");
Optional<List<Argument<?>>> match = expr.match("Point at 10,20");
if (match.isPresent()) {
Argument<?> arg = match.get().get(0);
Group group = arg.getGroup();
// Get all values (flattened)
List<String> values = group.getValues();
// values = ["10,20", "10", "20"]
// First value is the complete match, subsequent are capture groups
// Get child groups
Optional<List<Group>> children = group.getChildren();
if (children.isPresent()) {
Group child1 = children.get().get(0);
// child1.getValue() = "10"
// child1.getStart() = position of "10"
// child1.getEnd() = position after "10"
Group child2 = children.get().get(1);
// child2.getValue() = "20"
// child2.getStart() = position of "20"
// child2.getEnd() = position after "20"
}
}import io.cucumber.cucumberexpressions.*;
import java.util.List;
import java.util.Optional;
// Successful match
Expression expr = factory.createExpression("I have {int} cucumbers");
Optional<List<Argument<?>>> match = expr.match("I have 42 cucumbers");
if (match.isPresent()) {
List<Argument<?>> arguments = match.get();
// Process arguments
} else {
// No match
}
// Failed match
Optional<List<Argument<?>>> noMatch = expr.match("I have many cucumbers");
// noMatch.isEmpty() = true
// Alternative handling
match.ifPresent(arguments -> {
// Process arguments
Integer count = (Integer) arguments.get(0).getValue();
});
// With default
List<Argument<?>> arguments = match.orElse(List.of());
// Pattern matching (Java 17+)
switch (match) {
case Optional<List<Argument<?>>> opt when opt.isPresent() -> {
List<Argument<?>> args = opt.get();
// Process match
}
default -> {
// No match
}
}Use group position information for text highlighting, error reporting, or replacement:
import io.cucumber.cucumberexpressions.*;
import java.util.List;
import java.util.Optional;
String sourceText = "I have 42 cucumbers in my belly";
Expression expr = factory.createExpression("I have {int} cucumbers");
Optional<List<Argument<?>>> match = expr.match(sourceText);
if (match.isPresent()) {
Argument<?> arg = match.get().get(0);
Group group = arg.getGroup();
int start = group.getStart();
int end = group.getEnd();
// Highlight matched text
String before = sourceText.substring(0, start);
String matched = sourceText.substring(start, end);
String after = sourceText.substring(end);
String highlighted = before + "[" + matched + "]" + after;
// highlighted = "I have [42] cucumbers in my belly"
// Replace matched text
String replacement = "100";
String replaced = before + replacement + after;
// replaced = "I have 100 cucumbers in my belly"
// Error reporting with caret indicators
System.err.println("Error at position " + start + "-" + end);
System.err.println(sourceText);
System.err.println(" ".repeat(start) + "^".repeat(end - start));
// Output:
// Error at position 7-9
// I have 42 cucumbers in my belly
// ^^
// Syntax highlighting (conceptual)
List<HighlightSpan> spans = new ArrayList<>();
spans.add(new HighlightSpan(start, end, "parameter"));
}Multiple Parameter Types:
Expression expr = factory.createExpression(
"User {string} has {int} items worth {float} dollars"
);
Optional<List<Argument<?>>> match = expr.match(
"User \"John Doe\" has 5 items worth 99.99 dollars"
);
if (match.isPresent()) {
List<Argument<?>> arguments = match.get();
String username = (String) arguments.get(0).getValue();
// username = "John Doe" (quotes removed by {string} type)
Integer itemCount = (Integer) arguments.get(1).getValue();
// itemCount = 5
Float totalValue = (Float) arguments.get(2).getValue();
// totalValue = 99.99f
// Get position of each argument for UI highlighting
for (int i = 0; i < arguments.size(); i++) {
Argument<?> arg = arguments.get(i);
Group group = arg.getGroup();
System.out.printf("Argument %d: value=%s, type=%s, position=%d-%d, text='%s'%n",
i,
arg.getValue(),
arg.getType().getTypeName(),
group.getStart(),
group.getEnd(),
group.getValue());
}
// Output:
// Argument 0: value=John Doe, type=java.lang.String, position=5-15, text='"John Doe"'
// Argument 1: value=5, type=java.lang.Integer, position=20-21, text='5'
// Argument 2: value=99.99, type=java.lang.Float, position=34-39, text='99.99'
}Optional and Alternative Text:
// Optional text
Expression expr1 = factory.createExpression("I have {int} cucumber(s)");
Optional<List<Argument<?>>> match1 = expr1.match("I have 1 cucumber");
Optional<List<Argument<?>>> match2 = expr1.match("I have 42 cucumbers");
// Both match, same argument extraction
// match1.get().get(0).getValue() = 1
// match2.get().get(0).getValue() = 42
// Alternative text
Expression expr2 = factory.createExpression("I have {int} in my belly/stomach");
Optional<List<Argument<?>>> match3 = expr2.match("I have 42 in my belly");
Optional<List<Argument<?>>> match4 = expr2.match("I have 42 in my stomach");
// Both match, same argument extraction
// match3.get().get(0).getValue() = 42
// match4.get().get(0).getValue() = 42
// The matched text position will differ
Group group3 = match3.get().get(0).getGroup();
Group group4 = match4.get().get(0).getGroup();
// Both groups have same value ("42") but may be at different positionsRegular Expression Matching:
Regular expression capture groups are automatically typed based on matching parameter types:
// Regular expression with capture groups
Expression regexExpr = factory.createExpression("^I have (\\d+) cucumbers$");
Optional<List<Argument<?>>> match = regexExpr.match("I have 42 cucumbers");
if (match.isPresent()) {
Argument<?> arg = match.get().get(0);
// Value is automatically transformed to Integer (not String!)
// The \d+ pattern matches the built-in {int} parameter type
Integer captured = (Integer) arg.getValue();
// captured = 42 (Integer object, not String "42")
Group group = arg.getGroup();
// group.getValue() = "42" (raw matched text as String)
// group.getStart() = 7
// group.getEnd() = 9
// Note: Group.getValue() returns the raw matched text as String
// while Argument.getValue() returns the transformed typed value
}Understanding Regex Type Transformation:
// Built-in types are automatically detected in regex patterns
Expression intExpr = factory.createExpression("^Count: (\\d+)$");
Optional<List<Argument<?>>> intMatch = intExpr.match("Count: 123");
if (intMatch.isPresent()) {
Object value = intMatch.get().get(0).getValue();
// value is Integer 123, not String "123"
}
Expression floatExpr = factory.createExpression("^Price: (-?[0-9]*\\.?[0-9]+)$");
Optional<List<Argument<?>>> floatMatch = floatExpr.match("Price: 99.99");
if (floatMatch.isPresent()) {
Object value = floatMatch.get().get(0).getValue();
// value may be Float depending on pattern match
}
// To ensure String values from regex, use patterns that don't match parameter types
Expression stringExpr = factory.createExpression("^Name: (.+)$");
Optional<List<Argument<?>>> stringMatch = stringExpr.match("Name: John");
if (stringMatch.isPresent()) {
Object value = stringMatch.get().get(0).getValue();
// value is String "John" (pattern .+ doesn't match any specific type)
}// Use type hints for explicit type conversion with anonymous parameters
Expression expr = factory.createExpression("Value is {}");
// Without type hint - uses default transformer
Optional<List<Argument<?>>> match1 = expr.match("Value is 42");
Object value1 = match1.get().get(0).getValue();
// value1 type depends on default transformer (typically String)
// With type hint for Integer
Optional<List<Argument<?>>> match2 = expr.match("Value is 42", Integer.class);
Integer value2 = (Integer) match2.get().get(0).getValue();
// value2 = 42 (explicitly converted to Integer)
// With type hint for custom type
Optional<List<Argument<?>>> match3 = expr.match("Value is 42", CustomId.class);
CustomId value3 = (CustomId) match3.get().get(0).getValue();
// Uses default transformer to convert to CustomId
// Multiple anonymous parameters with different type hints
Expression expr2 = factory.createExpression("Data: {} and {} and {}");
Optional<List<Argument<?>>> match4 = expr2.match(
"Data: 42 and hello and 3.14",
Integer.class, String.class, Double.class
);
// First argument: Integer 42
// Second argument: String "hello"
// Third argument: Double 3.14// Transformers can return null
Transformer<String> nullableTransformer = (String arg) -> {
return arg == null || arg.isEmpty() ? null : arg;
};
ParameterType<String> nullableType = new ParameterType<>(
"nullable",
".*",
String.class,
nullableTransformer
);
registry.defineParameterType(nullableType);
Expression expr = factory.createExpression("Value is {nullable}");
Optional<List<Argument<?>>> match = expr.match("Value is ");
if (match.isPresent()) {
Argument<?> arg = match.get().get(0);
String value = (String) arg.getValue();
// value might be null if transformer returned null
if (value == null) {
System.out.println("Transformer returned null");
}
}Pattern 1: Argument Extraction Helper
public class ArgumentExtractor {
public static <T> T extractValue(Optional<List<Argument<?>>> match,
int index, Class<T> expectedType) {
if (match.isEmpty()) {
throw new IllegalArgumentException("No match found");
}
List<Argument<?>> args = match.get();
if (index >= args.size()) {
throw new IndexOutOfBoundsException("Argument index out of range: " + index);
}
Object value = args.get(index).getValue();
if (!expectedType.isInstance(value)) {
throw new ClassCastException(
"Expected " + expectedType + " but got " + value.getClass()
);
}
return expectedType.cast(value);
}
// Usage
Optional<List<Argument<?>>> match = expr.match("I have 42 cucumbers");
Integer count = ArgumentExtractor.extractValue(match, 0, Integer.class);
}Pattern 2: Position-Based Highlighting
public class SyntaxHighlighter {
public String highlightArguments(String text, List<Argument<?>> arguments) {
StringBuilder result = new StringBuilder();
int lastEnd = 0;
for (Argument<?> arg : arguments) {
Group group = arg.getGroup();
// Append text before argument
result.append(text, lastEnd, group.getStart());
// Append highlighted argument
result.append("<span class='parameter' data-type='")
.append(arg.getParameterType().getName())
.append("'>")
.append(text, group.getStart(), group.getEnd())
.append("</span>");
lastEnd = group.getEnd();
}
// Append remaining text
result.append(text.substring(lastEnd));
return result.toString();
}
}Pattern 3: Validation with Position Information
public class ArgumentValidator {
public static class ValidationError {
public final int argumentIndex;
public final int start;
public final int end;
public final String message;
public ValidationError(int argumentIndex, int start, int end, String message) {
this.argumentIndex = argumentIndex;
this.start = start;
this.end = end;
this.message = message;
}
}
public List<ValidationError> validate(Optional<List<Argument<?>>> match) {
if (match.isEmpty()) {
return List.of();
}
List<ValidationError> errors = new ArrayList<>();
List<Argument<?>> arguments = match.get();
for (int i = 0; i < arguments.size(); i++) {
Argument<?> arg = arguments.get(i);
Group group = arg.getGroup();
try {
Object value = arg.getValue();
// Perform custom validation
if (value instanceof Integer && (Integer) value < 0) {
errors.add(new ValidationError(
i,
group.getStart(),
group.getEnd(),
"Value must be positive"
));
}
} catch (Exception e) {
errors.add(new ValidationError(
i,
group.getStart(),
group.getEnd(),
"Transformation error: " + e.getMessage()
));
}
}
return errors;
}
}Pattern 4: Diff-Based Text Replacement
public class TextReplacer {
public String replaceArguments(String text,
Optional<List<Argument<?>>> match,
Function<Argument<?>, String> replacer) {
if (match.isEmpty()) {
return text;
}
List<Argument<?>> arguments = match.get();
StringBuilder result = new StringBuilder();
int lastEnd = 0;
for (Argument<?> arg : arguments) {
Group group = arg.getGroup();
// Copy text before this argument
result.append(text, lastEnd, group.getStart());
// Apply replacement
String replacement = replacer.apply(arg);
result.append(replacement);
lastEnd = group.getEnd();
}
// Copy remaining text
result.append(text.substring(lastEnd));
return result.toString();
}
// Usage
String replaced = replacer.replaceArguments(
"I have 42 cucumbers and 10 tomatoes",
match,
arg -> {
Integer value = (Integer) arg.getValue();
return String.valueOf(value * 2); // Double all numbers
}
);
// Result: "I have 84 cucumbers and 20 tomatoes"
}Empty Matches:
Expression expr = factory.createExpression("");
Optional<List<Argument<?>>> match1 = expr.match("");
// match1.isPresent() = true
// match1.get() = [] (empty list)
Optional<List<Argument<?>>> match2 = expr.match("any text");
// match2.isPresent() = falseOverlapping Groups (Not Supported):
// Cucumber Expressions don't support overlapping parameter groups
// This is not possible:
// "I have {int} {word}" matching "I have 42cucumbers"
// Parameters must be separated by literal textZero-Width Matches:
// Parameter types with zero-width patterns
ParameterType<Marker> markerType = new ParameterType<>(
"marker",
"",
Marker.class,
(String s) -> new Marker()
);
// Use case: position markers, boundariesUnicode Positions:
// Positions are based on Java char indices (UTF-16 code units)
Expression expr = factory.createExpression("Emoji {int} test");
Optional<List<Argument<?>>> match = expr.match("Emoji 42 test");
if (match.isPresent()) {
Group group = match.get().get(0).getGroup();
int start = group.getStart();
// start = position in Java char indices
// For proper Unicode handling with surrogate pairs:
String text = "Emoji 42 test";
int codePointStart = text.codePointCount(0, start);
}Matching Performance:
Memory Usage:
Optimization Tips:
// GOOD: Extract values once
List<Argument<?>> args = match.get();
Integer value = (Integer) args.get(0).getValue();
// Reuse value multiple times
// AVOID: Repeated getValue() calls if transformer is expensive
for (int i = 0; i < 1000; i++) {
Integer value = (Integer) match.get().get(0).getValue();
// getValue() is called 1000 times (transformer runs 1000 times if not cached)
}