Groovy (Jenkins, Gradle and Spock) Module 2 – Core syntax: values, strings, operators and control flow
Groovy truth, null safety and the Elvis operator
What if (value) decides in Groovy, why 'false' and File objects surprise, and null-safe code with ?., ?:, ?= and ?[], with the runtimes that accept each.
What you will learn
- Predict how null, numbers, strings, collections, matchers and files convert to boolean on each runtime
- Implement asBoolean() so that a class of your own has the truth you choose
- Convert string flags and numbers with toBoolean(), isInteger() and toInteger() instead of relying on truth
- Write null-safe code with ?., ?:, ?= and ?[], and state which runtimes accept each
Before you start
What you’ll learn · Before you start
What you will learn
- Predict how null, numbers, strings, collections, matchers and files convert to boolean on each runtime
- Implement asBoolean() so that a class of your own has the truth you choose
- Convert string flags and numbers with toBoolean(), isInteger() and toInteger() instead of relying on truth
- Write null-safe code with ?., ?:, ?= and ?[], and state which runtimes accept each
Before you start
On this page
if ('false') runs its block in Groovy, because a non-empty String is true, and retries ?: 3 turns a deliberate
retries = 0 into 3. Both follow from one rule set, the Groovy truth: every value, not only a boolean, can decide an
if, and the rules for numbers, text, collections and files are not Java’s. Groovy 5 even changed the truth of a
File, so the same if (report) decides differently on Jenkins’ Groovy 2.4.21 and on Groovy 6.0.0.
You will predict the truth of any value on each runtime, give a class of your own a truth of its choosing, read
'false' and '0' from the environment correctly, and use ?., ?:, ?= and ?[] where each is safe.
Empty, zero and null are false; everything else is true
The documentation lists the rules of the Groovy truth: Groovy's rules for turning any value into true or false in a condition: null, zero and empty strings, collections, maps and arrays are false, and every other value is true unless its class defines otherwise. type by type: null is false, a number is false when it is zero, and a String, a
collection, a map or an array is false when it is empty. A regular-expression matcher is true when it finds a match,
and any other object is true because it is not null. Each rule is an asBoolean(): The method that Groovy calls to decide the truth of a value in if, while, ! and ?:. The Groovy Development Kit defines it for strings, numbers, collections, maps, matchers and, from Groovy 5, files; a class can define its own. method that the
Groovy Development Kit gives a type. Step through six values and watch which question decides each one:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- null. The first question is whether the value is null. null is false, as the first line of the table shows, and no method is called.
- 'false'. 'false' is not null and not a Boolean. The GDK gives every CharSequence an asBoolean() that is false only for length 0, so the five letters of 'false' make it true.
- [0]. A list's asBoolean() asks only whether it is empty. [0] holds one item, so it is true, whatever the item is; [] is false.
- 0.0. A number is false when its double value is 0, the GDK says, so 0 and 0.0 are false and -1 is true.
- A missing File. From Groovy 5 the GDK gives File an asBoolean() that asks whether the file exists, so new File('nope.txt') is false on 5.1.3 and 6.0.0.
- The same File, older. Groovy 4.0.33 and 2.4.21 have no such method for File. The value falls through to the last question, a non-null object, and is true.
The program prints the truth of sixteen values on Groovy 6.0.0. The session then runs it on each older runtime, and
diff prints only the lines that changed:
// The Groovy truth of common values: what if (value) decides for each one.
def values = [
'null' : null,
'0' : 0,
'0.0' : 0.0,
'-1' : -1,
"''" : '',
"' '" : ' ',
"'false'" : 'false',
"'0'" : '0',
'[]' : [],
'[0]' : [0],
'[:]' : [:],
'new int[0]' : new int[0],
"'a' =~ /b/" : ('a' =~ /b/),
"'a' =~ /a/" : ('a' =~ /a/),
'Double.NaN' : Double.NaN,
"new File('nope.txt')" : new File('nope.txt'),
]
values.each { code, value -> println "${code.padRight(22)}${value ? 'true' : 'false'}" } from session import jenkins_jar, run, use
# The truth of sixteen values on Groovy 6.0.0, then on each older runtime: diff prints only the lines that differ from 6.0.0's.
run("""
groovy truth.groovy | tee on-6.0.0.txt
""")
for version in ("5.1.3", "4.0.33"):
use(version)
run(f"""
groovy truth.groovy > on-{version}.txt
diff on-6.0.0.txt on-{version}.txt && echo 'Groovy {version} printed the same lines' || echo 'Groovy {version} differs in the lines above'
""")
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain truth.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' || echo 'Groovy 2.4.21 differs in the lines above'
""") Output
$ groovy truth.groovy | tee on-6.0.0.txt
null false
0 false
0.0 false
-1 true
'' false
' ' true
'false' true
'0' true
[] false
[0] true
[:] false
new int[0] false
'a' =~ /b/ false
'a' =~ /a/ true
Double.NaN false
new File('nope.txt') false
# The groovy command is Groovy 5.1.3 from here on.
$ groovy truth.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' || echo 'Groovy 5.1.3 differs in the lines above'
Groovy 5.1.3 printed the same lines
# The groovy command is Groovy 4.0.33 from here on.
$ groovy truth.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' || echo 'Groovy 4.0.33 differs in the lines above'
16c16
< new File('nope.txt') false
---
> new File('nope.txt') true
Groovy 4.0.33 differs in the lines above
# 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 truth.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' || echo 'Groovy 2.4.21 differs in the lines above'
15,16c15,16
< Double.NaN false
< new File('nope.txt') false
---
> Double.NaN true
> new File('nope.txt') true
Groovy 2.4.21 differs in the lines above
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 truth_session.py
Fourteen lines were the same everywhere. 'false' and '0' are true, because they are non-empty text, and [0] is
true, because the list is not empty, whatever it holds. Two lines changed. A missing File is false on 6.0.0 and
5.1.3 but true on 4.0.33 and 2.4.21, and Double.NaN is false on 4.0.33 and later but true on Jenkins’ 2.4.21.
The GDK’s rule for numbers is false only for zero, which makes NaN true; it lists a separate truth for Double, false
for zero and for NaN, only since Groovy 2.6.0. Code that tests a computed double with if can therefore take
different paths on the two runtimes.
Key idea
null, zero and empty are false, and every other value is true, including the text 'false', the text '0' and the
list [0].
asBoolean() gives a class its truth, and File’s changed in Groovy 5
A class decides its own truth by defining boolean asBoolean(), as the documentation shows. A basket can then be false
while it is empty, without callers having to know about its list. The second program tests a File before and after
it exists; the Groovy 5 release notes say that a File or Path “now coerce to a boolean based on whether the file or
path exists”. Before Groovy 5, any non-null File was true:
GROOVY (basket.groovy) · basket.groovy
class Basket {
List<String> items = []
boolean asBoolean() { !items.isEmpty() }
}
def basket = new Basket()
println "an empty basket: ${basket ? 'true' : 'false'}"
basket.items << 'tea' << 'cake'
println "two items: ${basket ? 'true' : 'false'}"
println "!basket: ${!basket}" GROOVY (fileTruth.groovy) · fileTruth.groovy
def report = new File('report.txt')
println "before writing: ${report ? 'true' : 'false'}"
report.text = 'done'
println "after writing: ${report ? 'true' : 'false'}" from session import jenkins_jar, run, use
# A class with asBoolean(), then a File before and after it exists, on the four runtimes.
run("""
groovy basket.groovy
rm -f report.txt
groovy fileTruth.groovy
""")
for version in ("5.1.3", "4.0.33"):
use(version)
run("""
rm -f report.txt
groovy fileTruth.groovy
""")
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain basket.groovy
rm -f report.txt
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain fileTruth.groovy
""") Output
$ groovy basket.groovy an empty basket: false two items: true !basket: false $ rm -f report.txt $ groovy fileTruth.groovy before writing: false after writing: true # The groovy command is Groovy 5.1.3 from here on. $ rm -f report.txt $ groovy fileTruth.groovy before writing: false after writing: true # The groovy command is Groovy 4.0.33 from here on. $ rm -f report.txt $ groovy fileTruth.groovy before writing: true after writing: true # 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 basket.groovy an empty basket: false two items: true !basket: false $ rm -f report.txt $ java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain fileTruth.groovy before writing: true after writing: true
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 custom_session.py
The empty basket was false, the basket with two items true, and !basket called the same method. For the file, 6.0.0
and 5.1.3 printed false before it was written and true after, while 4.0.33 and 2.4.21 printed true both times:
there, if (report) only says that the variable is not null. The GDK’s documentation of File.asBoolean() names the
way back for code that is not ready: the system property groovy.truth.file.exists.enabled set to false.
Testing that a File variable is set with if (file)
On Groovy 5 and 6, if (output) asks whether the file exists, so a step that creates the file later is skipped. Write
if (output != null) for “the variable is set” and if (output.exists()) for “the file is there”; both mean the same on
every runtime.
Checkpoint
Check: The Groovy truth of common values · asBoolean() and the truth of a File
Text that says false is still true
Environment variables and command-line arguments always arrive as text, and so does a setting read from a properties
file or a form. The text 'false' is five characters long, so it is true. To read text as a flag or a number, convert it
first: the GDK’s toBoolean() returns true when the trimmed text is "true", "y" or "1", ignoring case, and
false for anything else, and isInteger() says whether toInteger() will work:
def skip = 'false'
println "if ('false') ${skip ? 'runs' : 'does not run'}"
['true', 'TRUE', 'y', '1', 'yes', 'false', '0', ''].each {
println "'${it}'.toBoolean()".padRight(23) + it.toBoolean()
}
println "'12'.isInteger() ${'12'.isInteger()}"
println "'1.5'.isInteger() ${'1.5'.isInteger()}"
println "'10' > '9' ${'10' > '9'}"
println "'10'.toInteger() > 9 ${'10'.toInteger() > 9}"
try {
println "'10' > 9 ${'10' > 9}"
} catch (e) {
println "'10' > 9 ${e.getClass().name}"
} from session import jenkins_jar, run, use
# String flags and comparisons on Groovy 6.0.0, then on each older runtime: diff prints only the lines that differ from 6.0.0's.
run("""
groovy flags.groovy | tee on-6.0.0.txt
""")
for version in ("5.1.3", "4.0.33"):
use(version)
run(f"""
groovy flags.groovy > on-{version}.txt
diff on-6.0.0.txt on-{version}.txt && echo 'Groovy {version} printed the same lines' || echo 'Groovy {version} differs in the lines above'
""")
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain flags.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' || echo 'Groovy 2.4.21 differs in the lines above'
""") Output
$ groovy flags.groovy | tee on-6.0.0.txt
if ('false') runs
'true'.toBoolean() true
'TRUE'.toBoolean() true
'y'.toBoolean() true
'1'.toBoolean() true
'yes'.toBoolean() false
'false'.toBoolean() false
'0'.toBoolean() false
''.toBoolean() false
'12'.isInteger() true
'1.5'.isInteger() false
'10' > '9' false
'10'.toInteger() > 9 true
'10' > 9 java.lang.IllegalArgumentException
# The groovy command is Groovy 5.1.3 from here on.
$ groovy flags.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' || echo 'Groovy 5.1.3 differs in the lines above'
Groovy 5.1.3 printed the same lines
# The groovy command is Groovy 4.0.33 from here on.
$ groovy flags.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' || echo 'Groovy 4.0.33 differs in the lines above'
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 flags.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' || echo 'Groovy 2.4.21 differs in the lines above'
14c14
< '10' > 9 java.lang.IllegalArgumentException
---
> '10' > 9 java.lang.ClassCastException
Groovy 2.4.21 differs in the lines above
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 flags_session.py
if ('false') ran its block. toBoolean() accepted 'y' and '1' but not 'yes', so a flag documented as “yes or
no” needs its own check. Comparing two strings compares them character by character, so '10' > '9' is false, because
'1' comes before '9'; convert first, and '10'.toInteger() > 9 is true. Comparing a String with a number failed
on every runtime, with a different exception: IllegalArgumentException on 6.0.0, 5.1.3 and 4.0.33, and
ClassCastException on 2.4.21, so a test that expects one class passes on one runtime and fails on the other.
if (env.SKIP_TESTS) for a flag
An environment value is text, so if (env.SKIP_TESTS) is true for 'false' and for '0'. Write
if (env.SKIP_TESTS?.toBoolean()), which is false for a missing value, for 'false' and for '0'.
Checkpoint
Check: Flags and numbers that arrive as text
The safe navigation operator returns null instead of throwing
Configuration is full of values that may be missing: a map without the key proxy, a list that was never set. Reading
config.proxy.host then throws a NullPointerException, because config.proxy is null. The
safe navigation operator: The operator ?. in Groovy: a?.b reads b only when a is not null, and gives null otherwise instead of throwing a NullPointerException., ?., checks the value before the dot: if it is
null, that step gives null instead of reading anything. Step through both reads:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The map. config is a map with the keys retry and name. It has no key proxy.
- config.proxy. Reading a key that a map does not have gives null, so config.proxy is null. Both reads get this far.
- With ?.. The safe navigation operator checks the value before it: proxy is null, so ?.host returns null without reading anything, and the session printed null.
- Without ?.. A plain .host asks null for a property, and Groovy throws NullPointerException with the message Cannot get property 'host' on null object.
- One ?. per step. Each ?. guards only the dot it replaces. config?.proxy.host would still throw, because the null appears after the safe dot, at .host.
def config = [retry: [count: 0], name: '']
println "config.proxy?.host ${config.proxy?.host}"
println "config.retry?.count ${config.retry?.count}"
try {
println config.proxy.host
} catch (NullPointerException e) {
println "config.proxy.host NullPointerException: ${e.message}"
}
List<Map> hosts = null
println "hosts*.name ${hosts*.name}"
println "[null, [name: 'db1']]*.name ${[null, [name: 'db1']]*.name}" from session import jenkins_jar, run, use
# Safe navigation and the spread operator on Groovy 6.0.0, then on each older runtime: diff prints only the lines that differ from 6.0.0's.
run("""
groovy safe.groovy | tee on-6.0.0.txt
""")
for version in ("5.1.3", "4.0.33"):
use(version)
run(f"""
groovy safe.groovy > on-{version}.txt
diff on-6.0.0.txt on-{version}.txt && echo 'Groovy {version} printed the same lines' || echo 'Groovy {version} differs in the lines above'
""")
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain safe.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' || echo 'Groovy 2.4.21 differs in the lines above'
""") Output
$ groovy safe.groovy | tee on-6.0.0.txt config.proxy?.host null config.retry?.count 0 config.proxy.host NullPointerException: Cannot get property 'host' on null object hosts*.name null [null, [name: 'db1']]*.name [null, db1] # The groovy command is Groovy 5.1.3 from here on. $ groovy safe.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' || echo 'Groovy 5.1.3 differs in the lines above' Groovy 5.1.3 printed the same lines # The groovy command is Groovy 4.0.33 from here on. $ groovy safe.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' || echo 'Groovy 4.0.33 differs in the lines above' 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 safe.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' || echo 'Groovy 2.4.21 differs in the lines above' 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 safe_session.py
config.proxy?.host printed null, while config.proxy.host threw Cannot get property 'host' on null object.
config.retry?.count printed 0: ?. stops only at null, never at a zero or an empty value. The spread operator is
null-safe in the same way, as the documentation says: hosts*.name on a null list gave null, and a list holding a
null gave [null, db1]. All four runtimes printed the same lines, so ?. and *. are safe in a Jenkinsfile too.
Key idea
?. stops the expression at a null and gives null; it guards only the dot it replaces, and it never treats zero or
empty values as missing.
The Elvis operators treat 0, empty text and false as missing
The Elvis operator: The operator ?: in Groovy: a ?: b gives a when a is true by the Groovy truth and b otherwise, so 0, empty text and false are replaced as well as null., value ?: fallback, gives the value when it is true and the fallback otherwise; the documentation
describes it as a shortening of the ternary operator for “returning a ‘sensible default’ value”. Because it uses Groovy
truth, a valid 0, an empty text or false count as missing too. Groovy 3 added the Elvis assignment x ?= fallback, which
assigns only when x is false, and the safe index list?[0]:
GROOVY (elvis.groovy) · elvis.groovy
def config = [retries: 0, name: '', timeout: null]
println "config.retries ?: 3 ${config.retries ?: 3}"
println "config.name ?: 'build' ${config.name ?: 'build'}"
println "config.timeout ?: 30 ${config.timeout ?: 30}"
println "retries, with a null check ${config.retries != null ? config.retries : 3}" GROOVY (elvisAssign.groovy) · elvisAssign.groovy
def timeout = null
timeout ?= 30
def retries = 0
retries ?= 3
def hosts = null
println "timeout ?= 30 gives $timeout, retries ?= 3 gives $retries, hosts?[0] gives ${hosts?[0]}" from session import jenkins_jar, run, use
# The Elvis operator on Groovy 6.0.0, then on each older runtime: diff prints only the lines that differ from 6.0.0's.
run("""
groovy elvis.groovy | tee on-6.0.0.txt
""")
for version in ("5.1.3", "4.0.33"):
use(version)
run(f"""
groovy elvis.groovy > on-{version}.txt
diff on-6.0.0.txt on-{version}.txt && echo 'Groovy {version} printed the same lines' || echo 'Groovy {version} differs in the lines above'
""")
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain elvis.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' || echo 'Groovy 2.4.21 differs in the lines above'
""") Output
$ groovy elvis.groovy | tee on-6.0.0.txt config.retries ?: 3 3 config.name ?: 'build' build config.timeout ?: 30 30 retries, with a null check 0 # The groovy command is Groovy 5.1.3 from here on. $ groovy elvis.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' || echo 'Groovy 5.1.3 differs in the lines above' Groovy 5.1.3 printed the same lines # The groovy command is Groovy 4.0.33 from here on. $ groovy elvis.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' || echo 'Groovy 4.0.33 differs in the lines above' 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 elvis.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' || echo 'Groovy 2.4.21 differs in the lines above' 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 elvis_session.py
from session import jenkins_jar, run, use
# ?= and ?[] on Groovy 6.0.0, 5.1.3 and 4.0.33, then on Jenkins' 2.4.21, where only the error's first line is shown.
run("""
groovy elvisAssign.groovy
""")
for version in ("5.1.3", "4.0.33"):
use(version)
run("""
groovy elvisAssign.groovy
""")
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain elvisAssign.groovy 2>&1 | grep 'elvisAssign.groovy: 2:'
""") Output
$ groovy elvisAssign.groovy timeout ?= 30 gives 30, retries ?= 3 gives 3, hosts?[0] gives null # The groovy command is Groovy 5.1.3 from here on. $ groovy elvisAssign.groovy timeout ?= 30 gives 30, retries ?= 3 gives 3, hosts?[0] gives null # The groovy command is Groovy 4.0.33 from here on. $ groovy elvisAssign.groovy timeout ?= 30 gives 30, retries ?= 3 gives 3, hosts?[0] gives null # 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 elvisAssign.groovy 2>&1 | grep 'elvisAssign.groovy: 2:' elvisAssign.groovy: 2: unexpected token: ? @ line 2, column 9.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 assign_session.py
config.retries ?: 3 gave 3 although the setting was 0, and config.name ?: 'build' replaced an empty name, while
config.timeout ?: 30 filled in a missing value as intended. The explicit check, config.retries != null ? config.retries : 3, kept the 0. retries ?= 3 replaced a 0 in the same way, and on Groovy 2.4.21 the file did not even
compile: unexpected token: ? at line 2, the first ?=. Choose the form for your setting:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Form | Keeps 0 and empty text | Compiles on Groovy 2.4.21 | Short to write |
|---|---|---|---|
| value ?: default Replaces: null, 0, empty text, false | Poor retries 0 became 3 in the session | Good gave the same results on 2.4.21 | Good one operator |
| value ?= default Replaces: the same, in place | Poor retries ?= 3 replaced 0 with 3 | Poor unexpected token: ? on 2.4.21 | Good one operator |
| value != null ? value : default Replaces: only null | Good kept retries at 0 | Good plain Groovy that every runtime reads | Mixed names the value twice |
| if (value == null) value = default Replaces: only null | Good replaces only a missing value | Good plain Groovy that every runtime reads | Mixed a statement of its own |
How the answers decide
- “Can the setting hold 0, an empty text or false as a real value?” Yes, 0 or empty can be meant: rules out value ?: default and value ?= default. Both treat 0, empty text and false as missing: retries 0 became 3 with ?: and with ?=.
- “Must the code also run in a Jenkinsfile or shared library?” Yes, on Groovy 2.4.21: rules out value ?= default. ?= arrived in Groovy 3; Jenkins compiles Pipeline code with Groovy 2.4.21, which stops at it.
- “Can the setting hold 0, an empty text or false as a real value?” No, only a missing value and “Must the code also run in a Jenkinsfile or shared library?” No, Groovy 3 or later only: adds a caveat to value != null ? value : default and if (value == null) value = default. They work, but ?: or ?= says the same in fewer characters when no valid value is falsy.
In short: ?: for text and objects where empty means missing; a null check where 0, '' or false are real values;
and no ?= or ?[] in code that Jenkins runs.
Checkpoint
Check: Safe navigation and the spread operator · The Elvis operators and their zero trap
Practise: true, false and null
Test what you can now predict about true, false and null. The practice opens where your earlier answers went wrong, and each explanation points back to a recorded run:
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 does the Elvis operator do in Groovy, and when is it the wrong choice?
value ?: fallback gives value when it is true by the Groovy truth and fallback otherwise, so it is a short form of
value ? value : fallback that names the value only once. It suits text and objects where empty or null both mean
“not set”, such as name ?: 'build'. It is the wrong choice when 0, an empty text or false is a valid value: with
retries = 0, retries ?: 3 gives 3, which silently turns “no retries” into three. There, write
retries != null ? retries : 3. The Elvis assignment retries ?= 3 has the same trap, and it also needs Groovy 3 or
later, so it fails to compile in a Jenkinsfile, which runs on Groovy 2.4.21.
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.
-
Fresher to mid
A pipeline step runs if (env.SKIP_TESTS) { echo 'Skipping tests' } else { runTests() }. A job sets SKIP_TESTS to the text false, and the tests are still skipped. Explain why, fix the check, and say how you would make every flag of the shared library behave the same way.
-
Mid to senior
A deploy helper reads int retries = config.retries ?: 3. A team sets retries to 0 to stop retrying a step that is not safe to repeat, and the step still runs three times. Explain the cause, write a fix that also compiles in a Jenkinsfile, and say how you would test it.
Key takeaways
null, zero and empty values are false and everything else is true, so'false','0'and[0]are true;Double.NaNis false on 4.0.33 and later but true on Jenkins’ 2.4.21.- A class defines its truth with
asBoolean(), and from Groovy 5 aFileis true only when the file exists, while 4.0.33 and 2.4.21 treat any non-nullFileas true. - Text must be converted before it is tested:
toBoolean()accepts'true','y'and'1',isInteger()guardstoInteger(), and'10' > '9'isfalseas text. ?.and*.turn anullinto anullresult instead of an exception, and they never treat 0 or empty as missing.?:and?=replace 0, empty text andfalseas well asnull, and?=and?[]do not compile on Groovy 2.4.21.
Terms in this lesson
- asBoolean()
- The method that Groovy calls to decide the truth of a value in if, while, ! and ?:. The Groovy Development Kit defines it for strings, numbers, collections, maps, matchers and, from Groovy 5, files; a class can define its own.
- Elvis operator
- The operator ?: in Groovy: a ?: b gives a when a is true by the Groovy truth and b otherwise, so 0, empty text and false are replaced as well as null.
- Groovy truth
- Groovy's rules for turning any value into true or false in a condition: null, zero and empty strings, collections, maps and arrays are false, and every other value is true unless its class defines otherwise.
- safe navigation operator
- The operator ?. in Groovy: a?.b reads b only when a is not null, and gives null otherwise instead of throwing a NullPointerException.
References
- Semantics, the Groovy Truth (Groovy 6.0.0 documentation) (The Apache Software Foundation (Apache Groovy))
- Operators, conditional and object operators (Groovy 6.0.0 documentation) (The Apache Software Foundation (Apache Groovy))
- Operators, the spread operator (Groovy 6.0.0 documentation) (The Apache Software Foundation (Apache Groovy))
- Object methods of the Groovy Development Kit (asBoolean) (The Apache Software Foundation (Apache Groovy))
- File methods of the Groovy Development Kit (asBoolean) (The Apache Software Foundation (Apache Groovy))
- Number methods of the Groovy Development Kit (asBoolean) (The Apache Software Foundation (Apache Groovy))
- Double methods of the Groovy Development Kit (asBoolean, since 2.6.0) (The Apache Software Foundation (Apache Groovy))
- CharSequence methods of the Groovy Development Kit (asBoolean) (The Apache Software Foundation (Apache Groovy))
- Collection methods of the Groovy Development Kit (asBoolean) (The Apache Software Foundation (Apache Groovy))
- Matcher methods of the Groovy Development Kit (asBoolean) (The Apache Software Foundation (Apache Groovy))
- String methods of the Groovy Development Kit (toBoolean) (The Apache Software Foundation (Apache Groovy))
- Release notes for Groovy 5.0 (File and Path truth) (The Apache Software Foundation (Apache Groovy))
- Release notes for Groovy 3.0 (Elvis assignment and safe indexing) (The Apache Software Foundation (Apache Groovy))
- Jenkins 2.580.1 bill of materials (bom/pom.xml) (The Jenkins project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress