陷阱 Runtime.exec 进程和 ProcessBuilder 不理解 shell 语法

Runtime.exec(String ...)Runtime.exec(String) 方法允许你作为外部进程 1 执行命令。在第一个版本中,你将命令名称和命令参数作为字符串数组的单独元素提供,Java 运行时请求 OS 运行时系统启动外部命令。第二个版本看起来很容易使用,但它有一些陷阱。

首先,这是一个使用 exec(String) 安全使用的例子:

Process p = Runtime.exec("mkdir /tmp/testDir");
p.waitFor();
if (p.exitValue() == 0) {
    System.out.println("created the directory");
}

路径名中的空格

假设我们概括了上面的例子,以便我们可以创建一个任意目录:

Process p = Runtime.exec("mkdir " + dirPath);
// ...

这通常会起作用,但如果 dirPath 是(例如)“/ home / user / My Documents”,则会失败。问题是 exec(String) 通过简单地查找空格将字符串拆分为命令和参数。命令字符串:

"mkdir /home/user/My Documents"

将分为:

"mkdir", "/home/user/My", "Documents"

这将导致 mkdir 命令失败,因为它需要一个参数,而不是两个。

面对这种情况,一些程序员试图在路径名周围添加引号。这也不起作用:

"mkdir \"/home/user/My Documents\""

将分为:

"mkdir", "\"/home/user/My", "Documents\""

在尝试引用空格时添加的额外双引号字符被视为与任何其他非空白字符一样。实际上,我们引用或转义空间的任何事情都将失败。

处理这个特殊问题的方法是使用 exec(String ...) 重载。

Process p = Runtime.exec("mkdir", dirPath);
// ...

如果 dirpath 包含空格字符,这将起作用,因为 exec 的这个重载不会尝试拆分参数。字符串按原样传递给 OS exec 系统调用。

重定向,管道和其他 shell 语法

假设我们要重定向外部命令的输入或输出,或运行管道。例如:

Process p = Runtime.exec("find / -name *.java -print 2>/dev/null");

要么

Process p = Runtime.exec("find source -name *.java | xargs grep package");

(第一个示例列出文件系统中所有 Java 文件的名称,第二个示例在树中的 Java 文件中打印 package 语句 2。

这些不会按预期工作。在第一种情况下,find 命令将以“2> / dev / null”作为命令参数运行。它不会被解释为重定向。在第二个示例中,管道字符(“|”)及其后面的工作将被赋予 find 命令。

这里的问题是 exec 方法和 ProcessBuilder 不理解任何 shell 语法。这包括重定向,管道,变量扩展,通配等。

在少数情况下(例如,简单的重定向),你可以使用 ProcessBuilder 轻松实现所需的效果。但是,一般情况下并非如此。另一种方法是在 shell 中运行命令行; 例如:

Process p = Runtime.exec("bash", "-c", 
                         "find / -name *.java -print 2>/dev/null");

要么

Process p = Runtime.exec("bash", "-c", 
                         "find source -name \\*.java | xargs grep package");

但请注意,在第二个示例中,我们需要转义通配符(“*”),因为我们希望通过 find 而不是 shell 来解释通配符。

Shell 内置命令不起作用

假设以下示例不适用于具有类 UNIX 的 shell 的系统:

Process p = Runtime.exec("cd", "/tmp");     // Change java app's home directory

要么

Process p = Runtime.exec("export", "NAME=value");  // Export NAME to the java app's environment

有几个原因可以解决这个问题:

  1. cdexport 命令是 shell 内置命令。它们不作为不同的可执行文件存在。

  2. 对于 shell 内置函数来执行它们应该执行的操作(例如,更改工作目录,更新环境),它们需要更改该状态所在的位置。对于普通应用程序(包括 Java 应用程序),状态与应用程序进程相关联。因此,例如,运行 cd 命令的子进程无法更改其父 java 进程的工作目录。同样,一个 exec’d 进程无法更改其后的进程的工作目录。

这种推理适用于所有 shell 内置命令。

1 - 你也可以使用 ProcessBuilder,但这与此示例无关。

2 - 这有点粗糙并且准备好……但是再一次,这种方法的失败与示例无关。