HOWTO · Go

如何在 Go 中執行 Shell 命令

了解如何使用 os/exec 從 Go 執行外部程式、擷取輸出、處理錯誤、傳遞參數,以及設定命令的工作目錄。Shell 功能需要明確呼叫 shell。

本頁內容

若要從 Go 執行外部程式,請使用標準函式庫的 os/exec 套件。它直接啟動指定的執行檔;它不會隱含地執行 shell。因此,管道、重新導向、變數展開和萬用字元等 shell 功能不會被套用。如果工作需要這些功能,請明確呼叫 shell,並將其腳本作為參數傳遞,同時注意不要將不受信任的文字放入該腳本中。

使用 os/exec 執行命令

使用 exec.Command 建立命令,然後呼叫 Output 等方法來啟動它並收集其標準輸出。第一個參數指定執行檔,其餘參數會傳遞給該程式。這個完整的程式會以一個參數將自己啟動為子程序。子程序會印出固定的一行,父程序則擷取該標準輸出並將其寫入自己的標準輸出。程式透過父程序印出子程序的那一行。

package main

import (
	"fmt"
	"os"
	"os/exec"
)

func main() {
	if len(os.Args) > 1 && os.Args[1] == "child" {
		fmt.Println("hello from child")
		return
	}

	cmd := exec.Command(os.Args[0], "child")
	output, err := cmd.Output()
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	fmt.Print(string(output))
}
hello from child

在這裡,os.Args[0] 標識當前的執行檔,因此本範例不依賴於是否安裝了單獨的公用程式。在應用程式中,將其替換為您需要的執行檔,例如 git 或 go,並分別提供該程式的參數。

擷取輸出並處理錯誤

在使用結果之前,請檢查 Output 或 CombinedOutput 傳回的錯誤。無法啟動程式(例如找不到執行檔)與程式啟動後以非零狀態結束是不同的。在後一種情況下,錯誤通常是 *exec.ExitError;當結束狀態重要時請檢查它。Cmd 類型還可讓您直接連接串流:當您希望將輸出傳送到某處而不是收集在位元組切片中時,請將 cmd.Stdout 或 cmd.Stderr 設定為寫入器。

當標準輸出是您的程式所需的資料時,請使用 Output。當您還需要在傳回的位元組中包含標準錯誤時,請使用 CombinedOutput;這兩個串流會被合併,因此它們的相對順序並不是重建個別串流的可靠方法。當診斷需要保持分開時,請為 cmd.Stdout 和 cmd.Stderr 分配個別的寫入器。在呼叫方可以決定是否重試、返回或繼續的位置處理並報告錯誤;印出錯誤然後將命令視為成功,可能會隱藏失敗的操作。

將參數傳遞給命令

將每個參數作為自己的字串傳遞到執行檔名稱之後,例如 exec.Command("grep", "-n", pattern, filename)。Go 不會將這些值連接成 shell 命令,也不會為您解釋空格、引號、管道或重新導向字元。包含空格的檔案名在作為一個字串提供時仍是一個參數。這可以避免 shell 解析,但不會為被呼叫的程式驗證輸入:參數對該程式仍可能具有特殊意義,因此請根據命令自己的介面驗證值。

對於輸入和輸出串流,將 io.Reader 分配給 cmd.Stdin,將寫入器分配給 cmd.Stdout 或 cmd.Stderr,然後呼叫 Run。上面的範例執行檔使用 Output,因為它只需要擷取一個短的結果。當您想要連接串流且不需要將標準輸出作為傳回值收集時,請使用 Run。執行檔必須可供程序使用,其選項在不同作業系統上可能有所不同。

在不同的目錄中執行命令

在啟動命令之前設定 cmd.Dir 以選擇其工作目錄。例如,在建構命令後,設定 cmd.Dir = projectDir,然後呼叫 Output、CombinedOutput 或 Run。該目錄必須存在且程序可以存取。相對的 cmd.Dir 是相對於 Go 程序的當前工作目錄來解析的;當呼叫方的起始目錄可能變化時,請使用絕對路徑。此設定會變更子程序的目錄,不會變更 Go 程序自己的工作目錄。