Groovy Regular Expressions
last modified October 15, 2026
Regular expressions describe patterns in text, and Groovy wraps Java's
Pattern and Matcher in a set of so-called slashy and
dollar-slashy operators that make matching feel like part of the language. You
can test for a match, extract groups, replace text, and split strings without
ever calling a constructor. This tutorial covers the operators and the full
Matcher API with practical examples.
The Match and Find Operators
Two operators dominate Groovy regex work. The find operator =~
locates a match anywhere in the string and returns a Matcher, while
the match operator ==~ requires the entire string to match and
returns a boolean.
def text = "The year is 2026"
def matcher = text =~ /\d+/
println matcher instanceof java.util.regex.Matcher
println matcher.find()
println matcher.group()
println(text ==~ /.*\d+.*/)
println("2026" ==~ /\d+/)
The pattern is written with slashy syntax /\d+/, which avoids
escaping backslashes. =~ yields a matcher whose first match is
2026. ==~ returns true only when the whole string fits
the pattern, so it fails for the sentence but succeeds for the bare number.
$ groovy MatchOperators.groovy true true 2026 true true
Creating a Pattern
The pattern operator ~ compiles a Pattern object up
front. Reusing a compiled pattern is more efficient when the same expression is
applied many times.
import java.util.regex.Pattern
def pattern = ~/\bcat\b/
println pattern instanceof Pattern
println pattern.matcher("the cat sat").find()
println pattern.matcher("concatenate").find()
The word-boundary pattern matches cat in the first sentence but not
inside concatenate. Compiling once and calling
matcher repeatedly keeps the reusable pattern out of the per-call
overhead.
$ groovy PatternOperator.groovy true true false
Using Pattern and Matcher
For more control you can work with Pattern.compile and
matcher directly. Iterating the matcher visits every occurrence in
the input.
import java.util.regex.Pattern
def text = "Emails: alice@example.com and bob@test.org"
def pattern = Pattern.compile(/\w+@\w+\.\w+/)
def matcher = pattern.matcher(text)
while (matcher.find()) {
println matcher.group()
}
Each call to find advances to the next match, and
group() returns the matched text. This is the standard Java idiom,
fully available in Groovy when you need explicit control over the search.
$ groovy FindAll.groovy alice@example.com bob@test.org
Extracting Matches with findAll
Groovy's findAll collects matches into a list in a single
expression, which is often all you need.
def text = "Dates: 2024-01-15, 2025-06-30, 2026-12-01"
def dates = text.findAll(/\d{4}-\d{2}-\d{2}/)
println dates
def years = text.findAll(/\d{4}/) { it.toInteger() > 2024 }
println years
The first call returns every ISO date, and the second filters matches with a
closure, keeping only years after 2024. Combining findAll with a
predicate avoids boring loops over the matcher.
$ groovy FindAllList.groovy [2024-01-15, 2025-06-30, 2026-12-01] [false, true, true]
Capturing Groups
Parentheses create groups, and numerical indices read them from the matcher. Group zero is the whole match, group one is the first parenthesized part, and so on.
def text = "2026-10-15"
def matcher = text =~ /(\d{4})-(\d{2})-(\d{2})/
if (matcher.matches()) {
println "year: ${matcher.group(1)}"
println "month: ${matcher.group(2)}"
println "day: ${matcher.group(3)}"
println matcher.groupCount()
}
The pattern splits the date into year, month, and day. groupCount()
returns three, and each numbered group yields its substring. This is the
foundation of parsing structured text.
$ groovy Groups.groovy year: 2026 month: 10 day: 15 3
Named Groups
Named groups with the (?<name>...) syntax make patterns
self-documenting, and Groovy exposes them as matcher.group('name').
def text = "Alice: 30, Bob: 25"
def pattern = /(?<name>\w+): (?<age>\d+)/
def matcher = text =~ pattern
while (matcher.find()) {
println "${matcher.group('name')} is ${matcher.group('age')}"
}
Each match exposes the whole text through m[0] and the age through
the named group. Named groups reduce off-by-one errors when the pattern has many
captures.
$ groovy NamedGroups.groovy Alice is 30 Bob is 25
Replacing Text
replaceAll and replaceFirst accept a regex pattern and
a replacement string. A closure replacement can compute the substitute per
match.
def text = "phone: 555-1234, office: 555-5678"
println text.replaceAll(/\d{3}-\d{4}/, "XXX-XXXX")
println text.replaceFirst(/555-(\d{4})/) { m -> "(555) ${m[1]}" }
The first line masks every number, and the second reformats only the first match using a closure that captures the last four digits. Closure replacements keep logic adjacent to the pattern.
$ groovy Replacement.groovy phone: XXX-XXXX, office: XXX-XXXX phone: (555) 1234, office: 555-5678
Splitting Strings
The split method on strings accepts a regex, which is handy for
separating fields that a single delimiter cannot describe.
def record = "apple, banana; cherry"
def parts = record.split(/[,;]\s*/)
parts.each { println it }
The pattern matches a comma or semicolon followed by optional whitespace, so the result is cleanly trimmed. A well-chosen delimiter pattern saves a separate cleaning pass.
$ groovy Splitting.groovy apple banana cherry
Case-Insensitive and Multiline Flags
Inline flags such as (?i) for case insensitivity and
(?m) for multiline anchors tune the engine without extra arguments.
def text = "Groovy is great, GROOVY is fast"
def matches = text.findAll(/(?i)groovy/)
println matches
println("end" ==~ /(?i)END/)
def lines = "first line\nsecond line"
println lines.findAll(/(?m)^\w+/)
The (?i) flag lets the pattern match both capitalized and lowercase
forms, returning two hits. The (?m) flag makes ^ match
at the start of every line rather than only at the start of the input, so the
last example returns the first word of each line.
$ groovy Flags.groovy [Groovy, GROOVY] true [first, second]
Validating Input
A common use of regex is validating that user input conforms to a known format.
The full-match operator ==~ is the right tool because it forbids
partial matches.
def email = ~/\A[\w.+-]+@[\w-]+\.[\w.]+\z/
def valid = ["alice@example.com", "bad@@mail", "bob@test.org"]
valid.each { address ->
println "$address -> ${email.matcher(address).matches()}"
}
The anchored pattern requires the whole string to be a well-formed address, so only the first and third entries pass. Anchors prevent a partial match from slipping through validation.
$ groovy Validation.groovy alice@example.com -> true bad@@mail -> false bob@test.org -> true
Best Practices for Regular Expressions
- Compile Once, Match Many: Reuse a
Patterninstead of recompiling in a loop. - Prefer Slashy Strings: Use
/.../to keep backslashes readable and avoid double escaping. - Anchor for Validation: Use
==~or explicit anchors when the entire input must match. - Name Complex Groups: Named groups document intent and reduce indexing mistakes.
- Test Against Edge Cases: Empty strings, unicode, and long inputs often reveal brittle patterns.
Source
Groovy Regular Expression Operators
In this tutorial, we covered Groovy regular expressions through the
=~, ==~, and ~ operators, the
Pattern and Matcher API, capturing and named groups,
replacement, splitting, inline flags, and validation. Groovy's operators turn
common regex tasks into concise, readable expressions.
Author
List all Groovy tutorials.