How to run a shell script from a Java application
Introduction
Running a shell script from a Java program using
Runtime.exec()
seems superficially simple. Yet,
time and again, I see developers struggling to get it to work
reliably. The Java documentation does not provide a lot of
help, even though the exec()
method has had developers
banging their heads on their desks for as long as Java has existed.
Runtime.exec()
is problematic for reasons that have
nothing to do with running shell scripts — scripts
simply add a bunch of other pain points to an already painful experience.
This article describes in detail why exec()
and its related
API methods are so difficult to use properly, and outlines what I hope is a
reasonably robust implementation.
I've tested the steps I describe on Linux, although I've no particular reason to think they won't be applicable to other Unix-like systems. The basic principles apply to Windows, too but, of course, the use of the command-line interpreter is completely different.
The basic problem
From a Java application, I want to run the shell script something.sh
, and pass it the string 42
as an argument. That is,
I want to get the same effect I would get if I executed at the shell
prompt:
$ something.sh 42
Specific problems with running scripts
The problematic implementations I typically see look something like this:
Process p = Runtime.getRuntime().exec ("something.sh 42"); int e = p.exitValue(); System.out.println ("Exit code = " + e);
I'll explain first what's wrong with this implementation, specifically as a way of running a script. Then I'll describe what would be wrong with it as a way of running any kind of executable.
exec()
(probably) only handles binaries
Most fundamentally, something.sh
is not an
executable binary — it is, of course,
a shell script. Although we get used to running scripts and binaries
interchangeably on the command line, they aren't the same thing at
all. To run a script, we must run a shell and indicate the location
of the script.
I should point out, in the interests of completeness, that some Linux implementations are configured to be able to run scripts at the system call level. That is, the kernel can actually process a script as if it were an executable, by implicitly invoking a shell. However, this isn't common behaviour, and shouldn't be relied on.
To invoke the shell to run the script, we might try something like this:
Process p = Runtime.getRuntime().exec ("sh -c something.sh 42");
The -c
switch tells the shell to process the next argument as
a command. This won't work, either, because ...
exec()
does not know how to group argments
In my example, I want to invoke something.sh
and pass
it the argument 42
. But Runtime.exec()
doesn't know that my "42" is an argument to "something.sh"; it thinks
that all three arguments are for sh
. So
something.sh
might get invoked, but it won't get the
42
argument. Or, indeed, any arguments.
Perhaps we can use quotes to group the arguments? This won't work, either, because...
The string tokenization done by exec()
is crude
It looks as is this should work:
Process p = Runtime.getRuntime().exec ("sh -c \"something.sh 42\"");
That is, I've enclosed the arguments to sh -c
in double-quotes,
to indicate to the shell that there is a single argument to
something.sh
.
However, it's not the shell we need to be concerned about here — it's
the crudeness of the exec()
string tokenization. The method
exec(String)
simply splits the string on whitespace. It doesn't
understand quotes or escapes.
So, in this case, the sh
utility still gets three arguments
(four, including 'sh' itself). The third argument is "something.sh
,
and the fourth 42"
. In practice, it's going to be necessary
to tokenize the string ourselves. That may be easy or it may be difficult,
depending on the application's needs. In any case, we need to end up
with an array of String
instances that can be passed
to exec (String[])
.
This should do the trick in this simple case, where we know all the arguments ahead of time:
Process p = Runtime.getRuntime().exec (new String[]{"sh", "-c", "something.sh 42"});
Here we've explicitly passed a single argument after sh -c
—
the script and its arguments — to be processed by the shell.
But we're still not done.
The fundametal problem raised in the last few sections is that
Runtime.exec()
is not a command-line processor — it's
only a process launcher. As well as not tokenizing very intelligently,
it won't expand shell wildcards (*.txt
), or substitute
environment variables ($HOME
). If you're launching a shell
script, you should expect to have to do any of this kind of thing
in the script itself.
Be careful about file locations
In practice, the shell might need to be told where to find the
script something.sh
. On Linux the shell won't necessarily look
in the current directory, although it might look in the directories
specified in $PATH
. If the script is in the working
directory of the Java application, you probably need to do this:
Process p = Runtime.getRuntime().exec (new String[]{"sh", "-c", "./something.sh 42"});
Alternatively, you could use the full pathname of the script.
Other things to watch out for, when running a script
It should be obvious, but bear in mind that running a script using Java doesn't do any additional magic. The script still needs to have execute permissions to be run as a command, and it will usually still need to start with an interpreter line:
#!/bin/sh
These things aren't specific to Java — my point is that using Java doesn't mean that we can stop thinking about basic platform issues.
General pitfalls, not related to scripts
The preceeding sections described pitfalls in the use of
Runtime.exec()
related specifically to executing shell
scripts. However, we haven't yet even touched on the pitfalls that
apply to any use of exec()
.
exec()
does not block
The exec()
method starts a process, but does not wait
for it to complete. In our example the shell script might run
perfectly well, but the Java application will fail at the call to
exitValue()
. If the script has not yet finished
by the time this method is called — and most likely it will hardly
have started — then the exitValue
method will throw
IllegalThreadStateException
.
If we want the Java application to wait for the script to complete, we must code this explicitly, like this:
Process p = Runtime.getRuntime().exec ("something.sh 42"); p.waitFor(); int e = p.exitValue();
Be aware that, in principle, waitFor()
can throw an
InterruptedException
. To be frank, this is unlikely
— profoundly unlikely in a single-threaded application — and it's
difficult to know how to handle the exception if it is raised.
This brings me to the biggest bugbear of running processes from Java — the need to handle process output.
The application must handle standard out and standard error
The script (or other process) being executed may produce output, to standard out or standard error. If it does, and that output is not read, then the process being executed will block forever. Depending on how the Java application is coded, you may or may not notice this stalled process.
On some Linux versions, the process will block even if it doesn't produce output. It's never safe to assume that you don't need to read standard out and standard error. In any event, it's almost always good practice to collect this data, if only for troubleshooting why the script didn't work.
Because you need to collect both standard out and standard error at the same time (unless you can absolutely certain that only meagre amounts of output are generated), you'll need to create an additional two threads, just to read and process the output. One of these threads will read standard out, the other standard error. The threads need to run until the streams to which they are attached close.
To get the relevant streams, do this:
Process p = Runtime.getRuntime().exec (new String[]{"sh", "-c", "./something.sh 42"}); InputStream stdout = p.getInputStream(); InputStream stderr = p.getErrorStream();
I'm all too aware how confusing it can be, that the output from the
process is an InputStream
. From the point of view
of the Java program, however, it is a source of data, not a sink, and
thus InputStream
is entirely appropriate.
The streams provided by Process.getInputStream()
, etc.,
should be supplied to threads, which will then consume the
streams, and process the results according to the application's needs.
In some cases it will be sufficient simply to accumulate the process
output, if there is any, and log it somewhere. In more complex cases
the data will need to be parsed and processed.
Here is the outline of a class that implements a thread to consume the output of the launched process.
class StreamEater extends Thread { BufferedReader br; /** Construct a StreamEater on an InputStream. */ public StreamEater (InputStream is) { this.br = new BufferedReader (new InputStreamReader (is)); } public void run () { try { String line; while ((line = br.readLine()) != null) { // Process the line of output in some way } } catch (IOException e) { // Do something to handle exception } finally { try { br.close(); } catch (Exception e) {}; } } }
Having obtained the standard out and standard error streams from the
Process
, they can be passed to the stream-consuming
threads, like this:
StreamEater stdoutEater = new StreamEater (stdout); StreamEater stderrEater = new StreamEater (stderr); stdoutEater.start(); stderrEater.start(); p.waitFor();
When waitFor()
returns, the launched process will have
completed, and the threads created to process the output will
have finished their run()
methods. However, the
thread objects will still exist, and any data that was accumulated
during the running of the process will continue to be available.
It's impossible to give general advice on how to handle the output
of the process, or whether it's best to use a single stream
consumer to handle both stdout
and stderr
.
If the process generates only a little output (kilobytes to megabytes),
it might be
practical to buffer it all up and process it after the process completes.
In more complicated cases, the stream consumer threads will need to
process the data as it is delivered to them.
Closing remarks
Running a process — particularly a shell script — from a Java application is surprisingly difficult to do robustly. A complete example, based on the description above, is available from my GitHub repository.