Executing Shell Commands with Java

Introduction

In this article, we'll take a look at how we can leverage the Runtime and ProcessBuilder classes to execute shell commands and scripts with Java.

We use computers to automate many things in our daily jobs. System administrators run many commands all the time, some of which are very repetitive and require minimal changes in-between runs.

This process is also ripe for automation. There's no need to run everything manually. Using Java, we can run single or multiple shell commands, execute shell scripts, run the terminal/command prompt, set working directories and manipulate environment variables through core classes.

Runtime.exec()

The Runtime class in Java is a high-level class, present in every single Java application. Through it, the application itself communicates with the environment it's in.

By extracting the runtime associated with our application via the getRuntime() method, we can use the exec() method to execute commands directly or run .bat/.sh files.

The exec() method offers a few overloaded variations:

  • public Process exec(String command) - Executes the command contained in command in a separate process.
  • public Process exec(String command, String[] envp) - Executes the command, with an array of environment variables. They're provided as an array of Strings, following the name=value format.
  • public Process exec(String command, String[] envp, File dir) - Executes the command, with the specified environment variables, from within the dir directory.
  • public Process exec(String cmdArray[]) - Executes a command in the form of an array of Strings.
  • public Process exec(String cmdArray[], String[] envp) - Executes a command with the specified environment variables.
  • public Process exec(String cmdarray[], String[] envp, File dir) - Executes a command, with the specified environment variables, from within the dir directory.

It's worth noting that these processes are run externally from the interpreter and will be system-dependent.

What's also worth noting is the difference between String command and String cmdArray[]. They achieve the same thing. A command is broken down into an array anyway, so using any of these two should yield the same results.

It's up to you to decide whether exec("dir /folder") or exec(new String[]{"dir", "/folder"} is what you'd like to use.

Let's write up a few examples to see how these overloaded methods differ from each other.

Executing a Command from String

Let's start off with the simplest approach out of these three:

Process process = Runtime.getRuntime().exec("ping www.stackabuse.com");

Running this code will execute the command we've supplied in String format. However, we don't see anything when we run this.

To validate if this ran correctly, we'll want to get ahold of the process object. Let's use a BufferedReader to take a look at what's going on:

