tessl install tessl/maven-io-cucumber--cucumber-expressions@19.0.0Cucumber Expressions are simple patterns for matching Step Definitions with Gherkin steps
The expression system provides core pattern matching functionality, supporting both Cucumber Expression syntax and Regular Expressions. The ExpressionFactory automatically determines which expression type to create based on string format using simple heuristics.
Core interface for matching text against patterns and extracting typed arguments.
package io.cucumber.cucumberexpressions;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
/**
* Core interface for matching text against patterns
* Implementations are immutable and thread-safe
*/
public interface Expression {
/**
* Match text against the expression and extract arguments
* Thread-safe operation - safe for concurrent calls on same instance
*
* @param text - Text to match
* @param typeHints - Optional type hints for argument conversion (used with anonymous {} parameters)
* @return Optional containing list of matched arguments, or empty if no match
*/
Optional<List<Argument<?>>> match(String text, Type... typeHints);
/**
* Get the compiled regular expression pattern
* @return Pattern representing the expression
*/
Pattern getRegexp();
/**
* Get the source expression string
* @return Original expression string
*/
String getSource();
}Implementations:
CucumberExpression - Cucumber Expression syntax with parameter types (e.g., "I have {int} cucumbers")RegularExpression - Standard Java regex with parameter type extraction (e.g., "^I have (\\d+) cucumbers$")Both implementations are package-private and should be created via ExpressionFactory.
Factory class that creates Expression instances from strings using heuristics to determine the appropriate type.
package io.cucumber.cucumberexpressions;
/**
* Creates a CucumberExpression or RegularExpression from a String
* using heuristics. This is particularly useful for languages that don't have a
* literal syntax for regular expressions.
*
* A string that starts with ^ and/or ends with $ is considered a regular expression.
* Everything else is considered a Cucumber expression.
*
* Thread-safe for reading after construction with configured registry.
*/
public final class ExpressionFactory {
/**
* Create factory with parameter type registry
* The registry should be fully configured before creating the factory
*
* @param parameterTypeRegistry - Registry containing parameter types
*/
public ExpressionFactory(ParameterTypeRegistry parameterTypeRegistry);
/**
* Create an Expression from a string
* Heuristics:
* - Strings starting with ^ or ending with $ become RegularExpression
* - All other strings become CucumberExpression
*
* Thread-safe operation
*
* @param expressionString - Expression string to parse
* @return Expression instance (CucumberExpression or RegularExpression)
* @throws CucumberExpressionException for invalid expression syntax
* @throws UndefinedParameterTypeException if expression references undefined parameter type
* @throws PatternSyntaxException if regular expression syntax is invalid
*/
public Expression createExpression(String expressionString);
}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);
// Cucumber Expression (default)
Expression cucumberExpr = factory.createExpression("I have {int} cucumbers");
Optional<List<Argument<?>>> match1 = cucumberExpr.match("I have 42 cucumbers");
// match1 contains [Argument(42)]
// Regular Expression (with anchors)
Expression regexExpr = factory.createExpression("^I have (\\d+) cucumbers$");
Optional<List<Argument<?>>> match2 = regexExpr.match("I have 42 cucumbers");
// match2 contains [Argument(Integer: 42)] - automatically typed based on built-in parameter types
// Empty string creates Cucumber Expression
Expression emptyExpr = factory.createExpression("");
Optional<List<Argument<?>>> match3 = emptyExpr.match("");
// match3 is present with empty argument list
// Expression with no parameters
Expression noParamsExpr = factory.createExpression("Click the button");
Optional<List<Argument<?>>> match4 = noParamsExpr.match("Click the button");
// match4 is present with empty argument listCucumber Expressions use a simpler, more readable syntax compared to regular expressions:
Built-in Parameter Types:
| Type | Syntax | Description | Java Type | Example Match |
|---|---|---|---|---|
| Integer | {int} | Matches integers | Integer | 42, -19 |
| Byte | {byte} | Matches integers, converts to byte | Byte | 127, -128 |
| Short | {short} | Matches integers, converts to short | Short | 1000, -1000 |
| Long | {long} | Matches integers, converts to long | Long | 1234567890 |
| Float | {float} | Matches floats | Float | 3.6, -9.2 |
| Double | {double} | Matches floats, converts to double | Double | 3.14159 |
| BigInteger | {biginteger} | Matches integers, converts to BigInteger | java.math.BigInteger | 999999999999 |
| BigDecimal | {bigdecimal} | Matches decimals, converts to BigDecimal | java.math.BigDecimal | 99.99 |
| Word | {word} | Matches words without whitespace | String | hello, user123 |
| String | {string} | Matches quoted strings | String | "hello world", 'foo' |
| Anonymous | {} | Matches anything | Object | Any text |
Number Format Details:
-?\d+ (optional minus, digits)Optional Text:
Use parentheses for optional text:
I have {int} cucumber(s)Matches both:
"I have 1 cucumber""I have 42 cucumbers"Important: The entire parenthesized section is optional, not just the contents.
Alternative Text:
Use forward slash for alternatives:
I have {int} cucumber(s) in my belly/stomachMatches both:
"I have 42 cucumbers in my belly""I have 42 cucumbers in my stomach"Nested Alternatives:
I have {int} cucumber(s) in my belly/stomach/tummyMatches:
"I have 42 cucumbers in my belly""I have 42 cucumbers in my stomach""I have 42 cucumbers in my tummy"Combining Optional and Alternative:
I (am/was) {int} years oldMatches:
"I am 25 years old""I was 25 years old""I 25 years old" (entire parenthesized section optional)Escaping Special Characters:
Escape special characters with backslash:
I have {int} \\{what} cucumber(s)Matches: "I have 42 {what} cucumbers"
Special characters that need escaping:
\ (backslash){ and } (braces)( and ) (parentheses)/ (forward slash, when used for alternatives)Regular expressions are created when the expression string starts with ^ or ends with $:
// These create RegularExpression:
Expression expr1 = factory.createExpression("^I have (\\d+) cucumbers$");
Expression expr2 = factory.createExpression("^I have (\\d+) cucumbers");
Expression expr3 = factory.createExpression("I have (\\d+) cucumbers$");
// This creates CucumberExpression:
Expression expr4 = factory.createExpression("I have {int} cucumbers");
// This creates CucumberExpression (anchors not at start/end):
Expression expr5 = factory.createExpression("I have {int}$ cucumbers");
// The $ is treated as literal text characterAutomatic Type Transformation for Regex:
When using regular expressions with capture groups, the library automatically transforms captured text based on matching built-in parameter types. This means that regex capture groups don't always return String values - they are typed based on the pattern.
ParameterTypeRegistry registry = new ParameterTypeRegistry(Locale.ENGLISH);
ExpressionFactory factory = new ExpressionFactory(registry);
// Regex with \d+ pattern matches the built-in {int} parameter type
Expression expr1 = factory.createExpression("^I have (\\d+) cucumbers$");
Optional<List<Argument<?>>> match1 = expr1.match("I have 42 cucumbers");
if (match1.isPresent()) {
Object value = match1.get().get(0).getValue();
// value is Integer 42, NOT String "42"
// The \d+ pattern matches the built-in {int} type, triggering automatic transformation
Integer count = (Integer) value;
}
// Custom parameter types also work with regex
ParameterType<String> colorType = new ParameterType<>(
"color",
"red|blue|green",
String.class,
(String s) -> s.toUpperCase()
);
registry.defineParameterType(colorType);
// Use in regular expression
Expression expr2 = factory.createExpression("^I have a (red|blue|green) ball$");
Optional<List<Argument<?>>> match2 = expr2.match("I have a red ball");
if (match2.isPresent()) {
String color = (String) match2.get().get(0).getValue();
// color = "RED" (transformed by the color parameter type)
}Important Notes on Regex Type Transformation:
\d+ for integers) are automatically transformedpreferForRegexpMatch=true is usedAmbiguity in Regex:
When multiple parameter types match the same regex pattern, use preferForRegexpMatch:
ParameterType<Integer> preferredType = new ParameterType<>(
"myint",
"\\d+",
Integer.class,
Integer::parseInt,
true, // useForSnippets
true // preferForRegexpMatch - marks as preferred
);Both CucumberExpression and RegularExpression provide:
Expression expr = factory.createExpression("I have {int} cucumbers");
// Get source string
String source = expr.getSource();
// "I have {int} cucumbers"
// Get compiled pattern
Pattern pattern = expr.getRegexp();
// Pattern representing the expression (internal regex representation)
// Pattern is useful for:
// - Debugging expression structure
// - Understanding generated regex
// - Integration with regex tools
System.out.println(pattern.pattern());
// Prints the internal regex pattern used for matchingNumber parameter types support localized formatting via the ParameterTypeRegistry's locale:
// English locale: comma as thousands separator, period as decimal
ParameterTypeRegistry englishRegistry = new ParameterTypeRegistry(Locale.ENGLISH);
ExpressionFactory englishFactory = new ExpressionFactory(englishRegistry);
Expression englishExpr = englishFactory.createExpression("I have {float} dollars");
Optional<List<Argument<?>>> match1 = englishExpr.match("I have 1,234.56 dollars");
// Parses as 1234.56f
// French locale: period as thousands separator, comma as decimal
ParameterTypeRegistry frenchRegistry = new ParameterTypeRegistry(Locale.FRENCH);
ExpressionFactory frenchFactory = new ExpressionFactory(frenchRegistry);
Expression frenchExpr = frenchFactory.createExpression("I have {float} euros");
Optional<List<Argument<?>>> match2 = frenchExpr.match("I have 1.234,56 euros");
// Parses as 1234.56f
// German locale
ParameterTypeRegistry germanRegistry = new ParameterTypeRegistry(Locale.GERMAN);
ExpressionFactory germanFactory = new ExpressionFactory(germanRegistry);
Expression germanExpr = germanFactory.createExpression("Preis ist {float} Euro");
Optional<List<Argument<?>>> match3 = germanExpr.match("Preis ist 1.234,56 Euro");
// Parses as 1234.56fKeyboard-Friendly Substitutions:
The library uses keyboard-friendly symbols for better accessibility:
- (ASCII 45), not Unicode minus sign (U+2212)., thousands separator is comma ,,, thousands separator is period .Scientific Notation:
Expression expr = factory.createExpression("Value is {double}");
Optional<List<Argument<?>>> match1 = expr.match("Value is 1.23e5");
// Parses as 123000.0
Optional<List<Argument<?>>> match2 = expr.match("Value is 1.23E-5");
// Parses as 0.0000123The match method accepts optional type hints to assist with type conversion:
/**
* Match text with optional type hints
*/
Optional<List<Argument<?>>> match(String text, Type... typeHints);Type hints help the transformer system when converting anonymous parameters or when multiple type conversions are possible.
Expression expr = factory.createExpression("I have {} items");
// Without type hint - uses default transformer
Optional<List<Argument<?>>> match1 = expr.match("I have 42 items");
// Returns Argument with value as String "42" (default behavior)
// With type hint - explicit conversion
Optional<List<Argument<?>>> match2 = expr.match("I have 42 items", Integer.class);
// Returns Argument with value as Integer 42 (if default transformer supports it)
// Multiple anonymous parameters with type hints
Expression expr2 = factory.createExpression("Transfer {} from {} to {}");
Optional<List<Argument<?>>> match3 = expr2.match(
"Transfer 100 from Alice to Bob",
Integer.class, String.class, String.class
);
// First argument: Integer 100
// Second argument: String "Alice"
// Third argument: String "Bob"Type Hint Ordering:
Type hints are matched to anonymous parameters in order of appearance:
Expression expr = factory.createExpression("Value {} and {} and {}");
Optional<List<Argument<?>>> match = expr.match(
"Value 42 and hello and 3.14",
Integer.class, String.class, Double.class
);
// Argument 0: Integer 42
// Argument 1: String "hello"
// Argument 2: Double 3.14CucumberExpressionException:
Thrown for various syntax errors in Cucumber Expressions.
Note: Anchors (^ and $) in Cucumber Expressions are treated as literal text characters rather than throwing exceptions. If you need regex anchors, use Regular Expression mode by placing anchors at the start (^) or end ($) of the expression:
// Anchors treated as literal text in Cucumber Expression
Expression expr1 = factory.createExpression("I have {int} cucumbers^");
// Matches text ending with literal "^" character: "I have 42 cucumbers^"
// To use regex anchors, create a Regular Expression
Expression expr2 = factory.createExpression("^I have {int} cucumbers$");
// Creates RegularExpression with proper anchors (must match entire string)UndefinedParameterTypeException:
Thrown when an expression references a parameter type that hasn't been registered:
try {
Expression expr = factory.createExpression("I have a {color} ball");
// Throws: UndefinedParameterTypeException if "color" not registered
} catch (UndefinedParameterTypeException e) {
String missingType = e.getUndefinedParameterTypeName();
// missingType = "color"
// Register the missing type and retry
ParameterType<Color> colorType = new ParameterType<>(
missingType,
"red|blue|green",
Color.class,
Color::new
);
registry.defineParameterType(colorType);
// Now create expression again
Expression expr = factory.createExpression("I have a {color} ball");
}PatternSyntaxException:
Thrown when regular expression syntax is invalid:
import java.util.regex.PatternSyntaxException;
try {
Expression expr = factory.createExpression("^I have (\\d+ cucumbers$");
// Missing closing parenthesis - throws PatternSyntaxException
} catch (PatternSyntaxException e) {
System.err.println("Regex syntax error: " + e.getMessage());
System.err.println("Error at index: " + e.getIndex());
System.err.println("Description: " + e.getDescription());
// Handle regex syntax error
}AmbiguousParameterTypeException:
Thrown when multiple parameter types match in a regular expression:
// Register two types with overlapping patterns
ParameterType<Integer> type1 = new ParameterType<>(
"count", "\\d+", Integer.class, Integer::parseInt
);
ParameterType<Long> type2 = new ParameterType<>(
"longcount", "\\d+", Long.class, Long::parseLong
);
registry.defineParameterType(type1);
registry.defineParameterType(type2);
try {
Expression expr = factory.createExpression("^Count: (\\d+)$");
// Throws: AmbiguousParameterTypeException
} catch (AmbiguousParameterTypeException e) {
// Get conflicting types
SortedSet<ParameterType<?>> conflicts = e.getParameterTypes();
// Get suggestions
List<GeneratedExpression> suggestions = e.getGeneratedExpressions();
// Handle ambiguity...
}Expression Creation:
Reuse Pattern:
// GOOD: Create once, reuse many times
Expression expr = factory.createExpression("I have {int} cucumbers");
for (String text : manyTexts) {
Optional<List<Argument<?>>> match = expr.match(text);
// Fast matching operation
}
// BAD: Creating expression in loop
for (String text : manyTexts) {
Expression expr = factory.createExpression("I have {int} cucumbers");
Optional<List<Argument<?>>> match = expr.match(text);
// Wastes time on repeated compilation
}Caching Expressions:
public class ExpressionCache {
private final ExpressionFactory factory;
private final Map<String, Expression> cache = new ConcurrentHashMap<>();
public ExpressionCache(ExpressionFactory factory) {
this.factory = factory;
}
public Expression getExpression(String expressionString) {
return cache.computeIfAbsent(expressionString, factory::createExpression);
}
}Matching Performance:
Empty Expressions:
Expression expr = factory.createExpression("");
Optional<List<Argument<?>>> match1 = expr.match("");
// match1.isPresent() = true, arguments = []
Optional<List<Argument<?>>> match2 = expr.match("any text");
// match2.isPresent() = falseWhitespace Handling:
// Whitespace in expression is significant
Expression expr1 = factory.createExpression("I have {int} cucumbers");
Optional<List<Argument<?>>> match1 = expr1.match("I have 42 cucumbers");
// Matches
Optional<List<Argument<?>>> match2 = expr1.match("I have 42 cucumbers");
// Does NOT match (extra spaces)
// To match variable whitespace, use regex
Expression expr2 = factory.createExpression("^I\\s+have\\s+(\\d+)\\s+cucumbers$");
Optional<List<Argument<?>>> match3 = expr2.match("I have 42 cucumbers");
// MatchesCase Sensitivity:
// Cucumber Expressions are case-sensitive by default
Expression expr = factory.createExpression("I have {int} cucumbers");
Optional<List<Argument<?>>> match1 = expr.match("I have 42 cucumbers");
// Matches
Optional<List<Argument<?>>> match2 = expr.match("I HAVE 42 CUCUMBERS");
// Does NOT match
// For case-insensitive matching, use regex with flag
Expression expr2 = factory.createExpression("^(?i)I have (\\d+) cucumbers$");
Optional<List<Argument<?>>> match3 = expr2.match("I HAVE 42 CUCUMBERS");
// MatchesUnicode Support:
// Unicode characters supported in expressions
Expression expr1 = factory.createExpression("I have {int} 🥒");
Optional<List<Argument<?>>> match1 = expr1.match("I have 42 🥒");
// Matches
// Unicode in parameter values
ParameterType<String> emojiType = new ParameterType<>(
"emoji",
"[\\p{So}]+",
String.class,
(String s) -> s
);
registry.defineParameterType(emojiType);
Expression expr2 = factory.createExpression("Emoji is {emoji}");
Optional<List<Argument<?>>> match2 = expr2.match("Emoji is 🥒🌶️🥕");
// MatchesSpecial Characters in Text:
// Expressions can contain special regex characters as literal text
Expression expr = factory.createExpression("Price is {float} $");
Optional<List<Argument<?>>> match1 = expr.match("Price is 99.99 $");
// Matches ($ is literal in Cucumber Expression)
// But $ at end triggers regex mode
Expression expr2 = factory.createExpression("Price is {float}$");
// This becomes a RegularExpression due to $ at endEmpty Parameter Matches:
// Anonymous parameters can match empty strings
Expression expr = factory.createExpression("Value is {}");
Optional<List<Argument<?>>> match = expr.match("Value is ");
// match.isPresent() = false (doesn't match trailing space alone)
// String parameter requires quotes
Expression expr2 = factory.createExpression("Value is {string}");
Optional<List<Argument<?>>> match2 = expr2.match("Value is \"\"");
// match2.isPresent() = true, value = "" (empty string)Combining Multiple Expressions:
public class MultiExpressionMatcher {
private final List<Expression> expressions;
public MultiExpressionMatcher(ExpressionFactory factory, String... patterns) {
this.expressions = Arrays.stream(patterns)
.map(factory::createExpression)
.collect(Collectors.toList());
}
public Optional<MatchResult> matchFirst(String text) {
for (int i = 0; i < expressions.size(); i++) {
Optional<List<Argument<?>>> match = expressions.get(i).match(text);
if (match.isPresent()) {
return Optional.of(new MatchResult(i, match.get()));
}
}
return Optional.empty();
}
}Dynamic Expression Construction:
public Expression buildExpression(String prefix, List<String> paramTypes, String suffix) {
StringBuilder sb = new StringBuilder(prefix);
for (int i = 0; i < paramTypes.size(); i++) {
if (i > 0) sb.append(" and ");
sb.append("{").append(paramTypes.get(i)).append("}");
}
sb.append(suffix);
return factory.createExpression(sb.toString());
}
// Usage
Expression expr = buildExpression(
"Transfer ",
Arrays.asList("int", "string", "string"),
" completed"
);
// Creates: "Transfer {int} and {string} and {string} completed"Expression Validation:
public boolean isValidExpression(String expressionString) {
try {
factory.createExpression(expressionString);
return true;
} catch (CucumberExpressionException | PatternSyntaxException e) {
return false;
}
}
public List<String> validateExpressions(List<String> expressions) {
List<String> errors = new ArrayList<>();
for (String expr : expressions) {
try {
factory.createExpression(expr);
} catch (UndefinedParameterTypeException e) {
errors.add(expr + ": undefined type " + e.getUndefinedParameterTypeName());
} catch (CucumberExpressionException | PatternSyntaxException e) {
errors.add(expr + ": " + e.getMessage());
}
}
return errors;
}