Groovy Execute Shell Command – Quick Recipes with 10+ Examples

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 ProcessBuilder for 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

Quick Reference Table

Method / APIPurposeReturns
"cmd".execute()Execute a command stringProcess
["cmd","arg"].execute()Execute with argument list (safe for spaces)Process
proc.textRead all stdout as a StringString
proc.err.textRead all stderr as a StringString
proc.exitValue()Get exit code (blocks until done)int
proc.waitFor()Wait for process to finishint (exit code)
proc.waitForOrKill(ms)Wait with timeout, kill if exceededvoid
proc.consumeProcessOutput(out, err)Capture stdout/stderr into StringBuffersvoid
proc1 | proc2Pipe stdout of proc1 into stdin of proc2Process
new ProcessBuilder(cmd)Full control: env vars, working dir, redirectsProcessBuilder

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 calling waitFor() – prevents deadlocks on large output.
  • DO always set a timeout using waitForOrKill(millis) or waitFor(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 ProcessBuilder when 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 .text for large output – stream line by line with .inputStream.eachLine{} to avoid memory issues.
  • DON’T forget to close the outputStream when writing to a process’s stdin – the process will hang waiting for EOF.
  • DON’T assume exitValue() is safe to call without first calling waitFor() – 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.

Previous in Series: Groovy Date and Time – Working with Dates in Groovy

Next in Series: @CompileStatic & @TypeChecked in Groovy

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 *