Of course. Executing a command from a Java application on a Linux system is a common task. The primary and most modern way to do this in Java is by using the java.lang.ProcessBuilder class.

Here’s a comprehensive guide covering:
- The Modern Approach:
ProcessBuilder(Recommended) - The Legacy Approach:
Runtime.exec()(For understanding old code) - Handling Input, Output, and Errors
- Complete, Runnable Examples
- Best Practices and Common Pitfalls
The Modern Approach: ProcessBuilder (Recommended)
ProcessBuilder was introduced in Java 5 and is the preferred way to start processes. It offers more control and is generally easier to use than Runtime.exec().
Key Steps:
- Create a
ProcessBuilderinstance: Provide the command and its arguments as a list of strings. This is the most robust method, as it handles argument escaping correctly. - Set up working directory and environment (optional): You can specify the directory where the command should run and modify the environment variables.
- Start the process: This executes the command and returns a
Processobject. - Wait for the process to finish: Call
process.waitFor()to block until the command completes. This is crucial to get the correct exit code. - Get the result: Check the exit code and read the standard output and standard error streams.
Example: A Simple Command (ls -l)
This example runs the ls -l command and prints its output to the console.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class ProcessBuilderExample {
public static void main(String[] args) {
// 1. Define the command and its arguments as a list of strings.
// This is safer than using a single string with spaces.
ProcessBuilder pb = new ProcessBuilder("ls", "-l");
// Optional: Set the working directory for the process
// pb.directory(new File("/path/to/your/directory"));
try {
// 2. Start the process
Process process = pb.start();
// 3. Read the standard output stream
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
System.out.println("--- Command Output ---");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
System.out.println("----------------------");
// 4. Wait for the process to complete and get the exit code
int exitCode = process.waitFor();
System.out.println("Process exited with code: " + exitCode);
} catch (IOException e) {
System.err.println("Error executing command: " + e.getMessage());
e.printStackTrace();
} catch (InterruptedException e) {
System.err.println("Process was interrupted: " + e.getMessage());
e.printStackTrace();
// Restore the interrupted status
Thread.currentThread().interrupt();
}
}
}
The Legacy Approach: Runtime.exec()
Before ProcessBuilder, Runtime.exec() was the only option. It's still functional but has several pitfalls, especially when handling commands with complex arguments or when you need to read both output and error streams simultaneously.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
public class RuntimeExecExample {
public static void main(String[] args) {
try {
// The command is passed as a single string. This can be problematic
// with arguments containing spaces or special characters.
Process process = Runtime.getRuntime().exec("ls -l");
// Read the output
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
System.out.println("--- Command Output ---");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
System.out.println("----------------------");
int exitCode = process.waitFor();
System.out.println("Process exited with code: " + exitCode);
} catch (IOException e) {
System.err.println("Error executing command: " + e.getMessage());
} catch (InterruptedException e) {
System.err.println("Process was interrupted: " + e.getMessage());
Thread.currentThread().interrupt();
}
}
}
Why ProcessBuilder is better:
- Argument Handling:
ProcessBuilder's list-based constructor avoids shell injection and complex parsing issues thatRuntime.exec()can have. - Control: You can easily redirect streams and set the working directory.
- Clarity: The API is generally cleaner and more intuitive.
Handling Input, Output, and Errors (Crucial!)
A common and dangerous mistake is not consuming the standard output and error streams of a process. If these buffers fill up, the process can hang, waiting for space to write more data. This is known as a deadlock.
The Solution: Always read from both InputStream (stdout) and ErrorStream (stderr) in separate threads.
Example with Input, Output, and Error Handling
This example demonstrates how to:
- Provide input to a process (
echo "Hello World"). - Read its standard output.
- Read its standard error.
- Get the final exit code.
import java.io.*;
public class ProcessIOHandling {
public static void main(String[] args) {
// Command to run: 'sh -c' allows us to pipe commands
ProcessBuilder pb = new ProcessBuilder("sh", "-c", "echo \"Hello from Java\" && echo \"This is an error\" >&2");
try {
Process process = pb.start();
// --- READING OUTPUT (stdout) ---
// Use a separate thread to read stdout to prevent deadlock
Thread outputThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("[STDOUT] " + line);
}
} catch (IOException e) {
System.err.println("Error reading stdout: " + e.getMessage());
}
});
// --- READING ERRORS (stderr) ---
// Use another thread to read stderr
Thread errorThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.err.println("[STDERR] " + line);
}
} catch (IOException e) {
System.err.println("Error reading stderr: " + e.getMessage());
}
});
// Start the reader threads
outputThread.start();
errorThread.start();
// --- WRITING INPUT (stdin) ---
// If you need to send input to the process
// try (OutputStream os = process.getOutputStream()) {
// os.write("input data\n".getBytes());
// os.flush();
// }
// Wait for the process to complete
int exitCode = process.waitFor();
// Wait for the reader threads to finish
outputThread.join();
errorThread.join();
System.out.println("\nProcess finished with exit code: " + exitCode);
} catch (IOException | InterruptedException e) {
System.err.println("An error occurred: " + e.getMessage());
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
}
}
}
Complete, Runnable Examples
Example 1: Running a script with arguments
Let's say you have a script greet.sh:
#!/bin/bash # greet.sh echo "Hello, $1! The time is $(date)."
Make it executable: chmod +x greet.sh
Now, run it from Java:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class RunScript {
public static void main(String[] args) {
// Command and arguments
ProcessBuilder pb = new ProcessBuilder("./greet.sh", "Alice");
try {
Process process = pb.start();
// Read output
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
int exitCode = process.waitFor();
System.out.println("Script exited with code: " + exitCode);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
Example 2: Checking Disk Space (df -h)
import java.io.*;
public class DiskSpaceChecker {
public static void main(String[] args) {
ProcessBuilder pb = new ProcessBuilder("df", "-h");
try {
Process process = pb.start();
// Using try-with-resources for automatic closing
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
System.out.println("--- Disk Usage ---");
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
int exitCode = process.waitFor();
if (exitCode == 0) {
System.out.println("\nDisk space information retrieved successfully.");
} else {
System.err.println("\nCommand failed with exit code: " + exitCode);
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
Best Practices and Common Pitfalls
-
Always Use
ProcessBuilderwith a List: AvoidRuntime.exec()andProcessBuilder(String command). TheProcessBuilder(List<String>)constructor is the safest way to pass arguments. -
Read the Output Streams: Never start a process and ignore its output and error streams. Always consume them, preferably in separate threads, to prevent deadlocks.
-
Handle
InterruptedException: ThewaitFor()method can throwInterruptedException. You must catch it and restore the thread's interrupt flag by callingThread.currentThread().interrupt(). This is good practice for long-running applications. -
Check the Exit Code: A command might execute but fail (e.g.,
ls /nonexistent). Always check the exit code (process.exitValue()) to know if the command was successful (usually0). -
Clean Up Resources: Use
try-with-resourcesforInputStreamandOutputStreamto ensure they are closed automatically, preventing resource leaks. -
Paths: If your command is not in the system's
PATH, provide the full path to the executable (e.g.,new ProcessBuilder("/usr/bin/ls", "-l")). If it's a local script in the same directory, usenew File("./your_script.sh").getAbsolutePath().
