110

I'm trying to execute an external command from java code, but there's a difference I've noticed between Runtime.getRuntime().exec(...) and new ProcessBuilder(...).start().

When using Runtime:

Process p = Runtime.getRuntime().exec(installation_path + 
                                       uninstall_path + 
                                       uninstall_command + 
                                       uninstall_arguments);
p.waitFor();

the exitValue is 0 and the command is terminated ok.

However, with ProcessBuilder:

Process p = (new ProcessBuilder(installation_path +    
                                 uninstall_path +
                                 uninstall_command,
                                 uninstall_arguments)).start();
p.waitFor();

the exit value is 1001 and the command terminates in the middle, although waitFor returns.

What should I do to fix the problem with ProcessBuilder?

Naman
  • 27,789
  • 26
  • 218
  • 353
gal
  • 1,111
  • 2
  • 8
  • 5

4 Answers4

112

The various overloads of Runtime.getRuntime().exec(...) take either an array of strings or a single string. The single-string overloads of exec() will tokenise the string into an array of arguments, before passing the string array onto one of the exec() overloads that takes a string array. The ProcessBuilder constructors, on the other hand, only take a varargs array of strings or a List of strings, where each string in the array or list is assumed to be an individual argument. Either way, the arguments obtained are then joined up into a string that is passed to the OS to execute.

So, for example, on Windows,

Runtime.getRuntime().exec("C:\DoStuff.exe -arg1 -arg2");

will run a DoStuff.exe program with the two given arguments. In this case, the command-line gets tokenised and put back together. However,

ProcessBuilder b = new ProcessBuilder("C:\DoStuff.exe -arg1 -arg2");

will fail, unless there happens to be a program whose name is DoStuff.exe -arg1 -arg2 in C:\. This is because there's no tokenisation: the command to run is assumed to have already been tokenised. Instead, you should use

ProcessBuilder b = new ProcessBuilder("C:\DoStuff.exe", "-arg1", "-arg2");

or alternatively

List<String> params = java.util.Arrays.asList("C:\DoStuff.exe", "-arg1", "-arg2");
ProcessBuilder b = new ProcessBuilder(params);
Luke Woodward
  • 63,336
  • 16
  • 89
  • 104
  • it still doesn't work: List params = java.util.Arrays.asList(installation_path+uninstall_path+uninstall_command, uninstall_arguments); Process qq=new ProcessBuilder(params).start(); – gal Jul 28 '11 at 09:50
  • 7
    I can not believe that this string concatanation makes any sense: "installation_path+uninstall_path+uninstall_command". – Angel O'Sphere Jul 28 '11 at 09:53
  • 9
    Runtime.getRuntime().exec(...) does *NOT* invoke a shell unless that is explicitly specified by the command. That is a good thing regarding the recent "Shellshock" bug issue. This answer is misleading, because it states that cmd.exe or equivalent (i.e. /bin/bash on unix) would be run, which does not seem to be the case. Instead tokenization is done inside the Java environment. – Stefan Paul Noack Oct 17 '14 at 18:49
  • @noah1989: thanks for the feedback. I've updated my answer to (hopefully) clarify things and in particular remove any mention of shells or `cmd.exe`. – Luke Woodward Oct 17 '14 at 22:41
  • the parser for exec doesn't work quite the same as a the parameterized version either, which took me a few days to figure out... – Drew Delano Jan 27 '19 at 04:26
  • @DrewDelano - yea. The "parser" is effectively `String.split("\\s+")` :-) – Stephen C Mar 05 '20 at 05:38
21

There are no difference between ProcessBuilder.start() and Runtime.exec() because implementation of Runtime.exec() is:

public Process exec(String command) throws IOException {
    return exec(command, null, null);
}

public Process exec(String command, String[] envp, File dir)
    throws IOException {
    if (command.length() == 0)
        throw new IllegalArgumentException("Empty command");

    StringTokenizer st = new StringTokenizer(command);
    String[] cmdarray = new String[st.countTokens()];
    for (int i = 0; st.hasMoreTokens(); i++)
        cmdarray[i] = st.nextToken();
    return exec(cmdarray, envp, dir);
}

public Process exec(String[] cmdarray, String[] envp, File dir)
    throws IOException {
    return new ProcessBuilder(cmdarray)
        .environment(envp)
        .directory(dir)
        .start();
}

So code:

List<String> list = new ArrayList<>();
new StringTokenizer(command)
.asIterator()
.forEachRemaining(str -> list.add((String) str));
new ProcessBuilder(String[])list.toArray())
            .environment(envp)
            .directory(dir)
            .start();

should be the same as:

Runtime.exec(command)

Thanks dave_thompson_085 for comment

Eugene Lopatkin
  • 2,351
  • 1
  • 22
  • 34
20

Look at how Runtime.getRuntime().exec() passes the String command to the ProcessBuilder. It uses a tokenizer and explodes the command into individual tokens, then invokes exec(String[] cmdarray, ......) which constructs a ProcessBuilder.

If you construct the ProcessBuilder with an array of strings instead of a single one, you'll get to the same result.

The ProcessBuilder constructor takes a String... vararg, so passing the whole command as a single String has the same effect as invoking that command in quotes in a terminal:

shell$ "command with args"
Costi Ciudatu
  • 37,042
  • 7
  • 56
  • 92
19

Yes there is a difference.

  • The Runtime.exec(String) method takes a single command string that it splits into a command and a sequence of arguments.

  • The ProcessBuilder constructor takes a (varargs) array of strings. The first string is the command name and the rest of them are the arguments. (There is an alternative constructor that takes a list of strings, but none that takes a single string consisting of the command and arguments.)

So what you are telling ProcessBuilder to do is to execute a "command" whose name has spaces and other junk in it. Of course, the operating system can't find a command with that name, and the command execution fails.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • No, there is not a difference. Runtime.exec(String) is a shortcut for ProcessBuilder. There are other constructors supported. – marcolopes Mar 05 '20 at 05:07
  • 2
    You are incorrect. Read the source code! `Runtime.exec(cmd)` is effectively a shortcut for `Runtime.exec(cmd.split("\\s+"))`. The `ProcessBuilder` class does not have a constructor that is a direct equivalent to `Runtime.exec(cmd)`. This is the point I am making in my answer. – Stephen C Mar 05 '20 at 06:14
  • 1
    In fact, if you instantiate a ProcessBuilder like this: `new ProcessBuilder("command arg1 arg2")`, the `start()` call will not do what you expect. It will probably fail, and will only succeed if you have a command with spaces in its name. This is precisely the problem that the OP is asking about! – Stephen C Mar 05 '20 at 06:20