Groovy execute command and process execution with 12+ examples. Covers String.execute(), ProcessBuilder, stdout/stderr capture, exit codes, timeouts, and piping. Groovy 5.x.
“Any sufficiently advanced build script will eventually shell out to the operating system. Groovy just makes it embarrassingly easy.”
Brian Kernighan, Unix Philosophy
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Intermediate | Reading Time: 25 minutes
When you need to groovy execute command from a script – checking disk space, calling git, restarting a service, automating a deployment pipeline – Groovy makes it a one-liner. While Java forces you to wrestle with Runtime.getRuntime().exec() and manually wire up input streams, Groovy lets you run "command".execute() and capture the output directly.
This post covers how to execute system commands from Groovy, capture stdout and stderr, handle exit codes, set timeouts, pipe commands together, use ProcessBuilder for advanced scenarios, and build real-world automation scripts – all with 12+ tested examples.
If you’re comfortable running Groovy from the terminal, you’re all set. If not, check out our Groovy Command Line -e Option guide first to get up and running quickly.
Table of Contents
What is Process Execution in Groovy?
Process execution means running an external operating system command (like ls, dir, git status, or any executable) from within your Groovy program. The command runs as a separate OS process, and your Groovy code can capture its output, check its exit code, and feed it input.
Groovy adds a method called execute() directly onto String and List objects via the GDK (Groovy Development Kit). This means you can literally write "ls -la".execute() and get a java.lang.Process object back. Under the hood, it delegates to Runtime.getRuntime().exec(), but with a much friendlier API.
According to the official Groovy documentation, Groovy provides several convenience methods on the Process class for working with process I/O, including .text, .inputStream, consumeProcessOutput(), and pipe operators.
Key Points:
"command".execute()returns ajava.lang.Processobject["cmd", "arg1", "arg2"].execute()is safer for commands with spaces or special characters- Groovy adds
.text,.inputStream,waitFor(), andconsumeProcessOutput()to the standard JavaProcessclass - For advanced control, you can use Java’s
ProcessBuilderalongside Groovy’s convenience methods
Why Use Groovy for Process Execution?
You might wonder – why not just write a bash script? Here’s the thing: Groovy gives you the best of both worlds. You get the power of a real programming language (conditionals, loops, exception handling, data structures) combined with one-liner shell commands. Think of it as bash scripting with superpowers.
Compared to Java’s verbose ProcessBuilder setup with manual stream handling, Groovy lets you execute a command and capture its output in a single expression. And unlike Python’s subprocess module, Groovy’s approach feels native – you’re calling .execute() right on a string. No imports needed for basic usage.
Groovy process execution is especially useful in Jenkins pipelines, Gradle build scripts, and DevOps automation where you need to mix scripting logic with system commands smoothly.
When to Use Process Execution?
Process execution shines when you need to:
- Automate deployments – run
git pull,docker build, orkubectl applyfrom a Groovy script - Build CI/CD pipelines – Jenkins pipelines are Groovy scripts that constantly shell out to tools
- System monitoring – check disk space, memory usage, running processes
- File processing – call external tools like
ffmpeg,ImageMagick, orpandoc - Database admin – run
pg_dump,mysqldump, or migration scripts
When should you not use it? If there’s a native Java or Groovy library that does the same thing, prefer the library. For example, don’t shell out to curl when you can use Groovy’s URL.text or an HTTP client. Libraries are more portable, testable, and don’t depend on the OS having a particular tool installed.
How Process Execution Works Under the Hood
When you call "ls -la".execute(), here’s what Groovy does internally:
- Tokenization: The string is split by whitespace into an array:
["ls", "-la"]. This is why commands with spaces in arguments can fail with the string form. - Process creation: Groovy calls
Runtime.getRuntime().exec(String[])to create an OS-level process. The JVM forks and executes the command. - Stream setup: Three streams are created –
inputStream(stdout from the process),errorStream(stderr), andoutputStream(stdin to the process). - GDK enhancement: Groovy adds convenience methods like
.text(reads all stdout as a String),consumeProcessOutput()(reads both stdout and stderr asynchronously), and the pipe operator|to chain processes.
The critical thing to understand: the process runs asynchronously by default. Calling .execute() starts the process but doesn’t wait for it to finish. You need .text or .waitFor() to block until it completes.
Syntax and Basic Usage
Official Syntax
Here’s the basic syntax for Groovy process execution:
Syntax
// String form - simple commands
Process proc = "command arg1 arg2".execute()
// List form - safer for arguments with spaces
Process proc = ["command", "arg1", "arg with spaces"].execute()
// With environment variables and working directory
Process proc = "command".execute(["VAR=value"], new File("/path"))
// Capture output
String output = "command".execute().text
// Wait for completion and get exit code
int exitCode = "command".execute().waitFor()
Key Methods on Process
| Method | Returns | Description |
|---|---|---|
.text | String | Reads all stdout and blocks until process finishes |
.inputStream | InputStream | Raw stdout stream from the process |
.errorStream | InputStream | Raw stderr stream from the process |
.waitFor() | int | Blocks until process finishes, returns exit code |
.exitValue() | int | Returns exit code (throws if not finished) |
consumeProcessOutput(out, err) | void | Asynchronously reads stdout and stderr into StringBuffers |
waitForOrKill(millis) | void | Waits with timeout, kills process if exceeded |
Practical Examples
Example 1: Basic String.execute() – Your First Command
What we’re doing: Running a simple system command using the String.execute() method and capturing its output.
Example 1: Basic String.execute()
// The simplest way to run a command in Groovy
def proc = "java -version".execute()
// .text reads all stdout - but java -version writes to stderr!
def stdout = proc.inputStream.text
def stderr = proc.errorStream.text
println "stdout: ${stdout}"
println "stderr: ${stderr}"
println "exit code: ${proc.waitFor()}"
Output
stdout: stderr: openjdk version "17.0.10" 2024-01-16 OpenJDK Runtime Environment Temurin-17.0.10+7 (build 17.0.10+7) OpenJDK 64-Bit Server VM Temurin-17.0.10+7 (build 17.0.10+7, mixed mode, sharing) exit code: 0
What happened here: We called "java -version".execute() which started a new OS process. Here’s the surprise – java -version writes to stderr, not stdout! That’s why .text (which reads stdout) would return empty. Always check both streams when a command seems to produce no output.
Example 2: List Form for Commands with Arguments
What we’re doing: Using the list form of execute() to safely handle commands with spaces in arguments.
Example 2: List Form execute()
// String form splits by whitespace - breaks with spaces in args // "echo Hello World".execute() // Works, but only by luck // List form - each element is a separate argument def proc1 = ["echo", "Hello World"].execute() println proc1.text.trim() // This is essential for paths with spaces def proc2 = ["groovy", "-e", "println 'Groovy rocks!'"].execute() println proc2.text.trim() // On Windows, use cmd /c for built-in commands // def proc3 = ["cmd", "/c", "echo", "Hello from Windows"].execute() // println proc3.text.trim() // On Linux/Mac, use shell for built-in commands def proc4 = ["/bin/sh", "-c", "echo Hello from shell"].execute() println proc4.text.trim()
Output
Hello World Groovy rocks! Hello from shell
What happened here: The list form gives you full control over argument boundaries. When you write ["groovy", "-e", "println 'Groovy rocks!'"], the entire "println 'Groovy rocks!'" is passed as a single argument to the -e flag. With the string form, it would be split at every space, breaking the command.
Pro Tip: Always use the list form when your arguments might contain spaces, quotes, or special characters. It’s the equivalent of using parameterized queries in SQL – safer by design.
Example 3: Capturing stdout and stderr Separately
What we’re doing: Using consumeProcessOutput() to capture both standard output and standard error simultaneously without deadlocks.
Example 3: Capturing stdout and stderr
// Create a command that writes to both stdout and stderr
def script = '''
import System.out as sout
import System.err as serr
sout.println "This goes to stdout"
sout.println "More stdout content"
serr.println "WARNING: this goes to stderr"
sout.println "Final stdout line"
serr.println "ERROR: another stderr line"
'''
def proc = ["groovy", "-e", script].execute()
// Use StringBuffers to capture output asynchronously
def stdout = new StringBuffer()
def stderr = new StringBuffer()
proc.consumeProcessOutput(stdout, stderr)
proc.waitFor()
println "=== STDOUT ==="
println stdout.toString().trim()
println ""
println "=== STDERR ==="
println stderr.toString().trim()
println ""
println "Exit code: ${proc.exitValue()}"
Output
=== STDOUT === This goes to stdout More stdout content Final stdout line === STDERR === WARNING: this goes to stderr ERROR: another stderr line Exit code: 0
What happened here: consumeProcessOutput() starts background threads to read both stdout and stderr at the same time. This is critical – if you only read one stream and the process writes a lot to the other, the buffer can fill up and the process will hang (deadlock). Always use consumeProcessOutput() when you need both streams.
Example 4: Exit Codes and Error Handling
What we’re doing: Checking the exit code to determine if a command succeeded or failed, and handling errors gracefully.
Example 4: Exit Codes
// A command that succeeds (exit code 0)
def success = "java -version".execute()
success.consumeProcessOutput(new StringBuffer(), new StringBuffer())
success.waitFor()
println "java -version exit code: ${success.exitValue()}"
// A command that fails
def fail = ["groovy", "-e", "System.exit(42)"].execute()
fail.consumeProcessOutput(new StringBuffer(), new StringBuffer())
fail.waitFor()
println "failed command exit code: ${fail.exitValue()}"
// Practical pattern: execute and check
def runCommand(String... cmd) {
def proc = cmd.toList().execute()
def stdout = new StringBuffer()
def stderr = new StringBuffer()
proc.consumeProcessOutput(stdout, stderr)
proc.waitFor()
if (proc.exitValue() != 0) {
println "FAILED (exit ${proc.exitValue()}): ${stderr}"
return null
}
return stdout.toString().trim()
}
def result = runCommand("groovy", "-e", "println 'All good!'")
println "Result: ${result}"
def bad = runCommand("groovy", "-e", "throw new RuntimeException('boom')")
println "Bad result: ${bad}"
Output
java -version exit code: 0 failed command exit code: 42 Result: All good! FAILED (exit 1): org.codehaus.groovy.runtime.InvokerInvocationException: java.lang.RuntimeException: boom Bad result: null
What happened here: Exit code 0 means success in Unix/Windows convention. Anything else indicates failure. We built a reusable runCommand() helper that captures output, checks the exit code, and returns null on failure. This pattern is extremely useful in automation scripts.
Example 5: Process.waitFor() with Timeout – waitForOrKill
What we’re doing: Setting a timeout so a long-running or stuck command doesn’t hang your script forever.
Example 5: Timeout Handling
// A command that takes a while
def proc = ["groovy", "-e", "Thread.sleep(2000); println 'Done after 2s'"].execute()
// Wait up to 5 seconds - command should finish in time
def stdout = new StringBuffer()
def stderr = new StringBuffer()
proc.consumeProcessOutput(stdout, stderr)
proc.waitForOrKill(5000) // 5000 ms timeout
println "Result: ${stdout.toString().trim()}"
println "Exit code: ${proc.exitValue()}"
println "---"
// Now a command that exceeds the timeout
def slow = ["groovy", "-e", "Thread.sleep(10000); println 'Finished'"].execute()
def out2 = new StringBuffer()
def err2 = new StringBuffer()
slow.consumeProcessOutput(out2, err2)
slow.waitForOrKill(1000) // Only wait 1 second
println "Slow output: '${out2.toString().trim()}'"
println "Slow exit code: ${slow.exitValue()}"
Output
Result: Done after 2s Exit code: 0 --- Slow output: '' Slow exit code: 143
What happened here: waitForOrKill(5000) gives the process up to 5 seconds to finish. The first command completed in 2 seconds, so it worked fine. The second command needed 10 seconds but only got 1 second – Groovy killed it. Exit code 143 means the process was terminated by a signal (128 + 15 = SIGTERM on Linux). Always use waitForOrKill() in production scripts to prevent runaway processes.
Example 6: Piping Commands Together
What we’re doing: Chaining commands using Groovy’s pipe operator so the output of one becomes the input of the next – just like | in the shell.
Example 6: Piping Commands
// Groovy supports the | operator between Process objects
// This is equivalent to: echo "hello\nworld\nhello\ngroovy" | sort | uniq
def proc = ["groovy", "-e", """
println 'hello'
println 'world'
println 'hello'
println 'groovy'
println 'world'
println 'hello'
"""].execute()
// On systems with sort and uniq:
// def piped = proc | "sort".execute() | "uniq".execute()
// println piped.text
// Cross-platform approach using shell
def piped = ["/bin/sh", "-c", "echo 'hello\nworld\nhello\ngroovy\nworld\nhello' | sort | uniq"].execute()
println piped.text.trim()
println "---"
// Another example: find files and count lines
def countCmd = ["/bin/sh", "-c", "echo 'line1\nline2\nline3\nline4\nline5' | wc -l"].execute()
println "Line count: ${countCmd.text.trim()}"
Output
groovy hello world --- Line count: 5
What happened here: Groovy overloads the | (pipe) operator on Process objects. When you write procA | procB, it connects procA’s stdout to procB’s stdin. For multi-step pipes, wrapping it in a shell command (/bin/sh -c "..." or cmd /c "..." on Windows) is often easier and more reliable.
Example 7: Setting Environment Variables
What we’re doing: Passing custom environment variables to the child process.
Example 7: Environment Variables
// The execute() method accepts environment variables as a list of "KEY=VALUE" strings
def envVars = ["MY_NAME=Groovy Developer", "MY_VERSION=5.0", "DEBUG=true"]
def proc = ["groovy", "-e", """
println "Name: \${System.getenv('MY_NAME')}"
println "Version: \${System.getenv('MY_VERSION')}"
println "Debug: \${System.getenv('DEBUG')}"
println "HOME: \${System.getenv('HOME') ?: System.getenv('USERPROFILE')}"
"""].execute(envVars, null)
def stdout = new StringBuffer()
def stderr = new StringBuffer()
proc.consumeProcessOutput(stdout, stderr)
proc.waitFor()
println stdout.toString().trim()
Output
Name: Groovy Developer Version: 5.0 Debug: true HOME: /home/developer
What happened here: The execute(List envp, File dir) overload lets you pass environment variables as a list of "KEY=VALUE" strings. Note that when you pass environment variables this way, the child process gets only these variables (plus inherited ones depending on the OS). The second argument sets the working directory – we passed null to inherit the parent’s directory.
Example 8: Setting the Working Directory
What we’re doing: Running a command in a specific directory – essential for git operations and project-relative scripts.
Example 8: Working Directory
// Create a temp directory with some files
def tempDir = File.createTempDir("groovy-process-", "")
new File(tempDir, "hello.txt").text = "Hello from Groovy!"
new File(tempDir, "data.csv").text = "name,age\nAlice,30\nBob,25"
new File(tempDir, "script.sh").text = "#!/bin/bash\necho 'script ran!'"
// Execute 'ls' in that specific directory
def proc = ["/bin/sh", "-c", "ls -1"].execute(null, tempDir)
println "Files in ${tempDir.name}:"
println proc.text.trim()
println "---"
// Read a file relative to the working directory
def catProc = ["/bin/sh", "-c", "cat hello.txt"].execute(null, tempDir)
println "File content: ${catProc.text.trim()}"
println "---"
// Show current working directory
def pwdProc = ["/bin/sh", "-c", "pwd"].execute(null, tempDir)
println "Working dir: ${pwdProc.text.trim()}"
// Cleanup
tempDir.deleteDir()
Output
Files in groovy-process-3847291056203: data.csv hello.txt script.sh --- File content: Hello from Groovy! --- Working dir: /tmp/groovy-process-3847291056203
What happened here: The second parameter of execute(envp, dir) sets the working directory for the child process. This is incredibly useful when you need to run git commands in a specific repository, or read files relative to a project root. If you want to learn more about file operations in Groovy, check out our Groovy File Read, Write, and Delete guide.
Example 9: ProcessBuilder for Advanced Control
What we’re doing: Using Java’s ProcessBuilder for fine-grained control – redirecting stderr to stdout, setting environment, and controlling I/O.
Example 9: ProcessBuilder
// ProcessBuilder gives you more control than String.execute()
def pb = new ProcessBuilder(["groovy", "-e", """
System.out.println "stdout line 1"
System.err.println "stderr line 1"
System.out.println "stdout line 2"
System.err.println "stderr line 2"
"""])
// Merge stderr into stdout - one stream to read
pb.redirectErrorStream(true)
// Set working directory
pb.directory(new File(System.getProperty("user.home")))
// Add environment variables
pb.environment().put("CUSTOM_VAR", "hello from ProcessBuilder")
// Start the process
def proc = pb.start()
def output = proc.inputStream.text
proc.waitFor()
println "Combined output:"
println output.trim()
println "Exit: ${proc.exitValue()}"
println "---"
// ProcessBuilder with redirects to files
def pb2 = new ProcessBuilder(["groovy", "-e", "println 'saved to file'"])
def outputFile = File.createTempFile("groovy-out-", ".txt")
pb2.redirectOutput(outputFile)
pb2.start().waitFor()
println "File contents: ${outputFile.text.trim()}"
outputFile.delete()
Output
Combined output: stdout line 1 stderr line 1 stdout line 2 stderr line 2 Exit: 0 --- File contents: saved to file
What happened here: ProcessBuilder is Java’s more powerful alternative to Runtime.exec(). The killer feature is redirectErrorStream(true), which merges stderr into stdout so you only need to read one stream. You can also redirect output directly to files with redirectOutput(File) – no manual stream copying needed.
Example 10: Reading Output Line by Line (Streaming)
What we’re doing: Reading process output line by line instead of waiting for the entire output – useful for long-running commands.
Example 10: Streaming Output
// Simulate a command that produces output over time
def proc = ["groovy", "-e", """
(1..5).each { i ->
println "Processing item \${i}..."
System.out.flush()
Thread.sleep(200)
}
println "All done!"
"""].execute()
// Read line by line as output arrives
proc.inputStream.eachLine { line ->
println "[${new Date().format('HH:mm:ss')}] ${line}"
}
println "Exit code: ${proc.waitFor()}"
println "---"
// Alternative: use BufferedReader for more control
def proc2 = ["groovy", "-e", """
println 'START'
Thread.sleep(100)
println 'MIDDLE'
Thread.sleep(100)
println 'END'
"""].execute()
def reader = proc2.inputStream.newReader()
def line
while ((line = reader.readLine()) != null) {
println "Got: ${line}"
}
reader.close()
proc2.waitFor()
Output
[14:32:05] Processing item 1... [14:32:05] Processing item 2... [14:32:06] Processing item 3... [14:32:06] Processing item 4... [14:32:06] Processing item 5... [14:32:06] All done! Exit code: 0 --- Got: START Got: MIDDLE Got: END
What happened here: Instead of using .text (which blocks until the process finishes and loads everything into memory), we used inputStream.eachLine to process output as it arrives. This is essential for long-running commands where you want real-time feedback – think build logs, deployment scripts, or monitoring tools.
Example 11: Executing Shell Scripts
What we’re doing: Creating and executing shell scripts from Groovy, including passing arguments to the script.
Example 11: Execute Shell Scripts
// Create a temporary shell script
def scriptFile = File.createTempFile("groovy-script-", ".sh")
scriptFile.text = '''#!/bin/bash
echo "Script name: $0"
echo "Argument 1: $1"
echo "Argument 2: $2"
echo "All args: $@"
echo "Current dir: $(pwd)"
echo "Date: $(date +%Y-%m-%d)"
exit 0
'''
scriptFile.setExecutable(true)
// Execute the script with arguments
def proc = [scriptFile.absolutePath, "hello", "world"].execute()
def stdout = new StringBuffer()
def stderr = new StringBuffer()
proc.consumeProcessOutput(stdout, stderr)
proc.waitFor()
println stdout.toString().trim()
if (stderr.toString().trim()) {
println "Errors: ${stderr}"
}
println "Script exit code: ${proc.exitValue()}"
// Cleanup
scriptFile.delete()
println "---"
// Alternative: execute inline script via shell
def inline = ["/bin/sh", "-c", '''
COUNT=0
for item in apple banana cherry; do
COUNT=$((COUNT + 1))
echo "${COUNT}. ${item}"
done
echo "Total: ${COUNT} items"
'''].execute()
println inline.text.trim()
Output
Script name: /tmp/groovy-script-8234567890.sh Argument 1: hello Argument 2: world All args: hello world Current dir: /home/developer Date: 2026-03-09 Script exit code: 0 --- 1. apple 2. banana 3. cherry Total: 3 items
What happened here: We created a temporary bash script, made it executable with setExecutable(true), and ran it with arguments. Groovy’s file handling makes this smooth – create the script, execute it, capture output, clean up. The inline approach with /bin/sh -c is handy for quick multi-line shell logic without creating a file.
Example 12: Real-World Automation – Git Status and Disk Usage
What we’re doing: Building a practical system information script that runs multiple commands and formats the output – the kind of thing you’d use in Jenkins, cron jobs, or monitoring dashboards.
Example 12: Real-World Automation
// Helper function to run a command and return trimmed output
def run(String cmd) {
try {
def proc = ["/bin/sh", "-c", cmd].execute()
def out = new StringBuffer()
def err = new StringBuffer()
proc.consumeProcessOutput(out, err)
proc.waitForOrKill(10000)
return [
stdout: out.toString().trim(),
stderr: err.toString().trim(),
exitCode: proc.exitValue()
]
} catch (Exception e) {
return [stdout: "", stderr: e.message, exitCode: -1]
}
}
println "=" * 50
println " SYSTEM INFORMATION REPORT"
println " Generated: ${new Date().format('yyyy-MM-dd HH:mm:ss')}"
println "=" * 50
// Hostname
def hostname = run("hostname")
println "\nHostname: ${hostname.stdout}"
// OS Info
def os = run("uname -s -r -m")
println "OS: ${os.stdout}"
// Uptime
def uptime = run("uptime -p 2>/dev/null || uptime")
println "Uptime: ${uptime.stdout}"
// Disk usage (just root partition)
def disk = run("df -h / | tail -1")
if (disk.exitCode == 0) {
def parts = disk.stdout.split(/\s+/)
println "\nDisk Usage (/):"
println " Total: ${parts.size() > 1 ? parts[1] : 'N/A'}"
println " Used: ${parts.size() > 2 ? parts[2] : 'N/A'}"
println " Free: ${parts.size() > 3 ? parts[3] : 'N/A'}"
println " Use%: ${parts.size() > 4 ? parts[4] : 'N/A'}"
}
// Memory info
def mem = run("free -h 2>/dev/null | grep Mem || echo 'N/A'")
println "\nMemory: ${mem.stdout}"
// Java version
def java = run("java -version 2>&1 | head -1")
println "\nJava: ${java.stdout}"
// Groovy version
def groovy = run("groovy --version 2>&1 | head -1")
println "Groovy: ${groovy.stdout}"
println "\n${'=' * 50}"
println " Report complete."
println "=" * 50
Output
================================================== SYSTEM INFORMATION REPORT Generated: 2026-03-09 14:35:22 ================================================== Hostname: dev-machine OS: Linux 6.1.0-18-amd64 x86_64 Uptime: up 14 days, 3 hours, 22 minutes Disk Usage (/): Total: 100G Used: 45G Free: 51G Use%: 47% Memory: Mem: 15Gi 8.2Gi 4.1Gi 320Mi 3.4Gi 6.9Gi Java: openjdk version "17.0.10" 2024-01-16 Groovy: Groovy Version: 5.0.0 JVM: 17.0.10 ================================================== Report complete. ==================================================
What happened here: This is a real-world automation pattern. We built a reusable run() helper that wraps every command in a try-catch, uses waitForOrKill() for safety, and returns a structured result map. Then we composed multiple system commands into a formatted report. This is the kind of script you’d run in a Jenkins pipeline or cron job to monitor server health.
Pro Tip: In production scripts, always wrap process execution in try-catch blocks. Commands can fail for many reasons – the binary might not exist, permissions might be wrong, or the process might be killed. Defensive coding prevents your entire script from crashing because one command failed.
Edge Cases and Best Practices
Edge Case: Commands with Special Characters
Special Characters in Commands
// WRONG: String form breaks with special characters
// "echo Hello && echo World".execute() // Won't work as expected
// RIGHT: Use shell wrapper for shell features
def proc1 = ["/bin/sh", "-c", "echo Hello && echo World"].execute()
println proc1.text.trim()
// RIGHT: List form for arguments with quotes/spaces
def proc2 = ["echo", "it's a \"quoted\" string"].execute()
println proc2.text.trim()
// Edge case: empty command
try {
"".execute()
} catch (IOException e) {
println "Empty command error: ${e.message}"
}
// Edge case: command not found
try {
"nonexistent_command_xyz".execute()
} catch (IOException e) {
println "Not found error: ${e.message}"
}
Output
Hello World it's a "quoted" string Empty command error: Cannot run program "" Not found error: Cannot run program "nonexistent_command_xyz"
Best Practices
DO:
- Use the list form (
["cmd", "arg"].execute()) for commands with arguments that may contain spaces - Always call
consumeProcessOutput()or read both streams to prevent deadlocks - Use
waitForOrKill(timeout)instead of barewaitFor()in production - Check the exit code – don’t assume a command succeeded just because it didn’t throw
- Use
ProcessBuilderwhen you need environment control, stderr merging, or file redirects - Wrap commands in
/bin/sh -c "..."when you need shell features (pipes, redirects, globbing)
DON’T:
- Use
.texton commands that produce very large output – it loads everything into memory. Use streaming instead. - Read only stdout and ignore stderr – your process may deadlock if the stderr buffer fills up
- Use process execution for tasks that have native Java/Groovy libraries (HTTP requests, file I/O, JSON parsing)
- Build commands by concatenating user input – this is a command injection vulnerability. Always use the list form.
- Forget to call
waitFor()– zombie processes can accumulate and exhaust OS resources
Performance Considerations
Process creation is expensive. Each call to .execute() forks a new OS process, which involves memory allocation, file descriptor duplication, and environment copying. Here are the key performance considerations:
- Process startup cost: On Linux, forking a process typically takes 1-10ms. On Windows, it can be 50-100ms due to heavier process creation overhead. If you’re calling hundreds of commands in a loop, batch them into a single shell script instead.
- Buffer deadlocks: OS pipes have limited buffer sizes (typically 64KB on Linux). If a process writes more than the buffer can hold to stdout or stderr, and nobody is reading, the process blocks. Always consume both streams, even if you don’t need the data.
- Memory with .text: Calling
.textloads the entire output into a String. For a command that dumps a 500MB log file, that’s 500MB in your heap. Use streaming (eachLine) for large outputs. - Thread overhead with consumeProcessOutput: Each call spawns two background threads (one for stdout, one for stderr). In a tight loop, this can create thousands of threads. Use
ProcessBuilder.redirectErrorStream(true)to reduce to one stream. - Zombie processes: If you call
.execute()without ever calling.waitFor()or.text, the process entry lingers in the OS process table. Always clean up.
Common Pitfalls
Pitfall 1: The Deadlock Trap
Problem: Your Groovy script hangs indefinitely when running a command that produces a lot of output.
Pitfall 1: Deadlock
// WRONG - this can deadlock! def proc = "some-command-with-lots-of-output".execute() proc.waitFor() // Blocks here forever if stdout buffer fills up def output = proc.text // Never reached // RIGHT - consume output BEFORE or DURING waitFor def proc2 = "some-command-with-lots-of-output".execute() def out = new StringBuffer() def err = new StringBuffer() proc2.consumeProcessOutput(out, err) // Start reading immediately proc2.waitFor() // Now safe to wait println out
Solution: Always start consuming the output streams before calling waitFor(). The consumeProcessOutput() method starts background threads that drain both streams, preventing the buffer from filling up.
Pitfall 2: String.execute() with Shell Built-ins
Problem: Commands like cd, export, alias, and source don’t work with .execute().
Pitfall 2: Shell Built-ins
// WRONG - cd is a shell built-in, not an executable
try {
"cd /tmp".execute()
} catch (IOException e) {
println "Error: ${e.message}"
}
// RIGHT - use a shell wrapper
def proc = ["/bin/sh", "-c", "cd /tmp && pwd"].execute()
println proc.text.trim()
// Or use ProcessBuilder to set the directory
def pb = new ProcessBuilder(["pwd"])
pb.directory(new File("/tmp"))
println pb.start().inputStream.text.trim()
Output
Error: Cannot run program "cd" /tmp /tmp
Solution: Shell built-ins exist only inside a shell session. They’re not standalone executables, so Runtime.exec() can’t find them. Always wrap shell built-ins in /bin/sh -c "..." (or cmd /c "..." on Windows), or use the Java equivalent like ProcessBuilder.directory().
Pitfall 3: Platform Differences
Problem: A script works on Linux but fails on Windows (or vice versa).
Pitfall 3: Cross-Platform
// Cross-platform helper
def shell(String command) {
def os = System.getProperty("os.name").toLowerCase()
def cmd
if (os.contains("win")) {
cmd = ["cmd", "/c", command]
} else {
cmd = ["/bin/sh", "-c", command]
}
def proc = cmd.execute()
def out = new StringBuffer()
def err = new StringBuffer()
proc.consumeProcessOutput(out, err)
proc.waitForOrKill(30000)
return out.toString().trim()
}
// Works on both Windows and Linux/Mac
println shell("echo Hello from the shell")
// Platform-specific commands
def os = System.getProperty("os.name").toLowerCase()
if (os.contains("win")) {
println shell("dir /b")
} else {
println shell("ls -1")
}
Solution: Build a cross-platform shell() helper that detects the OS and uses the appropriate shell wrapper. On Windows it’s cmd /c; on Linux/Mac it’s /bin/sh -c. Better yet, prefer Java/Groovy APIs (like new File(".").listFiles()) over OS commands when portability matters.
Conclusion
Groovy process execution turns what would be 15+ lines of Java boilerplate into a single expressive line. From "ls -la".execute().text for quick one-liners to full ProcessBuilder setups with environment variables, timeouts, and stream redirects – Groovy has you covered for every automation scenario.
The most important things to remember: always consume both output streams to avoid deadlocks, use waitForOrKill() for safety, prefer the list form for arguments with spaces, and use the shell wrapper (/bin/sh -c) when you need shell features like pipes and redirects.
If you’re building command-line tools, check out our Groovy Command Line -e Option guide for running Groovy scripts directly from the terminal. And for file operations that pair well with process execution, see Groovy File Read, Write, and Delete.
Summary
"command".execute()is the simplest way to run a system command – it returns ajava.lang.Process- Use
["cmd", "arg1", "arg2"].execute()for commands with spaces or special characters in arguments - Always consume both stdout and stderr with
consumeProcessOutput()to prevent deadlocks - Use
waitForOrKill(millis)to set timeouts and prevent runaway processes - Check exit codes –
0means success, anything else is failure ProcessBuildergives you advanced control: environment, working directory, stderr merging, and file redirects- Wrap shell features (pipes, globbing, redirects) in
/bin/sh -corcmd /c
If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.
Up next: Groovy Generics and Type Parameters
Frequently Asked Questions
How do I execute a system command in Groovy?
Use "command".execute() for simple commands or ["command", "arg1", "arg2"].execute() for commands with arguments. Both return a java.lang.Process object. Call .text on the process to get stdout as a String, or use consumeProcessOutput() to capture both stdout and stderr.
Why does my Groovy process execution hang or deadlock?
This happens when the process writes more output than the OS pipe buffer can hold (typically 64KB) and nobody is reading the stream. Always call consumeProcessOutput(stdout, stderr) before waitFor() to drain both streams asynchronously. Alternatively, use ProcessBuilder.redirectErrorStream(true) to merge stderr into stdout and read just one stream.
How do I run a command with a timeout in Groovy?
Use process.waitForOrKill(milliseconds) instead of waitFor(). For example, proc.waitForOrKill(5000) waits up to 5 seconds and kills the process if it hasn’t finished. On Linux, a killed process returns exit code 143 (SIGTERM). Always use this in production to prevent runaway processes.
What is the difference between String.execute() and ProcessBuilder in Groovy?
String.execute() is a Groovy convenience that calls Runtime.exec() under the hood – great for simple one-liners. ProcessBuilder is Java’s more powerful API that lets you merge stderr into stdout with redirectErrorStream(true), redirect output to files, set environment variables as a mutable map, and chain commands. Use ProcessBuilder when you need fine-grained control.
How do I execute commands on both Windows and Linux from Groovy?
Build a cross-platform helper that detects the OS: on Windows, wrap commands in ["cmd", "/c", command]; on Linux/Mac, use ["/bin/sh", "-c", command]. Check the OS with System.getProperty("os.name").toLowerCase(). For better portability, prefer native Java/Groovy APIs (file operations, HTTP clients) over shelling out to OS-specific commands.
Related Posts
Previous in Series: Groovy HTTP Client and REST API Calls
Next in Series: Groovy Generics and Type Parameters
Related Topics You Might Like:
- Groovy Command Line -e Option
- Groovy File Read, Write, and Delete
- Groovy HTTP Client and REST API Calls
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment