Running Scripts & the groovy Command

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Groovy 6.0.0 (released 2026-09-22), run on JDK 26 and checked on 2026-10-14. It covers the groovy launcher, which plays for Groovy the role the java command plays for Java, except that it takes source files, compiles them in memory and runs them. Two behaviours changed in 6.0 and are called out where they appear: the launchers no longer add the current directory to a classpath you supplied, and diagnostics go through the JDK Platform Logging API instead of being written straight to standard error. Installation is on Getting Started; the interactive tools are on groovysh & groovyconsole; ahead-of-time compilation with groovyc is on Compiling with groovyc & Joint Compilation.

All the commands below were run from a scratch directory that holds a small data/readings.csv sensor log for the greenhouse scenario:

sensor,time,temperature,humidity,soil
bench-1,2026-10-14T06:00:00Z,18.4,71,0.32
bench-1,2026-10-14T06:10:00Z,18.9,70,0.31
bench-2,2026-10-14T06:00:00Z,,68,0.27
basket-1,2026-10-14T06:00:00Z,17.2,75,0.40
bench-1,2026-10-14T06:20:00Z,19.6,69,0.30

Running a script

groovy MyScript.groovy compiles the file and runs it. The .groovy extension is optional, and a file with no extension at all also runs. Groovy compiles the file to a class and runs it, so the script’s class name comes from the file name, with characters that are illegal in a Java identifier replaced by underscores:

println "args = ${args as List} (${args.getClass().simpleName})"
println "script = ${getClass().name}"
println "prop = ${System.getProperty('greenhouse.zone')}"
groovy -Dgreenhouse.zone=bench-1 show-args.groovy a "b c" -x
groovy show-args a
args = [a, b c, -x] (String[])
script = show_args
prop = bench-1
args = [a] (String[])
script = show_args
prop = null

Everything after the script name is passed to the script untouched in the args array, including strings that look like options (-x). Every option meant for groovy itself must come before the script name. The -D option defines a JVM system property, which the script reads with System.getProperty. Do not name a script after a variable you use in it: a file called args.groovy becomes a class called args, and inside the script the name args then refers to the class instead of the argument array.

Options

groovy --help prints the usage for the installed version. The options of 6.0.0 are:

Option Meaning

-e <script>

Run an inline script. Arguments after it are available as args

-n

Process each line of the files, or of standard input if none is given, with the line in the implicit variable line

-p

Like -n, and print the value of the script for every line

-a [pattern], --autosplit[=pattern]

Split every line with a regular expression into the implicit array split. Use it with -n or -p

-i [extension]

Modify the files in place; if an extension such as .bak is given, keep a backup copy

-l [[host:]port]

Listen on a port (default 1960) and run the script once per line received. Binds the loopback address since 6.0 unless a host is given

-cp, -classpath, --classpath <path>

Classpath for compiling and running. Must be the first argument

-D <name=value>, --define

Define a system property

-b <class>, --basescript <class>

Base class for scripts; it must extend groovy.lang.Script

--configscript <script>

Run a script that adjusts the CompilerConfiguration (compilation customizers, for example)

-c <charset>, --encoding

Encoding of the source files

-pa, --parameters

Store method parameter names in the generated classes, so reflection can see them

-pr, --enable-preview

Enable JDK preview features (JDK 12 and newer)

-d, --debug

Print full stack traces

--disableopt <list>

Disable optimizations: all or int

--compile-static, --type-checked

Compile the script with @CompileStatic or @TypeChecked

-h, --help; -v, --version

Usage, and the Groovy and JVM versions

groovy -v prints the version of the install used to run these examples:

Groovy Version: 6.0.0 JVM: 26 Vendor: Oracle Corporation OS: Mac OS X

One-liners

-e runs the text that follows as a script, and -n, -p, -a and -i turn Groovy into a line processor in the tradition of awk and sed. The expressions below read the greenhouse log; the println results are exactly what the commands printed.

