Course – LS (cat=Java)

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

> CHECK OUT THE COURSE

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.

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); 

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 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are closed on this article!