Skip to Content
Course content

208: Running External Processes with ProcessBuilder

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this exact scenario play out a dozen times with junior devs: you need Java to do something that a command-line tool already does perfectly, so you reach for ProcessBuilder. It seems straightforward—you tell Java what to run, you call waitFor(), and you move on. But then, suddenly, your application just... stops. No exception, no crash, just a permanent hang.

Take a look at this snippet. I'm trying to run a recursive directory listing to get a feel for a project structure.

ProcessBuilder pb = new ProcessBuilder("ls", "-R", "/usr/local/bin");
Process process = pb.start();
// We just want to wait until the command finishes
int exitCode = process.waitFor(); 
System.out.println("Finished with code: " + exitCode);

The Mysterious Process Hang

If you run this on a directory with a few files, it works fine. But if you run it on a massive directory, the program will hang indefinitely. You'll be staring at your IDE, wondering why the process hasn't returned, and you might even think you've hit a deadlock in your Java code.

Here is what's actually happening: Every process has a limited buffer for its standard output (stdout) and standard error (stderr). When the external process (in this case, ls) prints a lot of text, it fills up that buffer. Once the buffer is full, the external process blocks—it literally stops executing—because it can't write any more data until some other process (your Java app) reads that data out of the pipe.

But your Java app is calling process.waitFor(). You've told Java to sit still and do nothing until the process finishes. You've created a classic stalemate: the process is waiting for you to read, and you are waiting for the process to finish.

Solving the Buffer Blockage

The quickest way to fix this, especially if you just want to see the output in your own console for debugging, is to use inheritIO(). This tells the child process to use the same standard I/O streams as the parent Java process. Essentially, it bypasses the Java-managed buffers and sends the output directly to your terminal.

ProcessBuilder pb = new ProcessBuilder("ls", "-R", "/usr/local/bin");
// This is the magic line
pb.inheritIO(); 
Process process = pb.start();
int exitCode = process.waitFor();
System.out.println("Finished with code: " + exitCode);

Now, the output flows straight to your console, the buffer never fills up, and waitFor() can finally return. If you actually need to capture that output to use it in a variable, you can't use inheritIO(). Instead, you'd need to read the process.getInputStream() in a separate thread or use redirectOutput(File file) to dump the results into a temporary file.

Handling Arguments Correctly

Another thing I want to mention is how you pass commands. A common instinct is to try and pass the whole command as one string: new ProcessBuilder("ls -R /bin"). This will fail with an IOException because Java looks for an executable file literally named "ls -R /bin", which obviously doesn't exist.

Always pass the command and its arguments as separate strings in a list or as varargs. This avoids shell injection vulnerabilities and ensures the operating system handles the argument parsing correctly. If you absolutely must use a shell feature like pipes (|) or redirects (>), you have to wrap the execution in a shell call, like this:

// On Linux/macOS: running a piped command
ProcessBuilder pb = new ProcessBuilder("sh", "-c", "ls -R /usr/local/bin | grep 'my-app'");
pb.inheritIO();
pb.start().waitFor();

I generally recommend avoiding sh -c unless it's strictly necessary. It adds overhead and makes your code platform-dependent. Stick to the list-of-arguments approach whenever possible.




📋 Practical Task

Exercise: Building a Git Branch Auditor

Your task is to create a small utility that audits a Git repository to see how many local branches exist. This will require you to execute an external process and actually process the output in Java rather than just printing it to the console.

Requirements:

  • Use ProcessBuilder to run the command git branch.
  • Do not use inheritIO(). Instead, use process.getInputStream() and a BufferedReader to read the output.
  • Count the number of lines returned by the command (each line represents a branch).
  • Print the final count to the console in the format: "Local branch count: X".
  • Ensure you handle the IOException and InterruptedException that these methods throw.

Hint: You'll need to wrap the input stream in an InputStreamReader before passing it to the BufferedReader.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.