groovy -e "println 'greenhouse ' + (1..3).collect { it * 2 }"
groovy -n -e 'if (line.startsWith("bench-1")) println line.split(",")[2]' data/readings.csv
groovy -p -e 'line.toUpperCase()' data/readings.csv | head -2
groovy -n '--autosplit=,' -e 'if (split[0] == "bench-1") println "${split[1]} -> ${split[2]}"' data/readings.csv
cat data/readings.csv | groovy -n '--autosplit=,' -e 'println split[0]' | sort | uniq -c
greenhouse [2, 4, 6]
18.4
18.9
19.6
SENSOR,TIME,TEMPERATURE,HUMIDITY,SOIL
BENCH-1,2026-10-14T06:00:00Z,18.4,71,0.32
2026-10-14T06:00:00Z -> 18.4
2026-10-14T06:10:00Z -> 18.9
2026-10-14T06:20:00Z -> 19.6
   1 basket-1
   3 bench-1
   1 bench-2
   1 sensor

With -n the script body is run once per line and nothing is printed unless the script prints it. With -p the value of the body, that is, the value of its last expression, is printed for every line, so line.toUpperCase() alone is enough. If you name no file, both options read standard input, which makes groovy usable at the end of a pipe.

-i rewrites the files in place. The log has a gap, an empty temperature for bench-2; this fills it with NA and keeps the original as copy.csv.bak:

groovy -p -i .bak -e 'line.replace(",,", ",NA,")' data/copy.csv
sensor,time,temperature,humidity,soil
bench-1,2026-10-14T06:00:00Z,18.4,71,0.32
bench-1,2026-10-14T06:10:00Z,18.9,70,0.31
bench-2,2026-10-14T06:00:00Z,NA,68,0.27
basket-1,2026-10-14T06:00:00Z,17.2,75,0.40
bench-1,2026-10-14T06:20:00Z,19.6,69,0.30

The content shown is data/copy.csv after the command. Two practical notes from running these on 6.0.0. First, give -a its pattern explicitly ('--autosplit=\s+', or --autosplit=, as above); when the option was given with no pattern, the lines were split between every character instead of at whitespace, so the documented default cannot be relied on. Second, -i without an extension edits the files with no backup, so use a backup extension until the script is proven.

The shebang line and exit codes

A file that starts with #!/usr/bin/env groovy and is marked executable (chmod +x) runs as a command on Unix-like systems, provided groovy is on the PATH. Groovy ignores a first line beginning with #!. The script below reports the zones whose soil moisture is under a threshold, takes its input from args, and sets the exit status for the shell:

#!/usr/bin/env groovy
// Report the zones whose soil moisture is below a threshold.
if (!args) {
    System.err.println 'usage: check-moisture <csv-file> [threshold]'
    System.exit 2
}
def threshold = (args.length > 1 ? args[1] : '0.30') as BigDecimal
def dry = new File(args[0]).readLines().drop(1)
        .collect { it.split(',') }
        .findAll { it[4] as BigDecimal < threshold }
        .collect { it[0] }
        .unique()
if (dry) {
    println "dry: ${dry.join(', ')}"
    System.exit 1
}
println 'all zones are moist enough'
./check-moisture data/readings.csv ; echo "exit=$?"
./check-moisture data/readings.csv 0.2 ; echo "exit=$?"
./check-moisture ; echo "exit=$?"
dry: bench-2
exit=1
all zones are moist enough
exit=0
usage: check-moisture <csv-file> [threshold]
exit=2

The exit status follows the usual rules of the JVM:

  • A script that ends normally exits with 0. The value of the last expression is not an exit status: groovy -e '42' exits with 0.

  • System.exit(n) ends the process with status n. The value 3 from groovy -e 'System.exit(3)' is what the shell sees.

  • An uncaught exception makes the launcher log the exception and exit with status 1. In 6.0.0 the message is written through the logging API (see Logging), and the first line of its output is the literal text Caught: {0} followed by the trace:

SEVERE: Caught: {0}
java.lang.IllegalStateException: sensor offline
	at script_from_command_line.run(script_from_command_line:1)
	...

The -d option asks for full stack traces. The log line carries a time stamp and class name before SEVERE, which depends on the locale, and is omitted above.

Classpath rules

-cp (or -classpath, --classpath) must be the first argument. It gives the classpath for compiling the script and for running it. A script can load classes from compiled .class files or from .groovy sources found on that classpath:

