Groovy Scripts - How do they work?

If you are already familiar with Groovy, you know that Groovy supports script mode where you can write Groovy code without explicitly creating a class. This mode is suitable when you want to house the entire code in a single class. If you are new to Groovy, you might want to get a taste of Groovy from this post.

The focus of this post is to explore how to scripts work in Groovy. Let's take a look at the following code snippets to help us appreciate the script mode.

Greeter.groovy:

class Greeter {
    static void main(String[] args) {
        String message = args.length > 0 ? "Hello ${args[0]}" : "Hello"
        println message
    }
}

GreeterScript.groovy

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

Let's quickly run both the code examples. Note that I have the files in a directory named 'demo'.

➜  demo groovy Greeter.groovy
Hello
➜  demo groovy Greeter.groovy Groovy
Hello Groovy
➜  demo groovy GreeterScript.groovy
Hello
➜  demo groovy GreeterScript.groovy Groovy
Hello Groovy

Looking at the results, we are sure that they behave the same. The script version of the code has fewer elements and hence simpler. If you look at Greeter.groovy, the class Greeter is here just to comply with the compiler. Since there is only one class, it doesn't contribute to the design aspects such as modularity or readability. Hence it is a noise from the developers perspective. While the presence of a class is necessary for JVM to execute the code, it doesn't necessarily have to be created by the developer. Tools can take away that responsibility from the developer, which is exactly what Groovy is doing here.

Does it mean that Groovy compiler created a class with a main method and placed our script code inside it? Let's find out with the help of 'groovyConsole' tool. Let's first open 'GreeterScript.groovy' from 'groovyConsole' with the following command.

➜  demo groovyConsole GreeterScript.groovy

Once you have the file open in 'groovyConsole' use "CMD+T" keyboard shortcut or "Script -> Inspect AST" from the menu. We see the following code at the end of 'Semantic Analysis' phase (there is a drop-down at the top of the Groovy AST Browser window).

public class com.nareshak.demo.script1596181510580 extends groovy.lang.Script { 

    public com.nareshak.demo.script1596181510580() {
    }

    public com.nareshak.demo.script1596181510580(groovy.lang.Binding context) {
        super(context)
    }

    public static void main(java.lang.String[] args) {
        org.codehaus.groovy.runtime.InvokerHelper.runScript(com.nareshak.demo.script1596181510580, args)
    }

    public java.lang.Object run() {
        java.lang.String message = args .length > 0 ? "Hello $args[0]" : 'Hello'
        this.println(message)
    }

}

All the statements we wrote in our script (GreeterScript.groovy) are available within the run method in the generated class. This method gets invoked from the main method. You might be wondering how does the run method get the values of args since we don't see the method run defining any parameters! Take a look at the constructor that accepts groovy.lang.Binding. The main method calls runScript method of org.codehaus.groovy.runtime.InvokerHelper with two arguments. The first argument is the script class, which is the only class we have at the moment. The second argument is args containing our command-line arguments. runScript creates an instance of groovy.lang.Binding by passing args to the constructor call. groovy.lang.Binding will store the value passed to it through constructor with the key 'args'. Thus the run method gets the command-line arguments in a variable args. To validate our findings, let's modify the script as follows.

GreeterScript.groovy

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

When we run the code, we see how args variable is available within the Binding.

➜  demo groovy GreeterScript.groovy
Hello
[args:[]]
➜  demo groovy GreeterScript.groovy Groovy
Hello Groovy
[args:[Groovy]]

At this point, you might have the following two questions.

  1. How did the run method access args which is present within the binding without specifying the exact path to it?
  2. Wouldn't it have been much simpler if the entire script contents were put inside the main method in the generated class?

Let's explore them in another post.