Groovy Scripts - Functions and Variables

In a previous post, we explored the simplicity of scripting mode offered by Groovy. We arrived at the following code.

String message = args.length > 0 ? "Hello ${args[0]}" : "Hello"
println message

Let's make the functionality slightly more sophisticated. Say, we want to send a custom message as the optional second argument. We arrive at the following code.

String message = args.length > 1 ? "Hello ${args[0]}, ${args[1]}" :
        (args.length > 0 ? "Hello ${args[0]}" : "Hello")
println message

Now, this code is hard to read. Let's refactor the code and move the message construction responsibility into individual methods as follows.

String message = args.length > 1 ? greetWithCustomMessage() :
        (args.length > 0 ? greet() : "Hello")

private String greet() {
    "Hello ${args[0]}"
}

private String greetWithCustomMessage() {
    "Hello ${args[0]}, ${args[1]}"
}

println message

Now it is better readable, yet works as before. Value of args is available through binding.variables to all the methods in the script. Hence I am not passing them as arguments to methods that construct the greeting messages.

Defining parameters to the methods greet and greetWithCustomMessage to pass args would make it less coupled to the rest of the code. But, the only reason why the current code would break is if groovy.lang.Script decides to change args to something else, say arguments. Since it is less probable, I have decided to use args in those two methods directly.

Suppose we wish to replace "Hello" to "Hey" in our greetings, we would require code changes in three places. Gotcha! We have violated the DRY(Don't Repeat Yourself) principle!

Let's attempt to fix the violation of the DRY principle by extracting the String "Hello" into a variable as follows.

String defaultMessage = "Hello"
String message = args.length > 1 ? greetWithCustomMessage() :
        (args.length > 0 ? greet() : defaultMessage)

private String greet() {
    "$defaultMessage ${args[0]}"
}

private String greetWithCustomMessage() {
    "$defaultMessage ${args[0]}, ${args[1]}"
}

println message

At this point, if you invoke the script with arguments, you will end up with the following error!

➜  demo groovy GreeterScript.groovy Raj "Have a great day"
Caught: groovy.lang.MissingPropertyException: No such property: defaultMessage for class: com.nareshak.demo.GreeterScript
groovy.lang.MissingPropertyException: No such property: defaultMessage for class: com.nareshak.demo.GreeterScript
	at com.nareshak.demo.GreeterScript.greetWithCustomMessage(GreeterScript.groovy:12)
	at com.nareshak.demo.GreeterScript.run(GreeterScript.groovy:4)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

We have already learned that the statements from our scripts are bundled into the run method in the generated class that extends groovy.lang.Script. While the methods we introduced in our script can be added to the generated class along with run, there is a challenge with the variables. They can be added as local variables inside the run method or as fields in the generated class. Groovy decides to add them as local variables. Hence, defaultMessage is not available in the scope of greetWithCustomMessage and greet.

Does it mean that you cannot create variables at the script scope? You can create variables at the script scope by instructing Groovy through the AST transformation groovy.transform.Field as follows.

import groovy.transform.Field

@Field
String defaultMessage = "Hello"

To summarise, we have seen how we can introduce additional functions and variables in our Groovy scripts to contain the complexity.