1. Overview

In this article, we’ll learn how to execute a shell command from Java applications.

First, we’ll use the .exec() method the Runtime class provides. Then, we’ll learn about ProcessBuilder, which is more customizable.

2. Operating System Dependency

Shell commands are OS-dependent as their behavior differs across systems. So, before we create any Process to run our shell command in, we need to be aware of the operating system on which our JVM is running.

Additionally, on Windows, the shell is commonly referred to as cmd.exe. Instead, on Linux and macOS, shell commands are run using /bin/sh. For compatibility on these different machines, we can programmatically append cmd.exe if on a Windows machine or /bin/sh otherwise. For instance, we can check if the machine where the code is running is a Windows machine by reading the “os.name” property from the System class:

boolean isWindows = System.getProperty("os.name")
  .toLowerCase().startsWith("windows");

3. Input and Output

Often, we need to connect the input and output streams of the process. In detail, the InputStream acts as the standard input, and the OutputStream acts as the standard output of the process. We must always consume the output stream. Otherwise, our process won’t return and will hang forever.

Let’s implement a commonly used class called StreamGobbler, which consumes an InputStream:

private static class StreamGobbler implements Runnable {
    private InputStream inputStream;
    private Consumer<String> consumer;

    public StreamGobbler(InputStream inputStream, Consumer<String> consumer) {
        this.inputStream = inputStream;
        this.consumer = consumer;
    }

    @Override
    public void run() {
        new BufferedReader(new InputStreamReader(inputStream)).lines()
          .forEach(consumer);
    }
}

This class implements the Runnable interface, which means any Executor could execute it.

4. Runtime.exec()

Next, we’ll spawn a new process using the .exec() method and use the StreamGobler created previously.

For example, we can list all the directories inside the user’s home directory and then print it to the console:

Process process;
if (isWindows) {
    process = Runtime.getRuntime()
      .exec(String.format("cmd.exe /c dir %s", homeDirectory));
} else {
    process = Runtime.getRuntime()
      .exec(String.format("/bin/sh -c ls %s", homeDirectory));
}
StreamGobbler streamGobbler = 
  new StreamGobbler(process.getInputStream(), System.out::println);
Future<?> future = executorService.submit(streamGobbler);

int exitCode = process.waitFor();

assertDoesNotThrow(() -> future.get(10, TimeUnit.SECONDS));
assertEquals(0, exitCode);

Here, we created a new sub-process with .newSingleThreadExecutor() and then used .submit() to run our Process containing the shell commands. Additionally, .submit() returns a Future object we utilize to check the result of the process. Also, make sure to call the .get() method on the returned object to wait for the computation to complete. If you are running the above code from a main method, be sure to call shutdown on the executorService object or the code will never stop. This also applies to all examples below. In our code, we use Junit lifecycle methods to do necessary cleanups like that.

NOTE: JDK 18 deprecates .exec(String command) from the Runtime class.

4.1. Handle Pipes

Currently, there is no way to handle pipes with .exec(). Fortunately, the pipes are a shell feature. So, we can create the whole command where we want to use pipe and pass it to .exec():

if (IS_WINDOWS) {
    process = Runtime.getRuntime()
        .exec(String.format("cmd.exe /c dir %s | findstr \"Desktop\"", homeDirectory));
} else {
    process = Runtime.getRuntime()
        .exec(String.format("/bin/sh -c ls %s | grep \"Desktop\"", homeDirectory));
}

Here, we list all the directories in the user’s home and search for the “Desktop” folder.

5. ProcessBuilder

Alternatively, we can use a ProcessBuilder, which is preferred over the Runtime approach because we can customize it instead of just running a string command.

In short, with this approach, we’re able to:

  • change the working directory our shell command is running in using .directory()
  • change environment variables by providing a key-value map to .environment()
  • redirect input and output streams in a custom way
  • inherit both of them to the streams of the current JVM process using .inheritIO()

Similarly, we can run the same shell command as in the previous example:

ProcessBuilder builder = new ProcessBuilder();
if (isWindows) {
    builder.command("cmd.exe", "/c", "dir");
} else {
    builder.command("sh", "-c", "ls");
}
builder.directory(new File(System.getProperty("user.home")));
Process process = builder.start();
StreamGobbler streamGobbler = 
  new StreamGobbler(process.getInputStream(), System.out::println);
Future<?> future = executorService.submit(streamGobbler);

int exitCode = process.waitFor();

assertDoesNotThrow(() -> future.get(10, TimeUnit.SECONDS));
assertEquals(0, exitCode); 

5.1. Handle Pipes

Java 9 introduced the concept of pipelines to the ProcessBuilder API:

public static List<Process> startPipeline(List<ProcessBuilder> builders) throws IOException

Using the startPipeline method we can pass a list of ProcessBuilder objects. This static method will then start a Process for each ProcessBuilder. Thus, creating a pipeline of processes which are linked by their standard output and standard input streams.

For example, we can create a process builder for each isolated command and compose them into the pipeline:

@Test
public void givenProcessBuilder_whenStartingPipeline_thenSuccess()
  throws IOException, InterruptedException {
    List<ProcessBuilder> builders = Arrays.asList(
      new ProcessBuilder("find", "src", "-name", "*.java", "-type", "f"), 
      new ProcessBuilder("wc", "-l"));

    List<Process> processes = ProcessBuilder.startPipeline(builders);
    Process last = processes.get(processes.size() - 1);

    List<String> output = readOutput(last.getInputStream());
    assertThat("Results should not be empty", output, is(not(empty())));
}

In the above example, we’re searching for all the java files inside the src directory and piping the results into another process to count them.

To learn about other improvements made to the Process API in Java 9, check out our great article on Java 9 Process API Improvements.

6. Conclusion

As we’ve seen in this quick tutorial, we can execute a shell command in Java in two distinct ways.

Generally, if we’re planning to customize the execution of the spawned process, for example, to change its working directory, we should consider using a ProcessBuilder.

As always, the sources are available over on GitHub.

Course – LS (cat=Java)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.