Groovy Process Execution – Run System Commands with 12+ Tested Examples

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.

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 a java.lang.Process object
  • ["cmd", "arg1", "arg2"].execute() is safer for commands with spaces or special characters
  • Groovy adds .text, .inputStream, waitFor(), and consumeProcessOutput() to the standard Java Process class
  • For advanced control, you can use Java’s ProcessBuilder alongside 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, or kubectl apply from 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, or pandoc
  • 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:

  1. 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.
  2. Process creation: Groovy calls Runtime.getRuntime().exec(String[]) to create an OS-level process. The JVM forks and executes the command.
  3. Stream setup: Three streams are created – inputStream (stdout from the process), errorStream (stderr), and outputStream (stdin to the process).
  4. 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

MethodReturnsDescription
.textStringReads all stdout and blocks until process finishes
.inputStreamInputStreamRaw stdout stream from the process
.errorStreamInputStreamRaw stderr stream from the process
.waitFor()intBlocks until process finishes, returns exit code
.exitValue()intReturns exit code (throws if not finished)
consumeProcessOutput(out, err)voidAsynchronously reads stdout and stderr into StringBuffers
waitForOrKill(millis)voidWaits 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 bare waitFor() in production
  • Check the exit code – don’t assume a command succeeded just because it didn’t throw
  • Use ProcessBuilder when 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 .text on 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 .text loads 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 a java.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 – 0 means success, anything else is failure
  • ProcessBuilder gives you advanced control: environment, working directory, stderr merging, and file redirects
  • Wrap shell features (pipes, globbing, redirects) in /bin/sh -c or cmd /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.

Previous in Series: Groovy HTTP Client and REST API Calls

Next in Series: Groovy Generics and Type Parameters

Related Topics You Might Like:

This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *