陷阱 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 - 這有點粗糙並且準備好……但是再一次,這種方法的失敗與示例無關。