public static void printResults(Process process) throws IOException {
    BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
    String line = "";
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

Now, when we run this method after the exec() method, it should yield something along the lines of:

Pinging www.stackabuse.com [104.18.57.23] with 32 bytes of data:
Reply from 104.18.57.23: bytes=32 time=21ms TTL=56
Reply from 104.18.57.23: bytes=32 time=21ms TTL=56
Reply from 104.18.57.23: bytes=32 time=21ms TTL=56
Reply from 104.18.57.23: bytes=32 time=21ms TTL=56

Ping statistics for 104.18.57.23:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 21ms, Maximum = 21ms, Average = 21ms

Keep in mind that we'll have to extract the process information from the Process instances as we go through other examples.

Specify the Working Directory

If you'd like to run a command from, say, a certain folder, we'd do something along the lines of:

Process process = Runtime.getRuntime()
        .exec("cmd /c dir", null, new File("C:\\Users\\"));
      //.exec("sh -c ls", null, new File("Pathname")); for non-Windows users
printResults(process);

Here, we've provided the exec() method with a command, a null for new environment variables and a new File() which is set as our working directory.

The addition of cmd /c before a command such as dir is worth noting.

Since I'm working on Windows, this opens up the cmd and /c carries out the subsequent command. In this case, it's dir.

The reason why this wasn't mandatory for the ping example, but is mandatory for this example is nicely answered by an SO user.

Running the previous piece of code will result in:

Volume in drive C has no label.
 Volume Serial Number is XXXX-XXXX

 Directory of C:\Users

08/29/2019  05:01 PM    <DIR>          .
08/29/2019  05:01 PM    <DIR>          ..
08/18/2016  09:11 PM    <DIR>          Default.migrated
08/29/2019  05:01 PM    <DIR>          Public
05/15/2020  11:08 AM    <DIR>          User
               0 File(s)              0 bytes
               5 Dir(s)  212,555,214,848 bytes free

Let's take a look at how we could supply the previous command in several individual parts, instead of a single String:

Process process = Runtime.getRuntime().exec(
        new String[]{"cmd", "/c", "dir"},
        null, 
        new File("C:\\Users\\"));
        
printResults(process);

Running this piece of code will also result in:

Volume in drive C has no label.
 Volume Serial Number is XXXX-XXXX

 Directory of C:\Users

08/29/2019  05:01 PM    <DIR>          .
08/29/2019  05:01 PM    <DIR>          ..
08/18/2016  09:11 PM    <DIR>          Default.migrated
08/29/2019  05:01 PM    <DIR>          Public
05/15/2020  11:08 AM    <DIR>          User
               0 File(s)              0 bytes
               5 Dir(s)  212,542,808,064 bytes free

Ultimately, regardless of the approach - using a single String or a String array, the command you input will always be broken down into an array before getting processed by underlying logic.

Which one you'd like to use boils down just to which one you find more readable.

Using Environment Variables

Let's take a look at how we can use environment variables:

Process process = Runtime.getRuntime().exec(
        "cmd /c echo %var1%",
        new String[]{"var1=value1"});
        
printResults(process);

We can supply as many environment variables as we'd like within the String array. Here, we've just printed the value of var1 using echo.

Running this code will return:

value1

Running .bat and .sh Files

Sometimes, it's just much easier to offload everything into a file and run that file instead of adding everything programmatically.

Depending on your operating system, you'd use either .bat or .sh files. Let's create one with the contents:

echo Hello World

Then, let's use the same approach as before:

Process process = Runtime.getRuntime().exec(
        "cmd /c start file.bat",
        null,
        new File("C:\\Users\\User\\Desktop\\"));

This'll open up the command prompt and run the .bat file in the working directory we've set.

Running this code surely enough results in:

command prompt with hello world

With all of the overloaded exec() signatures taken care of, let's take a look at the ProcessBuilder class and how we can execute commands using it.

ProcessBuilder

ProcessBuilder is the underlying mechanism that runs the commands when we use the Runtime.getRuntime().exec() method:

/**
 * Executes the specified command and arguments in a separate process with
 * the specified environment and working directory.
 *...
*/
public Process exec(String[] cmdarray, String[] envp, File dir) throws IOException {
    return new ProcessBuilder(cmdarray)
        .environment(envp)
        .directory(dir)
        .start();
}

JavaDocs for the Runtime class

Taking a look at how the ProcessBuilder takes our input from the exec() method and runs the command, gives us a good idea of how to use it as well.

It accepts a String[] cmdarray, and that's enough to get it running. Alternatively, we can supply it with optional arguments such as the String[] envp and File dir.

Let's explore these options.

ProcessBuilder: Executing Command from Strings

Instead of being able to provide a single String, such as cmd /c dir, we'll have to break it up in this case. For example, if we wanted to list the files in the C:/Users directory like before, we'd do:

ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("cmd", "/c", "dir C:\\Users");

Process process = processBuilder.start();
printResults(process);

To actually execute a Process, we run the start() command and assign the returned value to a Process instance.

Running this code will yield:

 Volume in drive C has no label.
 Volume Serial Number is XXXX-XXXX

 Directory of C:\Users

08/29/2019  05:01 PM    <DIR>          .
08/29/2019  05:01 PM    <DIR>          ..
08/18/2016  09:11 PM    <DIR>          Default.migrated
08/29/2019  05:01 PM    <DIR>          Public
05/15/2020  11:08 AM    <DIR>          User
               0 File(s)              0 bytes
               5 Dir(s)  212,517,294,080 bytes free

However, this approach isn't any better than the previous one. What's useful with the ProcessBuilder class is that it's customizable. We can set things programmatically, not just via commands.

ProcessBuilder: Specify the Working Directory

Instead of supplying the working directory via the command, let's set it programmatically:

processBuilder.command("cmd", "/c", "dir").directory(new File("C:\\Users\\"));

Here, we've set the working directory to be the same as before, but we've moved that definition out of the command itself. Running this code will provide the same result as the last example.

ProcessBuilder: Environment Variables

Using ProcessBuilders methods, it's easy to retrieve a list of environment variables in the form of a Map. It's also easy to set environment variables so that your program can use them.

Let's get the environment variables currently available and then add some for later use:

ProcessBuilder processBuilder = new ProcessBuilder();

Map<String, String> environmentVariables  = processBuilder.environment();
environmentVariables.forEach((key, value) -> System.out.println(key + value));

Here, we've packed the returned environment variables into a Map and ran a forEach() on it to print out the values to our console.

Running this code will yield a list of the environment variables you have on your machine:

DriverDataC:\Windows\System32\Drivers\DriverData
HerokuPathE:\Heroku
ProgramDataC:\ProgramData
...

Now, let's add an environment variable to that list and use it:

environmentVariables.put("var1", "value1");

processBuilder.command("cmd", "/c", "echo", "%var1%");
Process process = processBuilder.start();
printResults(process);

Running this code will yield:

value1

Of course, once the program has finished running, this variable will not stay in the list.

ProcessBuilder: Running .bat and .sh Files

If you'd like to run a file, again, we'd just supply the ProcessBuilder instance with the required information:

processBuilder
        .command("cmd", "/c", "start", "file.bat")
        .directory(new File("C:\\Users\\User\\Desktop"));
Process process = processBuilder.start();

Running this code results in the command prompt opening up and executing the .bat file:

command prompt hello world bat file

Conclusion

In this article, we've explored examples of running shell commands in Java. We've used the Runtime and ProcessBuilder classes to do this.

Using Java, we can run single or multiple shell commands, execute shell scripts, run the terminal/command prompt, set working directories and manipulate environment variables through core classes.