groovyc -d lib lib/Dep.groovy
groovy run.groovy                   # run.groovy uses class Local, found as Local.groovy in the current directory
groovy -cp lib run.groovy           # Groovy 6.0: the current directory is no longer searched
groovy -cp lib:. run.groovy         # add the current directory yourself (lib;. on Windows)
local class from .
run.groovy: 1: unable to resolve class Local
local class from .

The three lines are the outputs of the three commands in order (the middle one is an error message from the compiler). Before Groovy 6.0 the launchers appended . to whatever came from -cp or from the CLASSPATH environment variable, which the java launcher never did. Since 6.0 an explicit classpath is used as is; only a bare invocation with no classpath at all falls back to the current directory. Scripts that mix a library jar with sibling source files need . added by hand. The behaviour applies to groovyc, groovysh and the other launchers as well.

Dependencies that are not on disk are better fetched with @Grab than with a hand-built classpath; see Grape Dependency Management.

Listening on a port with -l

-l turns the script into a tiny line-oriented server: Groovy listens on a port and runs the script once for each line a client sends, with the line in line. Combined with -p, the result of the script is written back to the client:

groovy -l 1960 -p -e 'line.reverse()' &
printf 'bench-1\nbasket-1\n' | nc -w 2 127.0.0.1 1960
groovy is listening on 127.0.0.1 port 1960
1-hcneb
1-teksab

The first line is printed by the listener when it starts, and the other two are the client’s answers. Without -p, the script has to write the reply itself: println goes to the client, as the socket is the script’s out variable.

Since Groovy 6.0 the listener binds only the loopback address, so only clients on the same machine can connect. Earlier releases accepted connections from every network interface. To listen more widely, give a host or an address, for example -l 0.0.0.0:1960. Be careful: Groovy performs no authentication and the connection is not encrypted, so a listener bound beyond loopback hands the script to any peer that can reach the port. The option is a developer convenience for experimenting with a line-driven script, not an application server. The script is read once, when the listener starts, and recompiled for each connection.

Base scripts and configuration scripts

Every script compiles to a subclass of groovy.lang.Script. The option -b changes that superclass, so a command-line script gets the methods and properties of your own base class. This is the mechanism behind the greenhouse watering DSL.

abstract class GreenhouseScript extends Script {
    String zone = 'bench-1'

    void water(String plant) {
        println "watering $plant in $zone"
    }
}
water 'tomatoes'
groovyc GreenhouseScript.groovy
groovy -b GreenhouseScript evening.groovy
watering tomatoes in bench-1

--configscript runs a script that receives a configuration variable, the CompilerConfiguration that will be used for the script, so you can add compilation customizers without writing a launcher. This one adds java.time.* as a star import and compiles the script with @CompileStatic:

import org.codehaus.groovy.control.customizers.ImportCustomizer
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import groovy.transform.CompileStatic

configuration.addCompilationCustomizers(
        new ImportCustomizer().addStarImports('java.time'),
        new ASTTransformationCustomizer(CompileStatic))

With a script that says println LocalDate.of(2026, 10, 14).dayOfWeek, the command groovy --configscript config.groovy dates.groovy printed WEDNESDAY. Compilation customizers are the subject of Compilation Customizers & Secure Scripting. For one-off static checking there are also the shortcuts --type-checked and --compile-static; the first makes this script fail at compile time:

def x = 1; x.foo()
bad.groovy: 1: [Static type checking] - Cannot find matching method java.lang.Integer#foo(). Please check if the declared type is correct and if the method exists.
 @ line 1, column 12.

Run without the option, the same file compiles and fails at run time with groovy.lang.MissingMethodException: No signature of method: foo for class: java.lang.Integer is applicable for argument types: () values: [].

Script options with CliBuilder

A script that takes options is easier to use if it prints its usage and rejects bad input. CliBuilder wraps a command-line parser and builds the option set from a short DSL. The classes live in two modules, groovy-cli-commons (class groovy.cli.commons.CliBuilder, built on Apache Commons CLI) and groovy-cli-picocli (class groovy.cli.picocli.CliBuilder, built on picocli). The former groovy.util.CliBuilder no longer exists in 6.0: referring to it fails with MissingPropertyException: No such property: groovy. Both modules ship in the Groovy distribution, but the groovy-cli-picocli module needs the picocli jar, which is not part of the distribution; the commons variant works out of the box and is used here. Builders are described further on Builders.

