Как выполнить shell-команду из Java

Системные вызовы из Java часто приводят к зависаниям, потерям вывода и проблемам на разных ОС: ошибки при работе с потоками stdin/stdout/stderr и игнорирование кодов возврата ломают автоматизацию. В статье даны сравнение Runtime.exec и ProcessBuilder, показаны приёмы работы с потоками, обработка ошибок и нюансы кроссплатформенности.

Вы получите алгоритм безопасного запуска и контроля процесса: чтение/запись потоков, корректное закрытие ресурсов и проверка кодов возврата. В результате сможете надёжно интегрировать shell‑вызовы в серверное приложение и избежать утечек и блокировок.

Вадим Дворниковавтор материала

Зависимость операционной системы

Сначала нужно определить операционную систему, на которой работает наша JVM . В Windows необходимо запустить команду в качестве аргумента оболочки cmd.exe, а в остальных ОС мы будем использовать стандартную оболочку sh:

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

Ввод и вывод

Также нужно подключиться к входным и выходным потокам нашего процесса. По крайней мере, нужно получить выходные данные, иначе процесс зависнет.

Реализуем класс StreamGobbler, который использует 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);
    }
}

Примечание. Этот класс реализует интерфейс Runnable, а это означает, что он может быть выполнен любым исполнителем.

Runtime.exec()

Метод Runtime.exec() - это простой, но недостаточно гибкий способ создания нового подпроцесса.

В следующем примере мы запросим список пользователей из локальной директории и выведем его в консоли:

String homeDirectory = System.getProperty("user.home");
Process process;
if (isWindows) {
    process = Runtime.getRuntime()
      .exec(String.format("cmd.exe /c dir %s", homeDirectory));
} else {
    process = Runtime.getRuntime()
      .exec(String.format("sh -c ls %s", homeDirectory));
}
StreamGobbler streamGobbler = 
  new StreamGobbler(process.getInputStream(), System.out::println);
Executors.newSingleThreadExecutor().submit(streamGobbler);
int exitCode = process.waitFor();
assert exitCode == 0;

ProcessBuilder

Класс ProcessBuilder является более гибким в использовании, чем Runtime. Он позволяет настраивать целый ряд параметров.

Например, можно:

  • изменить рабочий каталог, в котором работает shell-команда,
  • перенаправить потоки ввода и вывода;
  • наследовать их в потоках текущего процесса JVM, используя builder.inheritIO().
	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);
Executors.newSingleThreadExecutor().submit(streamGobbler);
int exitCode = process.waitFor();
assert exitCode == 0;

Заключение

Shell команды в Java можно выполнять двумя различными способами. Но если нужно настроить выполнение созданного процесса, то используйте класс ProcessBuilder.

Программные исходники примеров, приведенных в этой статье, доступны на GitHub .

Вадим Дворниковавтор-переводчик статьи «How to Run a Shell Command in Java»

Комментарии

Оставьте свой комментарий
Пока никто не оставил комментариев