Quick copy-paste recipes to groovy execute shell command calls from scripts. 12 tested examples covering one-liners, output capture, piping, cross-platform tricks, and common CLI tool invocations.
“The best shell scripts are the ones you rewrite in a real language – and Groovy makes that rewrite shorter than the original script.”
Brian Kernighan, Unix Philosophy
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Beginner-Intermediate | Reading Time: 16 minutes
This is not a detailed guide on Groovy’s process API – we already wrote that full guide. This post is a recipe book. You need to groovy execute shell command calls for everyday tasks – run git, call curl, invoke a database dump, ping a server – and you want a working snippet you can copy into your script right now. Each recipe is self-contained: problem, solution, output, done.
We cover 12 practical recipes organized by real-world task: running CLI tools, capturing their output, handling errors, piping commands together, and writing scripts that work on both Linux and Windows. If you need the theory behind Process objects and stream handling, read the Process Execution guide first. If you just need working code, keep reading.
If you are new to Groovy strings and GString interpolation, review our Groovy Strings post first. For file I/O patterns that complement command execution, see Groovy File I/O.
What you’ll learn:
- How to execute commands with
"command".execute()and capture output - How to read standard output and standard error streams
- How to check exit codes and handle process failures
- How to pipe commands together in Groovy
- How to use
ProcessBuilderfor advanced control - How to set timeouts to prevent hanging processes
- How to write platform-safe code that works on Windows and Linux
- How to avoid command injection vulnerabilities
Table of Contents
Quick Reference Table
| Method / API | Purpose | Returns |
|---|---|---|
"cmd".execute() | Execute a command string | Process |
["cmd","arg"].execute() | Execute with argument list (safe for spaces) | Process |
proc.text | Read all stdout as a String | String |
proc.err.text | Read all stderr as a String | String |
proc.exitValue() | Get exit code (blocks until done) | int |
proc.waitFor() | Wait for process to finish | int (exit code) |
proc.waitForOrKill(ms) | Wait with timeout, kill if exceeded | void |
proc.consumeProcessOutput(out, err) | Capture stdout/stderr into StringBuffers | void |
proc1 | proc2 | Pipe stdout of proc1 into stdin of proc2 | Process |
new ProcessBuilder(cmd) | Full control: env vars, working dir, redirects | ProcessBuilder |
12 Practical Examples
Example 1: Basic Command Execution
What we’re doing: Running a simple command and capturing its output using Groovy’s execute() method on a String.
Example 1: Basic Command Execution
// The simplest way to run a command in Groovy
def proc = "echo Hello from Groovy".execute()
// .text reads all of stdout and waits for the process to finish
def output = proc.text
println "Output: ${output.trim()}"
// You can also get the exit code
println "Exit code: ${proc.exitValue()}"
// One-liner version
println "Java version: ${'java -version'.execute().err.text.readLines()[0]}"
Output
Output: Hello from Groovy Exit code: 0 Java version: openjdk version "17.0.10" 2024-01-16
What happened here: Groovy adds an execute() method to the String class through GDK (Groovy Development Kit) extensions. Calling "echo Hello from Groovy".execute() internally calls Runtime.getRuntime().exec() and returns a java.lang.Process object. The .text property (another GDK extension on InputStream) reads the entire standard output stream and blocks until the process completes. Note that java -version prints to stderr, not stdout – that is why we used .err.text for the last line.
Example 2: Capturing Standard Output and Standard Error Separately
What we’re doing: Reading both stdout and stderr streams using consumeProcessOutput() to avoid blocking and deadlocks.
Example 2: Capturing stdout and stderr
// Run a command that produces both stdout and stderr
def proc = "groovy -e \"println 'STDOUT message'; System.err.println 'STDERR message'\"".execute()
// Use StringBuffer to capture both streams concurrently
def stdout = new StringBuffer()
def stderr = new StringBuffer()
// consumeProcessOutput reads both streams on background threads
proc.consumeProcessOutput(stdout, stderr)
// Wait for the process to finish
proc.waitFor()
println "STDOUT: ${stdout.toString().trim()}"
println "STDERR: ${stderr.toString().trim()}"
println "Exit code: ${proc.exitValue()}"
Output
STDOUT: STDOUT message STDERR: STDERR message Exit code: 0
What happened here: consumeProcessOutput() is critical for production code. It spawns two background threads to drain stdout and stderr simultaneously. Without it, if a process writes enough data to fill the OS pipe buffer (typically 64KB), the process will block waiting for someone to read the buffer – and your Groovy script will block waiting for the process to finish. That is a classic deadlock. Always use consumeProcessOutput() when you need both streams or when the output volume is unpredictable.
Example 3: Using a List for Commands with Arguments
What we’re doing: Passing commands as a list of strings to handle arguments with spaces and special characters correctly.
Example 3: List-Based Command Execution
// Problem: String.execute() splits on whitespace - breaks with file paths containing spaces
// This would fail: "cat /path/to/my file.txt".execute()
// Solution: Use a List - each element is one argument, no splitting involved
def command = ['echo', 'Hello World', 'with spaces']
def proc = command.execute()
println "List output: ${proc.text.trim()}"
// Practical example: running groovy with arguments
def groovyCmd = ['groovy', '-e', 'println "2 + 2 = " + (2 + 2)']
def groovyProc = groovyCmd.execute()
println "Groovy output: ${groovyProc.text.trim()}"
// Listing directory contents
def listCmd = ['ls', '-la', '/tmp']
def listProc = listCmd.execute()
def lines = listProc.text.readLines()
println "Total entries in /tmp: ${lines.size()}"
println "First line: ${lines[0]}"
Output
List output: Hello World with spaces Groovy output: 2 + 2 = 4 Total entries in /tmp: 15 First line: total 48
What happened here: When you call "some command".execute(), Groovy splits the string on whitespace to create the argument array. This breaks when arguments contain spaces (like file paths). The list form ['cmd', 'arg1', 'arg2'].execute() passes each list element as a separate argument to the OS – no splitting, no quoting headaches. Always prefer the list form when arguments might contain spaces or when you are building commands dynamically from variables.
Example 4: Checking Exit Codes for Success or Failure
What we’re doing: Using exit codes to determine whether a command succeeded and implementing proper error handling.
Example 4: Exit Code Handling
// Helper method to run a command and check the result
def runCommand(String command) {
def proc = command.execute()
def stdout = new StringBuffer()
def stderr = new StringBuffer()
proc.consumeProcessOutput(stdout, stderr)
proc.waitFor()
def exitCode = proc.exitValue()
if (exitCode == 0) {
println "SUCCESS [${command}]"
println " Output: ${stdout.toString().trim()}"
} else {
println "FAILED [${command}] with exit code: ${exitCode}"
println " Error: ${stderr.toString().trim()}"
}
return exitCode
}
// Test with a command that succeeds
runCommand('echo Everything is fine')
println '---'
// Test with a command that fails
runCommand('ls /nonexistent_directory_12345')
println '---'
// Test with false (always exits with code 1)
runCommand('false')
Output
SUCCESS [echo Everything is fine] Output: Everything is fine --- FAILED [ls /nonexistent_directory_12345] with exit code: 2 Error: ls: cannot access '/nonexistent_directory_12345': No such file or directory --- FAILED [false] with exit code: 1 Error:
What happened here: The Unix convention is that exit code 0 means success and any non-zero value means failure. Different commands use different non-zero codes to indicate specific error types – ls uses 2 for “cannot access”, while false always returns 1. Always call waitFor() before exitValue(), otherwise exitValue() throws an IllegalThreadStateException if the process is still running. Our helper method demonstrates the production pattern: capture both streams, wait for completion, then check the exit code.
Example 5: Piping Commands Together
What we’re doing: Chaining commands using Groovy’s pipe operator | to send one command’s output to another command’s input.
Example 5: Piping Commands
// Groovy overloads the | operator on Process for piping
// Equivalent to: echo "apple\nbanana\ncherry\napricot" | grep "ap" | sort
def result = ['echo', 'apple\nbanana\ncherry\napricot'].execute() |
'grep ap'.execute() |
'sort'.execute()
println "Piped result:"
println result.text
// Count lines in a file using pipe
def lineCount = 'cat /etc/passwd'.execute() | 'wc -l'.execute()
println "Lines in /etc/passwd: ${lineCount.text.trim()}"
// More complex pipe: find unique shells used in /etc/passwd
def shells = 'cat /etc/passwd'.execute() |
['awk', '-F:', '{print $7}'].execute() |
'sort'.execute() |
'uniq -c'.execute() |
'sort -rn'.execute()
println "\nShells in use:"
println shells.text
Output
Piped result:
apple
apricot
Lines in /etc/passwd: 32
Shells in use:
18 /usr/sbin/nologin
7 /bin/false
5 /bin/bash
1 /bin/sync
1 /usr/bin/git-shell
What happened here: Groovy overloads the | (pipe) operator on the Process class. When you write proc1 | proc2, Groovy connects the standard output of proc1 to the standard input of proc2 using a background thread that pumps data between them. You can chain as many pipes as you want. The final | expression returns the last Process in the chain, so you call .text on that to get the final output. Note that piping in Groovy does not use the system shell – each command is a separate process wired together by Groovy, which means shell-specific features like globbing or redirection operators (>, >>) do not work here.
Example 6: Sending Input to a Process (stdin)
What we’re doing: Writing data to a process’s standard input stream – useful for commands that expect input from stdin.
Example 6: Sending Input to a Process
// Send input to 'cat' which echoes it back
def proc = 'cat'.execute()
// Write to the process's stdin
proc.outputStream.withWriter { writer ->
writer.write('Line 1: Hello\n')
writer.write('Line 2: from Groovy\n')
writer.write('Line 3: via stdin\n')
}
// withWriter auto-closes the stream, signaling EOF
println "Cat echoed back:"
println proc.text
// Send data to sort
def sortProc = 'sort'.execute()
sortProc.outputStream.withWriter { writer ->
writer << 'banana\n'
writer << 'apple\n'
writer << 'cherry\n'
writer << 'date\n'
}
println "Sorted:"
println sortProc.text
// Using the << operator shorthand
def grepProc = ['grep', 'error'].execute()
grepProc.outputStream << '''info: all good
error: disk full
info: retrying
error: connection timeout
info: recovered
'''
grepProc.outputStream.close() // Must close to signal EOF
println "Grep found:"
println grepProc.text
Output
Cat echoed back: Line 1: Hello Line 2: from Groovy Line 3: via stdin Sorted: apple banana cherry date Grep found: error: disk full error: connection timeout
What happened here: The Process.outputStream property is the process’s input stream (the naming is from your program’s perspective – you are outputting data to the process). The withWriter closure ensures the stream is closed when done, which sends EOF to the child process. Without closing the stream, commands like sort and cat will wait indefinitely for more input. You can also use the << (left-shift) operator for appending, but remember to call .close() explicitly afterward.
Example 7: Using ProcessBuilder for Advanced Control
What we’re doing: Using Java’s ProcessBuilder to set environment variables, change the working directory, and redirect error streams.
Example 7: ProcessBuilder
// ProcessBuilder gives you full control over the process environment
def pb = new ProcessBuilder(['bash', '-c', 'echo "User: $APP_USER, Env: $APP_ENV, Dir: $(pwd)"'])
// Set environment variables
pb.environment().put('APP_USER', 'groovy_admin')
pb.environment().put('APP_ENV', 'staging')
// Set the working directory
pb.directory(new File('/tmp'))
// Merge stderr into stdout (single stream)
pb.redirectErrorStream(true)
// Start the process
def proc = pb.start()
println proc.text.trim()
// Another example: running a script with a custom PATH
def pb2 = new ProcessBuilder(['bash', '-c', 'echo "PATH has $(echo $PATH | tr \":\" \"\\n\" | wc -l) entries"'])
pb2.redirectErrorStream(true)
def proc2 = pb2.start()
println proc2.text.trim()
// ProcessBuilder with inherited I/O (for interactive commands - output goes to console)
def pb3 = new ProcessBuilder(['echo', 'This goes directly to the console'])
pb3.redirectErrorStream(true)
def proc3 = pb3.start()
println "Direct: ${proc3.text.trim()}"
proc3.waitFor()
println "ProcessBuilder exit code: ${proc3.exitValue()}"
Output
User: groovy_admin, Env: staging, Dir: /tmp PATH has 8 entries Direct: This goes directly to the console ProcessBuilder exit code: 0
What happened here: ProcessBuilder is Java’s full-featured API for process management. The key advantages over "cmd".execute() are: (1) you can set environment variables via .environment(), (2) you can change the working directory via .directory(), (3) you can merge stderr into stdout with .redirectErrorStream(true) so you only need to read one stream, and (4) you can redirect I/O to files. When you need any of these features, use ProcessBuilder. For simple one-off commands, "cmd".execute() is more concise.
Example 8: Setting Timeouts to Prevent Hanging
What we’re doing: Using waitForOrKill() and Java’s Process.waitFor(timeout, unit) to kill processes that take too long.
Example 8: Process Timeouts
import java.util.concurrent.TimeUnit
// Groovy's waitForOrKill - waits N milliseconds, then destroys the process
println "=== waitForOrKill ==="
def slowProc = 'sleep 10'.execute()
slowProc.waitForOrKill(2000) // Wait 2 seconds max
println "Process alive after kill: ${slowProc.alive}"
println "Exit code: ${slowProc.exitValue()}" // 143 = killed (SIGTERM)
println ""
// Java's waitFor with timeout - returns true if finished, false if timed out
println "=== waitFor with timeout ==="
def fastProc = 'echo fast'.execute()
boolean finished = fastProc.waitFor(5, TimeUnit.SECONDS)
println "Fast command finished in time: ${finished}"
println "Output: ${fastProc.text.trim()}"
println ""
// Practical timeout wrapper
def runWithTimeout(List<String> command, long timeoutMs) {
def proc = command.execute()
def stdout = new StringBuffer()
def stderr = new StringBuffer()
proc.consumeProcessOutput(stdout, stderr)
boolean finished = proc.waitFor(timeoutMs, TimeUnit.MILLISECONDS)
if (!finished) {
proc.destroyForcibly()
return [success: false, error: "Command timed out after ${timeoutMs}ms"]
}
return [
success: proc.exitValue() == 0,
output : stdout.toString().trim(),
error : stderr.toString().trim(),
code : proc.exitValue()
]
}
def result1 = runWithTimeout(['echo', 'quick task'], 5000)
println "Quick: ${result1}"
def result2 = runWithTimeout(['sleep', '30'], 1000)
println "Slow: ${result2}"
Output
=== waitForOrKill === Process alive after kill: false Exit code: 143 === waitFor with timeout === Fast command finished in time: true Output: fast Quick: [success:true, output:quick task, error:, code:0] Slow: [success:false, error:Command timed out after 1000ms]
What happened here: Groovy’s waitForOrKill(millis) is the simplest timeout mechanism – it waits the specified time and then sends SIGTERM to the process. The exit code 143 means the process was killed (128 + 15, where 15 is SIGTERM). Java’s waitFor(timeout, unit) gives you more control – it returns a boolean so you can decide what to do when the timeout expires. The runWithTimeout wrapper shows a production pattern: capture output on background threads, wait with a timeout, kill if needed, and return a structured result map.
Example 9: Platform-Safe Command Execution
What we’re doing: Writing commands that work on both Windows and Unix/Linux by detecting the OS and choosing the right shell.
Example 9: Cross-Platform Commands
// Detect the operating system
def isWindows = System.getProperty('os.name').toLowerCase().contains('windows')
println "Running on: ${System.getProperty('os.name')}"
println "Is Windows: ${isWindows}"
// Platform-safe shell command executor
def shell(String command) {
def isWin = System.getProperty('os.name').toLowerCase().contains('windows')
def cmd = isWin ? ['cmd', '/c', command] : ['bash', '-c', command]
def proc = cmd.execute()
def stdout = new StringBuffer()
def stderr = new StringBuffer()
proc.consumeProcessOutput(stdout, stderr)
proc.waitFor()
return [out: stdout.toString().trim(), err: stderr.toString().trim(), code: proc.exitValue()]
}
// These commands work on both platforms via the shell wrapper
def result = shell('echo Hello from the shell')
println "Shell says: ${result.out}"
// Platform-specific commands with a single interface
def dirResult = shell(isWindows ? 'dir /b' : 'ls -1')
println "\nDirectory listing (first 3):"
dirResult.out.readLines().take(3).each { println " $it" }
// Get current user
def userResult = shell(isWindows ? 'echo %USERNAME%' : 'whoami')
println "\nCurrent user: ${userResult.out}"
// Get system uptime
def uptimeResult = shell(isWindows ? 'net statistics workstation | find "since"' : 'uptime -p')
println "Uptime: ${uptimeResult.out}"
Output
Running on: Linux Is Windows: false Shell says: Hello from the shell Directory listing (first 3): build.gradle src README.md Current user: developer Uptime: up 12 days, 3 hours, 45 minutes
What happened here: The most common cross-platform pitfall is executing shell-specific commands directly. On Linux, "ls".execute() works because ls is a real binary. But shell features like pipes, globbing, and environment variable expansion require a shell. By wrapping commands in ['bash', '-c', command] (or ['cmd', '/c', command] on Windows), you get consistent shell behavior on every platform. The shell() helper function shown here is a reusable pattern you can drop into any Groovy project.
Example 10: Streaming Output Line by Line
What we’re doing: Reading process output line by line as it arrives, instead of waiting for the entire output – useful for long-running commands.
Example 10: Streaming Output
// Simulate a long-running command that outputs lines over time
def proc = ['bash', '-c', 'for i in 1 2 3 4 5; do echo "Processing item $i"; sleep 0.2; done'].execute()
// Read output line by line as it arrives
println "=== Streaming output ==="
proc.inputStream.eachLine { line ->
println "[${new Date().format('HH:mm:ss.SSS')}] $line"
}
proc.waitFor()
println "Done. Exit code: ${proc.exitValue()}"
println ""
// Stream with a line counter and progress tracking
def proc2 = ['bash', '-c', 'seq 1 10'].execute()
def lineCount = 0
proc2.inputStream.eachLine { line ->
lineCount++
if (lineCount % 3 == 0) {
println "Progress: processed $lineCount lines (current: $line)"
}
}
proc2.waitFor()
println "Total lines processed: $lineCount"
println ""
// BufferedReader approach for more control
def proc3 = ['bash', '-c', 'echo "header"; echo "---"; echo "data1"; echo "data2"; echo "data3"'].execute()
def reader = proc3.inputStream.newReader()
def header = reader.readLine()
def separator = reader.readLine()
println "Header: $header"
println "Separator: $separator"
println "Data lines:"
reader.eachLine { println " > $it" }
proc3.waitFor()
Output
=== Streaming output === [14:23:01.112] Processing item 1 [14:23:01.315] Processing item 2 [14:23:01.518] Processing item 3 [14:23:01.721] Processing item 4 [14:23:01.924] Processing item 5 Done. Exit code: 0 Progress: processed 3 lines (current: 3) Progress: processed 6 lines (current: 6) Progress: processed 9 lines (current: 9) Total lines processed: 10 Header: header Separator: --- Data lines: > data1 > data2 > data3
What happened here: Instead of calling .text (which buffers everything in memory), we use .inputStream.eachLine{} to process output as it arrives. This is essential for long-running processes – you see output in real time and you do not accumulate a massive string in memory. The timestamps in the first example show that each line arrives approximately 200ms apart, matching the sleep 0.2 in the shell command. The BufferedReader approach gives even more control – you can read specific lines (like headers) before processing the rest.
Example 11: Executing Shell Commands with Redirections and Subshells
What we’re doing: Running complex shell commands that use redirections, subshells, and shell built-in features by invoking through bash -c.
Example 11: Shell Commands with Redirections
// Direct execute() does NOT interpret shell syntax
// "echo hello > /tmp/test.txt".execute() // This would NOT create a file!
// You need bash -c for shell features
def tmpFile = File.createTempFile('groovy_cmd_', '.txt')
// Redirect output to a file using bash
['bash', '-c', "echo 'Written by Groovy' > ${tmpFile.absolutePath}"].execute().waitFor()
println "File contents: ${tmpFile.text.trim()}"
// Append to the file
['bash', '-c', "echo 'Second line' >> ${tmpFile.absolutePath}"].execute().waitFor()
println "After append: ${tmpFile.readLines()}"
// Use subshell and command substitution
def proc = ['bash', '-c', 'echo "Today is $(date +%Y-%m-%d) and uptime is $(uptime -p)"'].execute()
println "\n${proc.text.trim()}"
// Use heredoc in bash
def heredocCmd = ['bash', '-c', '''cat <<HEREDOC
Name: Groovy Developer
Role: Backend Engineer
Language: Groovy 5.x
HEREDOC''']
println "\nHeredoc output:"
println heredocCmd.execute().text
// Conditional execution with && and ||
def condProc = ['bash', '-c', 'test -f /etc/passwd && echo "passwd exists" || echo "passwd missing"'].execute()
println "Conditional: ${condProc.text.trim()}"
// Cleanup
tmpFile.delete()
Output
File contents: Written by Groovy After append: [Written by Groovy, Second line] Today is 2026-03-12 and uptime is up 12 days, 3 hours, 45 minutes Heredoc output: Name: Groovy Developer Role: Backend Engineer Language: Groovy 5.x Conditional: passwd exists
What happened here: Groovy’s .execute() does not invoke a shell – it calls the OS exec() directly. This means shell features like > (redirect), | (shell pipe), $() (command substitution), &&/|| (conditional), and heredocs do not work unless you wrap them in ['bash', '-c', 'your command']. This is the single most common confusion when executing commands in Groovy. Think of bash -c as “give me a real shell to interpret this string.”
Example 12: Security – Avoiding Command Injection
What we’re doing: Demonstrating how command injection happens and how to prevent it with list-based execution and input validation.
Example 12: Preventing Command Injection
// === DANGEROUS: String interpolation with user input ===
def userInput = 'myfile.txt; rm -rf /tmp/important' // Malicious input!
// BAD: This would execute BOTH commands if run through bash
// ['bash', '-c', "cat ${userInput}"].execute() // NEVER DO THIS!
println "BAD (simulated): bash -c 'cat ${userInput}'"
println " ^ This would run: cat myfile.txt AND rm -rf /tmp/important"
println ""
// === SAFE: Use list form - arguments are passed directly, not interpreted ===
def safeUserInput = 'myfile.txt; rm -rf /tmp/important'
// GOOD: The entire string is treated as a single filename argument
// The semicolon and rm command are NOT interpreted - they are just characters in the filename
def safeCmd = ['cat', safeUserInput]
println "SAFE: ${safeCmd}"
println " ^ This tries to open a file literally named '${safeUserInput}'"
// Would fail with: No such file or directory (which is correct!)
println ""
// === Input validation helper ===
def sanitizeFilename(String input) {
// Allow only alphanumeric, dots, hyphens, underscores
if (!(input ==~ /^[a-zA-Z0-9._-]+$/)) {
throw new IllegalArgumentException("Invalid filename: ${input}")
}
return input
}
// Test the validator
['report.csv', 'my-file_v2.txt', '../../../etc/passwd', 'file; rm -rf /'].each { filename ->
try {
def safe = sanitizeFilename(filename)
println "VALID: '${filename}'"
} catch (IllegalArgumentException e) {
println "BLOCKED: '${filename}' - ${e.message}"
}
}
println ""
// === Safe pattern: ProcessBuilder with explicit arguments ===
def safeSearch(String directory, String pattern) {
// Validate inputs
def dir = new File(directory)
if (!dir.isDirectory()) {
return "Error: ${directory} is not a valid directory"
}
// List form: each argument is separate - no shell interpretation
def proc = ['grep', '-r', '-l', pattern, directory].execute()
def stdout = new StringBuffer()
def stderr = new StringBuffer()
proc.consumeProcessOutput(stdout, stderr)
proc.waitFor()
return stdout.toString().trim() ?: 'No matches found'
}
println "Safe search result: ${safeSearch('/tmp', 'hello')}"
Output
BAD (simulated): bash -c 'cat myfile.txt; rm -rf /tmp/important' ^ This would run: cat myfile.txt AND rm -rf /tmp/important SAFE: [cat, myfile.txt; rm -rf /tmp/important] ^ This tries to open a file literally named 'myfile.txt; rm -rf /tmp/important' VALID: 'report.csv' VALID: 'my-file_v2.txt' BLOCKED: '../../../etc/passwd' - Invalid filename: ../../../etc/passwd BLOCKED: 'file; rm -rf /' - Invalid filename: file; rm -rf / Safe search result: No matches found
What happened here: Command injection is the most dangerous pitfall in process execution. When you pass user input into ['bash', '-c', "command ${userInput}"], the shell interprets the entire string – including any malicious commands the user injected. The fix is simple: (1) Never pass user input through a shell. Use the list form ['command', userArg1, userArg2] where each argument is a separate list element – the OS treats them as literal arguments, not shell commands. (2) Validate and sanitize all user input before using it in commands. (3) Use the principle of least privilege – run your Groovy process with minimal OS permissions.
Common Pitfalls
Deadlock from Unread Streams
The most common production bug. If a process produces enough output to fill the OS pipe buffer (~64KB) and nobody is reading from it, the process blocks. Meanwhile, your Groovy code is calling waitFor() – which blocks until the process finishes. Both sides wait forever.
Deadlock from Unread Streams
// BAD - can deadlock if output exceeds pipe buffer
def proc = 'find / -name "*.log"'.execute()
proc.waitFor() // Blocks forever if output fills the buffer
def output = proc.text // Never reached
// GOOD - consume output on background threads, then wait
def proc2 = 'find / -name "*.log"'.execute()
def stdout = new StringBuffer()
def stderr = new StringBuffer()
proc2.consumeProcessOutput(stdout, stderr)
proc2.waitFor() // Safe - streams are being drained in background
println "Found ${stdout.toString().readLines().size()} log files"
Confusing Process Stream Names
The Process object’s stream names are from your program’s perspective, not the child process’s perspective. This confuses nearly everyone the first time.
Process Stream Name Confusion
// proc.inputStream = child's STDOUT (you read FROM the child's output) // proc.errorStream = child's STDERR (you read FROM the child's errors) // proc.outputStream = child's STDIN (you write TO the child's input) // BAD - trying to read child's output from outputStream // proc.outputStream.text // This is the child's STDIN, not its output! // GOOD def proc = 'echo hello'.execute() def childOutput = proc.inputStream.text // Read child's stdout def childErrors = proc.errorStream.text // Read child's stderr // proc.outputStream << 'data' // Write to child's stdin
Expecting Shell Features Without a Shell
Groovy’s .execute() calls the OS directly – no shell is involved. Wildcards, pipes, redirects, and environment variable expansion do not work.
Missing Shell Features
// BAD - none of these shell features work with direct execute()
// "ls *.groovy".execute() // Glob won't expand
// "echo $HOME".execute() // Variable won't expand
// "cat file.txt > output.txt".execute() // Redirect won't work
// GOOD - wrap in bash -c for shell interpretation
['bash', '-c', 'ls *.groovy'].execute()
['bash', '-c', 'echo $HOME'].execute()
['bash', '-c', 'cat file.txt > output.txt'].execute()
// BETTER - use Groovy native alternatives when possible
new File('.').listFiles().findAll { it.name.endsWith('.groovy') } // Instead of ls *.groovy
System.getenv('HOME') // Instead of echo $HOME
new File('output.txt').text = new File('file.txt').text // Instead of cat > redirect
Best Practices
After working through all 12 examples, here are the guidelines that will keep your Groovy command execution code safe and reliable.
- DO use the list form
['cmd', 'arg1', 'arg2'].execute()instead of string form for any command with arguments – especially when arguments come from variables or user input. - DO always call
consumeProcessOutput()or read streams on separate threads before callingwaitFor()– prevents deadlocks on large output. - DO always set a timeout using
waitForOrKill(millis)orwaitFor(timeout, unit)– never let a process run indefinitely in production code. - DO check exit codes – a process that produces output might still have failed. Exit code 0 is the only reliable success indicator.
- DO use
ProcessBuilderwhen you need environment variables, working directory changes, or stream redirects. - DON’T interpolate user input into shell command strings – this creates command injection vulnerabilities. Use the list form instead.
- DON’T assume shell features work with
.execute()– wrap in['bash', '-c', cmd]or['cmd', '/c', cmd]when you need shell interpretation. - DON’T use
.textfor large output – stream line by line with.inputStream.eachLine{}to avoid memory issues. - DON’T forget to close the
outputStreamwhen writing to a process’s stdin – the process will hang waiting for EOF. - DON’T assume
exitValue()is safe to call without first callingwaitFor()– it throws an exception if the process is still running.
Conclusion
Groovy makes external command execution remarkably concise – a one-liner where Java needs a dozen lines. We started with the basics of "command".execute(), captured stdout and stderr safely with consumeProcessOutput(), piped commands together with the | operator, and used ProcessBuilder for fine-grained control over environment and working directory. We also covered the critical production concerns: timeouts to prevent hanging processes, cross-platform patterns for Windows and Linux, streaming output for long-running commands, and security practices to prevent command injection.
The bottom line: Groovy’s convenience is powerful but hides real complexity. Always consume both output streams to avoid deadlocks, always set timeouts, always use the list form for dynamic arguments, and never pass unsanitized user input through a shell. Follow these rules and you will have reliable, production-safe process execution in your Groovy scripts.
For related topics, see our Groovy File I/O post for reading and writing files (often the next step after running commands), Groovy Strings for building command arguments with GStrings, and Groovy Shell for interactive Groovy execution.
Up next: @CompileStatic & @TypeChecked in Groovy
Frequently Asked Questions
How do I execute a system command in Groovy?
Use the execute() method on a String: "ls -la".execute(). This returns a Process object. Call .text on it to get the standard output as a String. For commands with arguments that contain spaces, use the list form: ['ls', '-la', '/my path'].execute(). Always call consumeProcessOutput() before waitFor() for production code to avoid deadlocks.
How do I capture both stdout and stderr from a Groovy command?
Use consumeProcessOutput() with two StringBuffer objects: def stdout = new StringBuffer(); def stderr = new StringBuffer(); proc.consumeProcessOutput(stdout, stderr); proc.waitFor(). This drains both streams on background threads, preventing deadlocks. After waitFor() returns, read the results with stdout.toString() and stderr.toString().
How do I pipe commands in Groovy?
Use the | (pipe) operator between Process objects: 'cat /etc/passwd'.execute() | 'grep root'.execute() | 'wc -l'.execute(). Groovy overloads the pipe operator on the Process class to connect stdout of one process to stdin of the next. Call .text on the last process in the chain to get the final output. Note that this is Groovy piping, not shell piping – shell-specific features like globbing are not available.
How do I set a timeout for a Groovy process execution?
Use Groovy’s waitForOrKill(millis) method: proc.waitForOrKill(5000) waits 5 seconds then kills the process. For more control, use Java’s proc.waitFor(5, TimeUnit.SECONDS) which returns a boolean – true if the process finished, false if it timed out. On timeout, call proc.destroyForcibly() to kill the process.
How do I avoid command injection when executing commands in Groovy?
Never interpolate user input into shell command strings like ['bash', '-c', "cat ${userInput}"]. Instead, use the list form where each argument is a separate element: ['cat', userInput].execute(). The list form passes arguments directly to the OS without shell interpretation, so special characters like ;, |, and && are treated as literal text, not as shell operators. Additionally, validate and sanitize all user input before using it in commands.
Why does my Groovy command execution hang or deadlock?
The most common cause is not reading the process’s output streams. When a process writes enough data to fill the OS pipe buffer (typically 64KB), it blocks waiting for someone to read. If your Groovy code is simultaneously calling waitFor(), both sides block forever. Fix this by calling proc.consumeProcessOutput(stdout, stderr) before proc.waitFor(). This starts background threads that continuously drain both streams.
Related Posts
Previous in Series: Groovy Date and Time – Working with Dates in Groovy
Next in Series: @CompileStatic & @TypeChecked in Groovy
Related Topics You Might Like:
- Groovy File I/O – Reading and Writing Files
- Groovy Strings – GStrings, Interpolation, and Multiline
- Groovy Shell – Interactive Groovy Programming
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment