Groovy (Jenkins, Gradle and Spock) Module 2 – Core syntax: values, strings, operators and control flow
Regular expressions the Groovy way
Find, match and extract text with Groovy's ~, =~ and ==~, named groups, replaceAll with closures, Matchers in Jenkins, and patterns safe for hostile input.
What you will learn
- Match, find and extract text with ~/pattern/, =~ and ==~, and predict what each one returns
- Extract capture groups by number and by name, and state which runtimes accept each form
- Write replaceAll with a closure, and quote literal text with Pattern.quote and Matcher.quoteReplacement
- Explain why a Matcher must not stay in a Pipeline variable, and keep only the strings it found
- Protect a pattern that reads untrusted input with Groovy 6's RegexGuard, and catch a broken pattern at compile time
Before you start
What you’ll learn · Before you start
What you will learn
- Match, find and extract text with ~/pattern/, =~ and ==~, and predict what each one returns
- Extract capture groups by number and by name, and state which runtimes accept each form
- Write replaceAll with a closure, and quote literal text with Pattern.quote and Matcher.quoteReplacement
- Explain why a Matcher must not stay in a Pipeline variable, and keep only the strings it found
- Protect a pattern that reads untrusted input with Groovy 6's RegexGuard, and catch a broken pattern at compile time
Before you start
On this page
('order-42 shipped, order-7 delayed' =~ /order-(\d+)/).collect { it[1] } gives [42, 7] in one line. Groovy turns
Java’s regular-expression classes into three operators, and the same Matcher that makes this short is the object that
stops a Jenkins Pipeline with NotSerializableException when it is kept in a variable. Named groups work through the
operator only from Groovy 5, and Groovy 6 adds a guard for patterns that hostile input could keep busy for minutes.
You will find, match and extract with ~, =~ and ==~, read groups by number and by name, compute replacements with a
closure, keep a Matcher out of Pipeline state, and guard and check patterns on the runtimes that support it.
Three operators make a Pattern, a Matcher and a boolean
The documentation describes three operators. ~/order-(\d+)/ compiles a java.util.regex.Pattern, usually written as
a slashy string so backslashes need no escaping. text =~ /…/ gives a java.util.regex.Matcher, which finds matches
anywhere in the text, and its truth is whether it finds one. text ==~ /…/ gives a Boolean, and the whole text must
match. A capture group: A part of a regular expression in parentheses, such as (\d+), whose matched text the Matcher keeps. Group 0 is the whole match, so m[0][1] is group 1 of the first match., a part of the pattern in parentheses, keeps what it matched.
Step through the find operator on a line of log:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The operator. log =~ /order-(\d+)/ does not return true or false. It returns a java.util.regex.Matcher that pairs the text with the pattern.
- Its truth. Asked for its truth, as in if (matcher), the Matcher looks for a first match. order-42 matches, so it is true, and the session printed yes.
- The first match. matcher[0] is a list, the whole match first and then each group. Group 1, (\d+), captured 42.
- The second match. matcher[1] is the second match, order-7, so matcher[1][1] is 7. matcher.size() counts the matches, 2.
- Every match. matcher.collect { it[1] } visits the matches in order and keeps group 1 of each, which gave the ids [42, 7].
- The full-match operator. ==~ gives a Boolean instead and needs the whole text to match, so 'v6.2' ==~ /v\d+\.\d+/ was true and 'v6.2.1' false.
// The three regex operators, what each one gives, and how a Matcher hands out its matches.
def log = 'order-42 shipped, order-7 delayed'
def pattern = ~/order-(\d+)/
def matcher = log =~ /order-(\d+)/
println "~/order-(\\d+)/ ${pattern.getClass().name}"
println "log =~ /order-(\\d+)/ ${matcher.getClass().name}"
println "'v6.2' ==~ /v\\d+\\.\\d+/ ${'v6.2' ==~ /v\d+\.\d+/} (${('v6.2' ==~ /v\d+\.\d+/).getClass().simpleName})"
println "'v6.2.1' ==~ same ${'v6.2.1' ==~ /v\d+\.\d+/}"
println "matcher found any? ${matcher ? 'yes' : 'no'}"
println "matcher.size() ${matcher.size()}"
println "matcher[0] ${matcher[0]}"
println "matcher[1][1] ${matcher[1][1]}"
println "ids ${matcher.collect { it[1] }}"
println "log.findAll(pattern) ${log.findAll(pattern)}"
println "log.find(/\\d+/) ${log.find(/\d+/)}"
def none = 'no orders today' =~ /order-(\d+)/
try {
none[0]
} catch (IndexOutOfBoundsException e) {
println "no match, none[0] IndexOutOfBoundsException"
} from session import jenkins_jar, run, use
# The regex operators on Groovy 6.0.0, then on each older runtime, compared with 6.0.0's output.
run("""
groovy basics.groovy | tee on-6.0.0.txt
""")
for version in ("5.1.3", "4.0.33"):
use(version)
run(f"""
groovy basics.groovy > on-{version}.txt
diff on-6.0.0.txt on-{version}.txt && echo 'Groovy {version} printed the same lines'
""")
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain basics.groovy > on-2.4.21.txt
diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines'
""") Output
$ groovy basics.groovy | tee on-6.0.0.txt ~/order-(\d+)/ java.util.regex.Pattern log =~ /order-(\d+)/ java.util.regex.Matcher 'v6.2' ==~ /v\d+\.\d+/ true (Boolean) 'v6.2.1' ==~ same false matcher found any? yes matcher.size() 2 matcher[0] [order-42, 42] matcher[1][1] 7 ids [42, 7] log.findAll(pattern) [order-42, order-7] log.find(/\d+/) 42 no match, none[0] IndexOutOfBoundsException # The groovy command is Groovy 5.1.3 from here on. $ groovy basics.groovy > on-5.1.3.txt $ diff on-6.0.0.txt on-5.1.3.txt && echo 'Groovy 5.1.3 printed the same lines' Groovy 5.1.3 printed the same lines # The groovy command is Groovy 4.0.33 from here on. $ groovy basics.groovy > on-4.0.33.txt $ diff on-6.0.0.txt on-4.0.33.txt && echo 'Groovy 4.0.33 printed the same lines' Groovy 4.0.33 printed the same lines # groovy-all-2.4.21.jar in this folder is the Groovy jar that Jenkins bundles, from Maven Central. $ java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain basics.groovy > on-2.4.21.txt $ diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines' Groovy 2.4.21 printed the same lines
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 basics_session.py
matcher[0] is a list, the whole match and then group 1, and matcher[1][1] is the second match’s group, 7.
collect { it[1] } visits every match. findAll(pattern) returned the whole matches, and find(/\d+/) the first one,
42. 'v6.2.1' ==~ /v\d+\.\d+/ is false, because ==~ needs the whole text to match. Asking a Matcher with no match for
none[0] threw IndexOutOfBoundsException, so test the Matcher’s truth, or use find, which returns null, before
you index. All four runtimes printed the same lines. As a rule of thumb, use find for the first match, findAll for
every match as a string, and the Matcher itself when you need the groups of each match.
Key idea
=~ finds anywhere and gives a Matcher, whose truth is “found one”; ==~ asks whether the whole text matches; index a
Matcher only after you know it matched.
Groups come out by number everywhere, and by name from Groovy 5
A named group: A capture group with a name, written (?<name>…) in Java's pattern syntax. Matcher.group('name') reads it on every runtime; m[0].name reads it through Groovy's match operator from Groovy 5., (?<job>[A-Z]+), is part of Java’s pattern syntax, so Matcher.group('job')
works on every runtime. From Groovy 5, as its release notes describe, Groovy’s own indexing reads such a group by name
too: the list that m[0] gives answers to m[0].job and to m[0]['job']. Groovy 6 adds findGroups, which its
release notes describe as returning “the full match followed by each capture group as a List”, ready for multiple
assignment:
GROOVY (groups.groovy) · groups.groovy
// Groups by number, by name, and with Groovy 6's findGroups.
def line = 'BUILD-1042 failed after 37 s'
def m = line =~ /(?<job>[A-Z]+)-(?<number>\d+)/
println "by number: ${m[0][1]} ${m[0][2]}"
m.reset()
m.find()
println "Matcher.group('number'): ${m.group('number')}"
println "by name: ${m[0].job} ${m[0].number}" GROOVY (findGroups.groovy) · findGroups.groovy
def (all, major, minor, patch) = 'v6.2.1'.findGroups(/v(\d+)\.(\d+)\.(\d+)/)
println "findGroups: major $major, minor $minor, patch $patch" from session import jenkins_jar, run, use
# Groups by number and by name on the four runtimes (on 4.0.33 and 2.4.21 the lines printed before the error and the
# error's first line), then Groovy 6's findGroups, and on 5.1.3 only the first line of its error.
run("""
groovy groups.groovy
groovy findGroups.groovy
""")
use("5.1.3")
run("""
groovy groups.groovy
groovy findGroups.groovy 2>&1 | grep -m1 'Caught'
""")
use("4.0.33")
run("""
groovy groups.groovy 2>&1 | grep -v -e '^[[:space:]]*at ' -e '^groovy[.]lang[.]'
""")
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain groups.groovy 2>&1 | grep -v -e '^[[:space:]]*at ' -e '^groovy[.]lang[.]'
""") Output
$ groovy groups.groovy
by number: BUILD 1042
Matcher.group('number'): 1042
by name: BUILD 1042
$ groovy findGroups.groovy
findGroups: major 6, minor 2, patch 1
# The groovy command is Groovy 5.1.3 from here on.
$ groovy groups.groovy
by number: BUILD 1042
Matcher.group('number'): 1042
by name: BUILD 1042
$ groovy findGroups.groovy 2>&1 | grep -m1 'Caught'
Caught: groovy.lang.MissingMethodException: No signature of method: findGroups for class: java.lang.String is applicable for argument types: (String) values: [v(\d+)\.(\d+)\.(\d+)]
# The groovy command is Groovy 4.0.33 from here on.
$ groovy groups.groovy 2>&1 | grep -v -e '^[[:space:]]*at ' -e '^groovy[.]lang[.]'
by number: BUILD 1042
Matcher.group('number'): 1042
Caught: groovy.lang.MissingPropertyException: Exception evaluating property 'job' for java.util.ArrayList, Reason: groovy.lang.MissingPropertyException: No such property: job for class: java.lang.String
# groovy-all-2.4.21.jar in this folder is the Groovy jar that Jenkins bundles, from Maven Central.
$ java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain groups.groovy 2>&1 | grep -v -e '^[[:space:]]*at ' -e '^groovy[.]lang[.]'
by number: BUILD 1042
Matcher.group('number'): 1042
Caught: groovy.lang.MissingPropertyException: Exception evaluating property 'job' for java.util.ArrayList, Reason: groovy.lang.MissingPropertyException: No such property: job for class: java.lang.String
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 groups_session.py
Every runtime read the groups by number and with Matcher.group('number'). m[0].job worked on 6.0.0 and 5.1.3 and threw
MissingPropertyException on 4.0.33 and 2.4.21, where the list of a match has no names. findGroups exists only on
6.0.0: 5.1.3 answered No signature of method: findGroups. Note the two calls before group('number'): Java’s API
says that group throws IllegalStateException when no match has been attempted, so groups.groovy resets the
Matcher and calls find() first.
Reading a named group with m[0].name in Jenkins code
Jenkins’ Groovy 2.4.21 and Gradle’s 4.0.33 fail on m[0].job. Reset the Matcher and call find() and
group('job'), as groups.groovy does, or read the group by number, which every runtime accepts.
Checkpoint
Check: The ~, =~ and ==~ operators · Groups by number and by name
replaceAll takes a closure, and literal text needs quoting
replaceAll with a closure: A block of code in braces that can be stored in a variable, passed to a method and called later. In Groovy, braces after = always start a closure. More in “Groovy for Java developers: what changes” computes each replacement: the closure gets the whole match and
its groups, and returns the text to put in. Two kinds of literal text need care. A user’s text used as a pattern is a pattern, so . matches any
character and + repeats the one before it; Pattern.quote makes it match only itself. And in a replacement string, $1
means group 1, so a literal dollar needs Matcher.quoteReplacement:
import java.util.regex.Matcher
import java.util.regex.Pattern
// replaceAll with a closure, then quoting text that must be taken literally.
println "snake to camel: ${'build_api_server'.replaceAll(/_([a-z])/) { all, letter -> letter.toUpperCase() }}"
println "double each number: ${'retries=3 timeout=30'.replaceAll(/\d+/) { it.toInteger() * 2 }}"
def userText = 'v1.2+build'
println "as a pattern, v1x22build matches: ${'v1x22build' ==~ userText}"
println "quoted: ${Pattern.quote(userText)}"
println "quoted, v1x22build matches: ${'v1x22build' ==~ Pattern.quote(userText)}"
println "quoted, itself matches: ${userText ==~ Pattern.quote(userText)}"
try {
'price: 5'.replaceAll(/\d+/, '$10')
} catch (IndexOutOfBoundsException e) {
println "replacement '\$10': IndexOutOfBoundsException: ${e.message}"
}
println "quoteReplacement: ${'price: 5'.replaceAll(/\d+/, Matcher.quoteReplacement('$10'))}" from session import jenkins_jar, run, use
# Replacements and quoting on Groovy 6.0.0, then on each older runtime, compared with 6.0.0's output.
run("""
groovy replace.groovy | tee on-6.0.0.txt
""")
for version in ("5.1.3", "4.0.33"):
use(version)
run(f"""
groovy replace.groovy > on-{version}.txt
diff on-6.0.0.txt on-{version}.txt && echo 'Groovy {version} printed the same lines'
""")
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain replace.groovy > on-2.4.21.txt
diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines'
""") Output
$ groovy replace.groovy | tee on-6.0.0.txt snake to camel: buildApiServer double each number: retries=6 timeout=60 as a pattern, v1x22build matches: true quoted: \Qv1.2+build\E quoted, v1x22build matches: false quoted, itself matches: true replacement '$10': IndexOutOfBoundsException: No group 1 quoteReplacement: price: $10 # The groovy command is Groovy 5.1.3 from here on. $ groovy replace.groovy > on-5.1.3.txt $ diff on-6.0.0.txt on-5.1.3.txt && echo 'Groovy 5.1.3 printed the same lines' Groovy 5.1.3 printed the same lines # The groovy command is Groovy 4.0.33 from here on. $ groovy replace.groovy > on-4.0.33.txt $ diff on-6.0.0.txt on-4.0.33.txt && echo 'Groovy 4.0.33 printed the same lines' Groovy 4.0.33 printed the same lines # groovy-all-2.4.21.jar in this folder is the Groovy jar that Jenkins bundles, from Maven Central. $ java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain replace.groovy > on-2.4.21.txt $ diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines' Groovy 2.4.21 printed the same lines
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 replace_session.py
The closure turned build_api_server into buildApiServer and doubled each number. Used as a pattern, the text
v1.2+build matched v1x22build, because . took the x and 2+ took both 2s; Pattern.quote wrapped it in \Q…\E,
and then it matched only itself. The replacement '$10' failed with IndexOutOfBoundsException: No group 1, because
$1 names a group, and quoteReplacement made it a literal $10.
Lakh and crore grouping with one pattern
A lookahead can place the commas of Indian grouping: (\d)(?=(\d\d)+\d$) matches a digit followed by pairs of digits
and one last digit, and replaceAll with '$1,' puts a comma after it. 12345678 became 1,23,45,678 and 1000000000
became 1,00,00,00,000, while 999 stayed as it was.
// Indian digit grouping with one replaceAll: a comma after every digit followed by pairs of digits and one more digit.
['999', '1234', '100000', '12345678', '1000000000'].each { digits ->
println "${digits.padRight(12)}${digits.replaceAll(/(\d)(?=(\d\d)+\d$)/, '$1,')}"
} from session import jenkins_jar, run, use
# Digit grouping with one replaceAll on Groovy 6.0.0, then on each older runtime, compared with 6.0.0's output.
run("""
groovy grouping.groovy | tee on-6.0.0.txt
""")
for version in ("5.1.3", "4.0.33"):
use(version)
run(f"""
groovy grouping.groovy > on-{version}.txt
diff on-6.0.0.txt on-{version}.txt && echo 'Groovy {version} printed the same lines'
""")
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain grouping.groovy > on-2.4.21.txt
diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines'
""") Output
$ groovy grouping.groovy | tee on-6.0.0.txt 999 999 1234 1,234 100000 1,00,000 12345678 1,23,45,678 1000000000 1,00,00,00,000 # The groovy command is Groovy 5.1.3 from here on. $ groovy grouping.groovy > on-5.1.3.txt $ diff on-6.0.0.txt on-5.1.3.txt && echo 'Groovy 5.1.3 printed the same lines' Groovy 5.1.3 printed the same lines # The groovy command is Groovy 4.0.33 from here on. $ groovy grouping.groovy > on-4.0.33.txt $ diff on-6.0.0.txt on-4.0.33.txt && echo 'Groovy 4.0.33 printed the same lines' Groovy 4.0.33 printed the same lines # groovy-all-2.4.21.jar in this folder is the Groovy jar that Jenkins bundles, from Maven Central. $ java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain grouping.groovy > on-2.4.21.txt $ diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines' Groovy 2.4.21 printed the same lines
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 grouping_session.py
A Matcher cannot be saved, and Jenkins saves Pipeline variables
A running Jenkins Pipeline is saved as it goes. The Pipeline best practices page says that CPS transformation: The continuation-passing style rewrite that the Jenkins Pipeline plugin applies to Pipeline Groovy, so that Jenkins can save a running build's program state to disk and resume it after a restart. More in “Groovy in 2026: one language, three runtimes” “relies on being
able to serialize the pipeline’s current state”, that local variables are part of that state, and that a variable holding an
object that cannot be serialized causes a NotSerializableException. A Matcher is such an object. Step through what
Jenkins has to save, then watch Java’s own serialization refuse the Matcher without a Jenkins controller:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- A Matcher in a variable. The Pipeline reads a version from a step's output with =~ and keeps the Matcher in the local variable m.
- Jenkins saves the state. The Jenkins documentation says Pipeline's CPS relies on serializing the running state, and that local variables are part of that state, so m has to be saved with the rest.
- The Matcher cannot be saved. java.util.regex.Matcher is not Serializable, so saving fails with NotSerializableException, the error saving.groovy printed for the Matcher.
- Keep only the String. Read the value out at once, def version = (output =~ /…/)[0][1], so the variable holds a String. The String and a Pattern saved without trouble.
- Saved. Now every local can be saved, and the state reaches the controller's disk. For longer regex work, the documentation's tool is a method marked @NonCPS.
// What Java serialization does with each value: a Pipeline is saved only when every value it holds can be serialized.
def save(String name, Object value) {
try {
new ObjectOutputStream(new ByteArrayOutputStream()).withCloseable { it.writeObject(value) }
println "${name.padRight(22)}saved"
} catch (NotSerializableException e) {
println "${name.padRight(22)}NotSerializableException: ${e.message}"
}
}
def output = 'Built image app:6.2.1 in 41 s'
def matcher = output =~ /app:(\d+\.\d+\.\d+)/
save 'the Matcher', matcher
save 'the String it found', matcher[0][1]
save 'the Pattern', ~/app:(\d+\.\d+\.\d+)/ from session import jenkins_jar, run, use
# Saving a Matcher, a String and a Pattern on Groovy 6.0.0, then on each older runtime, compared with 6.0.0's output.
run("""
groovy saving.groovy | tee on-6.0.0.txt
""")
for version in ("5.1.3", "4.0.33"):
use(version)
run(f"""
groovy saving.groovy > on-{version}.txt
diff on-6.0.0.txt on-{version}.txt && echo 'Groovy {version} printed the same lines'
""")
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain saving.groovy > on-2.4.21.txt
diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines'
""") Output
$ groovy saving.groovy | tee on-6.0.0.txt the Matcher NotSerializableException: java.util.regex.Matcher the String it found saved the Pattern saved # The groovy command is Groovy 5.1.3 from here on. $ groovy saving.groovy > on-5.1.3.txt $ diff on-6.0.0.txt on-5.1.3.txt && echo 'Groovy 5.1.3 printed the same lines' Groovy 5.1.3 printed the same lines # The groovy command is Groovy 4.0.33 from here on. $ groovy saving.groovy > on-4.0.33.txt $ diff on-6.0.0.txt on-4.0.33.txt && echo 'Groovy 4.0.33 printed the same lines' Groovy 4.0.33 printed the same lines # groovy-all-2.4.21.jar in this folder is the Groovy jar that Jenkins bundles, from Maven Central. $ java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain saving.groovy > on-2.4.21.txt $ diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines' Groovy 2.4.21 printed the same lines
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 saving_session.py
The Matcher failed with NotSerializableException: java.util.regex.Matcher on every runtime, while the String it found
and the Pattern were saved. In a Jenkinsfile, read what you need out of the Matcher in the same expression, as in
def version = (output =~ /app:(\d+\.\d+\.\d+)/)[0][1], so the variable holds a String. For longer text processing, the
best practices page names @NonCPS, which disables the CPS transformation for one method; such a method may use
Matchers freely, but it may not call Pipeline steps such as sh.
Key idea
Keep strings, not Matchers, in Pipeline variables: extract the groups at once, or do the regex work in a @NonCPS
method that returns plain values.
Checkpoint
Check: Replacements and quoting · Matchers in Pipeline variables
Patterns that read untrusted text need a time limit and a compile-time check
Some patterns backtrack for an extremely long time on input built to defeat them, a problem known as
catastrophic backtracking: A regular expression trying so many ways to match a crafted input that one evaluation runs for minutes or longer. Java has no timeout for it; Groovy 6's RegexGuard stops it with a RegexTimeoutException.; the documentation calls the attack a regular
expression denial of service and notes that “Java’s regex engine has no native timeout”. Groovy 6 adds
groovy.util.regex.RegexGuard, which stops an evaluation with RegexTimeoutException once its time limit passes. A
broken pattern is a different risk: it compiles and fails only when it runs, unless the RegexChecker type-checking
extension, bundled since Groovy 4, checks it when the code compiles:
GROOVY (guard.groovy) · guard.groovy
import groovy.util.regex.RegexGuard
import groovy.util.regex.RegexTimeoutException
// A pattern that backtracks for a very long time on hostile input, run under Groovy 6's guard.
def hostile = '1,' * 40
long start = System.nanoTime()
try {
RegexGuard.guard(200) { hostile ==~ /(.*,){16}X/ }
println 'finished'
} catch (RegexTimeoutException e) {
long ms = (System.nanoTime() - start).intdiv(1_000_000)
println "stopped by the guard: ${e.getClass().simpleName}, in under a second: ${ms < 1000}"
}
println "an ordinary match under the guard: ${RegexGuard.guard(200) { 'v6.2.1' ==~ /v\d+\.\d+\.\d+/ }}" GROOVY (checked.groovy) · checked.groovy
import groovy.transform.TypeChecked
@TypeChecked(extensions = 'groovy.typecheckers.RegexChecker')
boolean isTag(String name) {
name ==~ /v(\d+\.\d+/
}
println isTag('v6.2') from session import jenkins_jar, run, use
# Groovy 6's RegexGuard, then the RegexChecker extension on each runtime. A file that does not compile shows only the
# line that names its error.
run("""
groovy guard.groovy
groovy checked.groovy 2>&1 | grep -o 'Bad regex: .*'
""")
use("5.1.3")
run("""
groovy guard.groovy 2>&1 | grep -m1 -o 'unable to resolve class .*'
groovy checked.groovy 2>&1 | grep -o 'Bad regex: .*'
""")
use("4.0.33")
run("""
groovy checked.groovy 2>&1 | grep -o 'Bad regex: .*'
""")
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain checked.groovy 2>&1 | grep -o "Static type checking extension .*"
""") Output
$ groovy guard.groovy stopped by the guard: RegexTimeoutException, in under a second: true an ordinary match under the guard: true $ groovy checked.groovy 2>&1 | grep -o 'Bad regex: .*' Bad regex: Unclosed group near index 10 # The groovy command is Groovy 5.1.3 from here on. $ groovy guard.groovy 2>&1 | grep -m1 -o 'unable to resolve class .*' unable to resolve class groovy.util.regex.RegexGuard $ groovy checked.groovy 2>&1 | grep -o 'Bad regex: .*' Bad regex: Unclosed group near index 10 # The groovy command is Groovy 4.0.33 from here on. $ groovy checked.groovy 2>&1 | grep -o 'Bad regex: .*' Bad regex: Unclosed group near index 10 # groovy-all-2.4.21.jar in this folder is the Groovy jar that Jenkins bundles, from Maven Central. $ java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain checked.groovy 2>&1 | grep -o "Static type checking extension .*" Static type checking extension 'groovy.typecheckers.RegexChecker' was not found on the classpath.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 guard_session.py
The guard stopped the documentation’s runaway pattern in under a second and let an ordinary match through; on 5.1.3 the
class does not exist. RegexChecker refused /v(\d+\.\d+/ with Bad regex: Unclosed group near index 10 on 6.0.0,
5.1.3 and 4.0.33, before anything ran. Jenkins’ 2.4.21 has no such extension, so there a broken pattern is found only by
a test that runs it.
- 2.4
The operators: ~, =~ and ==~ Jenkins
Pattern literals, the find and match operators, groups by number, findAll and replaceAll with a closure all work on Groovy 2.4.21, the Groovy of Jenkins.
- 4.0
A compile-time regex checker incubating
Groovy 4 bundled the optional groovy-typecheckers module with RegexChecker, which reports a broken pattern when the code compiles. On 2.4.21 the extension was not found.
- 5.0
Groups by name through the match operator new
Groovy 5 lets m[0].name and m[0]['name'] read a named group. On 4.0.33 and 2.4.21 the same line threw MissingPropertyException.
- 6.0
findGroups, RegexGuard, and a stable checker new
findGroups returns the match and its groups as a list, RegexGuard and @SafeRegex stop runaway patterns with a time limit, and RegexChecker leaves incubation and also checks String#matches.
The timeline has 4 events, from 2.4 to 6.0.
Patterns that read text you do not control
For log lines, parameters or request data, keep patterns simple and anchored, run them through RegexGuard or
@SafeRegex on Groovy 6, and limit the length of the input on older runtimes, where no guard exists.
Checkpoint
Check: Guarding and checking patterns
Practise: regular expressions
Practise the regex operators, groups, replacements, Matchers and guards. The session puts the idea you know least first and gives each answer its reason from the sessions:
Practice that adapts to you
Practise this lesson
15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): What is the difference between =~ and ==~ in Groovy?
text =~ /pattern/ is the find operator: it returns a java.util.regex.Matcher that searches for the pattern anywhere
in the text. Its truth is whether it finds a match, so if (line =~ /ERROR/) picks every line that contains ERROR, and
indexing it gives the matches and their groups, as matcher[0][1] for group 1 of the first match. text ==~ /pattern/
is the match operator: it returns a Boolean, and it is true only when the whole text matches, so 'v6.2' ==~ /v\d+\.\d+/
is true and 'v6.2.1' ==~ /v\d+\.\d+/ is false. Use ==~ to validate a whole value, such as a version or an id, and =~ to
find and extract parts of a longer text.
Two longer interview questions of this module start from this lesson:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A scripted Pipeline does def m = readFile('build.log') =~ /app:(\S+)/, then runs sh 'make deploy', and afterwards uses m[0][1]. The build fails with java.io.NotSerializableException: java.util.regex.Matcher. Explain the cause and give two fixes.
-
Fresher to mid
Write the approach for a helper that returns the highest release tag from a list such as v1.9.0, v1.10.0, v2.0.0-rc1 and release-1.2, ignoring pre-releases and names that are not versions. Which pattern do you use, how do you compare, and which cases do you test?
Key takeaways
~/…/compiles a Pattern,=~gives a Matcher that finds matches anywhere and is true when it finds one, and==~is a Boolean that needs the whole text to match.- Groups come out by number on every runtime and through
Matcher.group(name);m[0].nameneeds Groovy 5, andfindGroupsneeds Groovy 6. replaceAllwith a closure computes each replacement,Pattern.quotemakes user text match only itself, andMatcher.quoteReplacementkeeps a$literal.- A Matcher cannot be serialized, so a Jenkins Pipeline must keep only the strings it extracts, or do the regex work in a
@NonCPSmethod. - Groovy 6’s
RegexGuardstops runaway patterns with a time limit, andRegexChecker, from Groovy 4 on, rejects a broken pattern when the code compiles.
Terms in this lesson
- capture group
- A part of a regular expression in parentheses, such as (\d+), whose matched text the Matcher keeps. Group 0 is the whole match, so m[0][1] is group 1 of the first match.
- catastrophic backtracking
- A regular expression trying so many ways to match a crafted input that one evaluation runs for minutes or longer. Java has no timeout for it; Groovy 6's RegexGuard stops it with a RegexTimeoutException.
- named group
- A capture group with a name, written (?<name>…) in Java's pattern syntax. Matcher.group('name') reads it on every runtime; m[0].name reads it through Groovy's match operator from Groovy 5.
References
- Operators, regular expression operators (Groovy 6.0.0 documentation) (The Apache Software Foundation (Apache Groovy))
- Matcher methods of the Groovy Development Kit (The Apache Software Foundation (Apache Groovy))
- Release notes for Groovy 6.0 (findGroups, RegexGuard, RegexChecker) (The Apache Software Foundation (Apache Groovy))
- Release notes for Groovy 5.0 (named groups with the match operator) (The Apache Software Foundation (Apache Groovy))
- Release notes for Groovy 4.0 (built-in type checkers) (The Apache Software Foundation (Apache Groovy))
- java.util.regex.Pattern (Java SE 25 API) (Oracle)
- java.util.regex.Matcher (Java SE 25 API) (Oracle)
- Pipeline best practices (avoiding NotSerializableException, @NonCPS) (The Jenkins project (jenkins.io))
- Jenkins 2.580.1 bill of materials (bom/pom.xml) (The Jenkins project)
- Gradle compatibility matrix (embedded Groovy versions) (Gradle (docs.gradle.org))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress