Почему Runtime.exec(String) работает для некоторых, но не для всех команд?

Когда я пытаюсь запустить Runtime.exec(String), некоторые команды работают, в то время как другие команды выполняются, но терпят неудачу или делают разные вещи, чем в моем терминале. Вот автономный тестовый пример, демонстрирующий эффект:

public class ExecTest {
  static void exec(String cmd) throws Exception {
    Process p = Runtime.getRuntime().exec(cmd);

    int i;
    while( (i=p.getInputStream().read()) != -1) {
      System.out.write(i);
    }
    while( (i=p.getErrorStream().read()) != -1) {
      System.err.write(i);
    }
  }

  public static void main(String[] args) throws Exception {
    System.out.print("Runtime.exec: ");
    String cmd = new java.util.Scanner(System.in).nextLine();
    exec(cmd);
  }
}

Этот пример отлично работает, если я заменяю команду echo hello world, но для других команд, особенно тех, которые связаны с именами файлов с такими пробелами, как здесь, я получаю ошибки, хотя команда явно выполняется:

myshell$ javac ExecTest.java && java ExecTest
Runtime.exec: ls -l 'My File.txt'
ls: cannot access 'My: No such file or directory
ls: cannot access File.txt': No such file or directory

тем временем, скопировать в мою оболочку:

myshell$ ls -l 'My File.txt'
-rw-r--r-- 1 me me 4 Aug  2 11:44 My File.txt

Почему существует разница? Когда это работает, и когда это не удается? Как заставить его работать для всех команд?

Ответ 1

Почему некоторые команды терпят неудачу?

Это происходит потому, что команда, переданная в Runtime.exec(String), не выполняется в оболочке. Оболочка выполняет множество общих служб поддержки для программ, и когда оболочка не собирается их выполнять, команда будет терпеть неудачу.

Когда команды не выполняются?

Команда будет терпеть неудачу всякий раз, когда это зависит от функций оболочки. В оболочке много общих, полезных вещей, о которых мы обычно не думаем:

  • Оболочка правильно раскладывается на кавычки и пробелы

    Это гарантирует, что имя файла в "My File.txt" останется единственным аргументом.

    Runtime.exec(String) наивно разбивается на пробелы и передает это как два отдельных имени файла. Это, очевидно, не удается.

  • Оболочка расширяет globs/wildcards

    При запуске ls *.doc оболочка перезаписывает его в ls letter.doc notes.doc.

    Runtime.exec(String) нет, он просто передает их как аргументы.

    ls не знает, что такое *, поэтому команда не работает.

  • Оболочка управляет каналами и перенаправлением.

    При запуске ls mydir > output.txt оболочка открывает "output.txt" для вывода команды и удаляет ее из командной строки, предоставляя ls mydir.

    Runtime.exec(String) нет. Он просто передает их в качестве аргументов.

    ls не знает, что означает >, поэтому команда не работает.

  • Оболочка расширяет переменные и команды

    При запуске ls "$HOME" или ls "$(pwd)" оболочка перезаписывает его в ls /home/myuser.

    Runtime.exec(String) нет, он просто передает их как аргументы.

    ls не знает, что означает $, поэтому команда не работает.

Что вы можете сделать вместо этого?

Существует два способа выполнения произвольно сложных команд:

Простой и неряшливый: делегировать оболочку.

Вы можете просто использовать Runtime.exec(String[]) (обратите внимание на параметр массива) и передать команду непосредственно в оболочку, которая может выполнять весь тяжелый подъем:

// Simple, sloppy fix. May have security and robustness implications
String myFile = "some filename.txt";
String myCommand = "cp -R '" + myFile + "' $HOME 2> errorlog";
Runtime.getRuntime().exec(new String[] { "bash", "-c", myCommand });

Безопасный и надежный: взять на себя обязанности оболочки.

Это не исправление, которое может быть применено механически, но требует понимания модели исполнения Unix, каких оболочек и как вы можете сделать то же самое. Тем не менее, вы можете получить надежное, надежное и надежное решение, сняв оболочку с рисунка. Этому способствует ProcessBuilder.

Команда из предыдущего примера, которая требует, чтобы кто-то обрабатывал 1. кавычки, 2. переменные и 3. перенаправления, может быть записан как:

String myFile = "some filename.txt";
ProcessBuilder builder = new ProcessBuilder(
    "cp", "-R", myFile,        // We handle word splitting
       System.getenv("HOME")); // We handle variables
builder.redirectError(         // We set up redirections
    ProcessBuilder.Redirect.to(new File("errorlog")));
builder.start();