import groovy.cli.commons.CliBuilder

def cli = new CliBuilder(usage: 'moisture-report [options] <csv-file>', header: 'Options:')
cli.h(longOpt: 'help', 'show this help')
cli.t(longOpt: 'threshold', args: 1, argName: 'level', 'soil moisture threshold (default 0.30)')
cli.v(longOpt: 'verbose', 'print every dry reading')

def options = cli.parse(args)
if (!options) {          // parse errors were already reported
    return
}
if (options.h || !options.arguments()) {
    cli.usage()
    return
}
def threshold = (options.t ?: '0.30') as BigDecimal
def rows = new File(options.arguments()[0]).readLines().drop(1)*.split(',')
def dry = rows.findAll { it[4] as BigDecimal < threshold }
println "${dry.size()} of ${rows.size()} readings are below $threshold"
if (options.v) {
    dry.each { println "  ${it[0]} at ${it[1]}: ${it[4]}" }
}
groovy moisture-report.groovy -t 0.31 -v data/readings.csv
groovy moisture-report.groovy --help
2 of 5 readings are below 0.31
  bench-2 at 2026-10-14T06:00:00Z: 0.27
  bench-1 at 2026-10-14T06:20:00Z: 0.30
usage: moisture-report [options] <csv-file>
Options:
 -h,--help                show this help
 -t,--threshold <level>   soil moisture threshold (default 0.30)
 -v,--verbose             print every dry reading

The commons parser is lenient with options it does not know: -z in front of a file name was treated as an argument and the script then failed with a FileNotFoundException for -z, so check options.arguments() yourself. The annotation-based form of CliBuilder (an interface with @Option and @Unparsed) is described in the documentation linked below.

Logging

Groovy 6.0 replaced the diagnostic output that its tools used to write straight to standard error with the JDK Platform Logging API (java.lang.System.Logger). By default the messages still go to the console, via java.util.logging, but you can route, filter or silence them like those of any JDK component, or plug in SLF4J, Log4j2 and others by providing a System.LoggerFinder. The configuration file is resolved in this order, and the first match wins:

  1. a file named by -Djava.util.logging.config.file=... (passed to the JVM through JAVA_OPTS)

  2. ~/.groovy/logging.properties, found automatically at startup

  3. the JDK default, $JAVA_HOME/conf/logging.properties

Loggers are named after the class that emits the message, for example groovy.ui.GroovyMain (script errors and uncaught exceptions), groovy.grape.Grape (Grape provider selection) and org.codehaus.groovy.tools.FileSystemCompiler (compilation errors and warnings). This file keeps the trace of an uncaught exception out of the output, and shortens the format of everything else:

handlers=java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.SimpleFormatter.format=%4$s [%3$s] %5$s%n
.level=INFO
groovy.ui.GroovyMain.level=OFF
JAVA_OPTS=-Djava.util.logging.config.file=$PWD/logging.properties \
  groovy -e 'throw new IllegalStateException("sensor offline")'
java.lang.IllegalStateException: sensor offline
	at script_from_command_line.run(script_from_command_line:1)

The two lines are the short summary that the launcher prints for itself; the long log entry was suppressed. The process still exits with status 1. Removing the groovy.ui.GroovyMain.level=OFF line restores the log entry in the short format, SEVERE [groovy.ui.GroovyMain] Caught: {0}, followed by the trace. For a troubleshooting session on dependency resolution, put groovy.grape.Grape.level=FINE in ~/.groovy/logging.properties. The complete list of loggers is in the documentation linked below.

Where the book is outdated

The first edition of the reference book (Groovy 1.0, 2007) describes groovy -l as listening on all interfaces, a convenience that became a security concern: the option binds the loopback address since 6.0 and has no authentication. The book’s launcher also adds the current directory to the classpath you pass, which stopped in 6.0. Its -e, -n, -p, -i and -a line-processing idioms still work as described, although the -a default pattern was found unreliable in 6.0.0 (see above). The book’s CliBuilder is groovy.util.CliBuilder, which no longer exists in 6.0; its successors are the groovy.cli.commons and groovy.cli.picocli variants. Its approach to dependencies, copying jars to ~/.groovy/lib, is superseded by @Grab. See What’s New & Migrating from Older Versions for the full list.