External Processes
|
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 (6.0.0, 2026-09-22) and every example was run with Groovy 6.0.0 on JDK 26
(checked on 2026-10-14, on macOS, so the commands are POSIX ones such as echo, sort and sh). The GDK adds execute()
to String, List and String[]. It starts an operating-system process and returns a standard java.lang.Process, and a
set of helper methods on Process make the awkward parts (draining output, piping, timeouts) one-liners. The examples use
only harmless commands, read and write only inside a scratch directory work, and never use a shell unless they say so.
Running a command
'cmd arg'.execute() splits the string on whitespace and starts the program; ['cmd', 'arg'].execute() uses each list
element as one argument. The returned Process gives its standard output as in (the stream you read), its standard input
as out (the stream you write to) and its standard error as err. text reads the standard output until it closes, and
waitFor() blocks until the process ends and returns its exit value. A command that cannot be started at all, such as one
that does not exist, throws an IOException from execute().
def proc = 'echo hello from a child'.execute()
println proc.class.name
print proc.text // reads stdout until it closes
println proc.waitFor() // exit value
def args = ['echo', 'two words', 'x'].execute() // a List keeps each element as one argument
println args.text.trim()
def failing = 'ls /definitely/not/here'.execute()
failing.waitFor()
println failing.exitValue()
println failing.err.text.trim().contains('No such file')
println 'true'.execute().waitFor()
println 'false'.execute().waitFor()
try { 'no-such-command-xyz'.execute() } catch (IOException e) { println e.class.simpleName }
java.lang.ProcessImpl
hello from a child
0
two words x
1
true
0
1
IOException
A non-zero exit value is not an exception; check exitValue() (or the ok property of the result in the section on
waitForResult below) yourself.
Quoting and the shell
execute() does not start a shell. The consequences are the usual surprises:
-
A
Stringcommand is split on whitespace only, so quote characters are not interpreted. They reach the program as part of its arguments. -
There is no expansion of variables, globs or
~, and no|,>or&&of the shell. -
Interpolating a value into a string command (
"ls ${dir.path}".execute()) splits it again at every space inside the value. -
Shell built-ins (
diron Windows,cd,source) are not programs. On Windows the DOSdirneedscmd /c dir.
def one = 'printf "b\\na\\n"'.execute() // a String is split on spaces: the quotes reach printf
println one.text.readLines()
def two = ['printf', 'b\na\n'].execute() // a List keeps arguments intact
println two.text.readLines()
println 'echo $HOME'.execute().text.trim() // no shell, so nothing is expanded
println 'echo *'.execute().text.trim()
println(['sh', '-c', 'echo "$((6 * 7))"'].execute().text.trim()) // ask a shell explicitly
def dir = new File('work/my beds')
dir.mkdirs()
new File(dir, 'a.txt').text = 'x'
def bad = "ls ${dir.path}".execute() // the space splits the path in two arguments
bad.waitFor()
println bad.exitValue()
println(['ls', dir.path].execute().text.trim())
["b, a, "]
[b, a]
$HOME
*
42
1
a.txt
The first line shows the quotes as data: the command was split into the program and one argument that still contains its
quote characters, so printf printed them ("b, a, "). Prefer the List form for anything with a variable part. When a real
shell is wanted for a pipeline or a redirect, say so with ['sh', '-c', '...'], and never build that string from untrusted
input: it is a command-injection vector. With the List form and no shell there is nothing to inject into.
Draining the output
A child that writes more than the operating system’s pipe buffer blocks until somebody reads, and a parent that waits for the
child without reading deadlocks. The JDK documentation of Process warns about this, and the GDK helpers exist to avoid it:
-
consumeProcessOutput()discards both output streams in background threads;consumeProcessOutput(out, err)collects them into anyAppendable(aStringBuilder) orOutputStream. -
waitForProcessOutput()andwaitForProcessOutput(out, err)do the same and then wait for the process to end. -
consumeProcessOutputStreamandconsumeProcessErrorStreamhandle one stream each and return the thread.
def p = ['sh', '-c', 'echo out; echo err 1>&2; exit 3'].execute()
def out = new StringBuilder(), err = new StringBuilder()
p.consumeProcessOutput(out, err) // drains both pipes in background threads
p.waitFor()
println "exit=${p.exitValue()} out=${out.toString().trim()} err=${err.toString().trim()}"
def q = ['sh', '-c', 'echo a; echo b 1>&2'].execute()
def o2 = new StringBuilder(), e2 = new StringBuilder()
q.waitForProcessOutput(o2, e2) // consume both streams and wait for exit
println([o2.toString().trim(), e2.toString().trim()])
def lines = []
['printf', 'x\ny\n'].execute().in.eachLine { lines << it } // 'in' is the child's stdout
println lines
exit=3 out=out err=err
[a, b]
[x, y]
Pipes
The | operator maps to pipeTo: it connects the standard output of the left process to the standard input of the right one
and returns the right one, so a chain is read from its last element. Groovy 6.0 adds pipeline() on a list of commands,
which starts them all connected and returns the list of processes.
def sorted = ['printf', 'b\na\nc\n'].execute() | ['sort'].execute()
println sorted.text.readLines()
def procs = [['printf', 'x\ny\nx\n'], ['sort'], ['uniq', '-c']].pipeline()
println procs.size()
println procs.last().text.readLines()*.trim()
def cat = ['cat'].execute()
cat.withWriter { it << 'via the writer\n' } // closing the writer ends stdin
println cat.text.trim()
def srt = ['sort'].execute()
srt << 'pear\napple\n'
srt.out.close()
println srt.text.readLines()
[a, b, c]
3
[2 x, 1 y]
via the writer
[apple, pear]
To feed text to a child, write to out: process.withWriter { ... } and process.withOutputStream { ... } close the stream
afterwards, which is the signal to programs such as sort and cat that input has ended. With << the stream stays
open until you close it (srt.out.close() above), and a reader that waits for end of input would otherwise wait forever.
Timeouts, completion callbacks and ProcessBuilder
waitForOrKill(millis) waits that long and destroys the process if it is still running. waitFor(time, unit) is the JDK’s
version that returns false instead of killing. onExit() is the JDK’s method (Java 9 and later) and returns a CompletableFuture<Process> that completes when the child
ends; the GDK adds an overload that takes a closure, onExit { proc -> ... }, among the process additions listed in the
Groovy 6.0 release notes. toProcessBuilder() turns a command
(String, String[] or List) into a ProcessBuilder when the JDK API is needed for options such as redirects.
import java.util.concurrent.TimeUnit
def slow = ['sleep', '5'].execute()
slow.waitForOrKill(300) // destroys the process after 300 ms
println slow.waitFor() // 143 = 128 + SIGTERM
def p = ['sleep', '5'].execute()
println p.waitFor(300, TimeUnit.MILLISECONDS) // the JDK method: false means still running
p.destroy()
def r = ['sleep', '5'].execute().waitForResult(300, TimeUnit.MILLISECONDS)
println r
def f = ['echo', 'async'].execute().onExit() // CompletableFuture<Process>
println f.get().exitValue()
def pb = ['sh', '-c', 'echo $0 $1', 'a', 'b'].toProcessBuilder()
println pb.class.simpleName
pb.redirectErrorStream(true)
println pb.start().text.trim()
143
false
ProcessResult(exitCode=137, out="", err="")
0
ProcessBuilder
a b
The exit value 143 is the shell convention for a process ended by SIGTERM, and 137 for SIGKILL; both are Unix conventions, and on Windows a destroyed process reports a different value.
Source: Groovy 6.0 release notes
Options with named parameters (Groovy 6.0)
Groovy 6.0 lets execute take a map of options in place of the older execute(envp, dir) signature: dir is the working
directory, env a map of variables added to the inherited environment, redirectErrorStream: true merges standard error into
standard output, inheritIO: true shares the parent’s console, and outputFile, errorFile and inputFile redirect to and
from files, with appendOutput and appendError taking a file to append to. waitForResult() (also Groovy 6.0) waits and
returns a ProcessResult (in org.codehaus.groovy.runtime) with out, err, exitCode and ok, collecting both streams for you.
def work = new File('work')
println(['pwd'].execute(dir: work).text.trim().endsWith('/work'))
println(['sh', '-c', 'echo $GREENHOUSE_BED'].execute(env: [GREENHOUSE_BED: 'bed7']).text.trim())
println(['sh', '-c', 'echo "${HOME:+HOME kept} $GREENHOUSE_BED"'].execute(env: [GREENHOUSE_BED: 'bed7']).text.trim())
println(['sh', '-c', 'echo "${HOME:+HOME kept} $GREENHOUSE_BED"'].execute(['GREENHOUSE_BED=bed7'], null).text.trim())
def merged = ['sh', '-c', 'echo out; echo err 1>&2'].execute(redirectErrorStream: true)
println merged.text.readLines().sort()
def out = new File('work/out.txt')
out.delete()
['echo', 'to a file'].execute(outputFile: out).waitFor()
['echo', 'more'].execute(appendOutput: out).waitFor()
println out.readLines()
def ins = new File('work/in.txt'); ins.text = 'c\nb\na\n'
println(['sort'].execute(inputFile: ins).text.readLines())
def res = ['sh', '-c', 'echo out; echo err 1>&2; exit 2'].execute().waitForResult()
println res
println([res.out.trim(), res.err.trim(), res.exitCode, res.ok])
println(['true'].execute().waitForResult().ok)
def errFile = new File('work/err.txt'); errFile.delete()
['sh', '-c', 'echo oops 1>&2'].execute(errorFile: errFile).waitFor()
println errFile.readLines()
true
bed7
HOME kept bed7
bed7
[err, out]
[to a file, more]
[a, b, c]
ProcessResult(exitCode=2, out="out", err="err")
[out, err, 2, false]
true
[oops]
The third and fourth lines show a difference worth remembering: with env: the child still had HOME, while the older list
form execute(['GREENHOUSE_BED=bed7'], null) replaced the whole environment, so HOME was gone. outputFile truncates, while
appendOutput and appendError append.
Source: Groovy 6.0 release notes
Pitfalls
-
Reading
textbeforewaitFor()is safe for small output becausetextwaits for end of stream; waiting first and reading afterwards can deadlock on large output. UsewaitForProcessOutputwhen both are needed. -
Reading only standard output while the child fills standard error (or the other way round) can also deadlock; drain both, or merge them with
redirectErrorStream: true. -
A
printlnwith a leading list literal, such asprintln ['true'].execute(), is parsed as an index into a property namedprintlnand fails withNo such property: println. Writeprintln(['true'].execute()). -
The commands differ between operating systems; code that must run on Windows and Unix has to branch or avoid the shell.
-
Do not run commands built from untrusted input, and never splice such input into a
sh -cstring.
Where the book is outdated
Books from the Groovy 1.x and 2.x era describe execute(), waitFor and consumeProcessOutput and the | operator, which
are unchanged, but they know none of the Groovy 6.0 additions: pipeline(), onExit, toProcessBuilder, waitForResult
with ProcessResult and the named-parameter execute(dir:, env:, ...). They also predate the JDK’s Process.onExit(),
pid() and ProcessHandle, which are plain Java and available from Groovy.