Go 標準庫之 os/exec(執行外部命令、非阻塞等待、阻塞等待、命令輸出)

1. 概述

golang 下的 os/exec 包執行外部命令包執行外部命令。它包裝了 os.StartProcess 函數以便更容易的修正輸入和輸出,使用管道連接I/O,以及作其它的一些調整。

C 語言或者其他語言中的“系統”庫調用不同, os/exec 包並不調用系統 shell ,也不展開任何 glob (正則匹配)模式,也不處理通常由 shell 完成的其他擴展、管道或重定向。

2. 相關函數

2.1 Variables

var ErrNotFound = errors.New("executable file not found in $PATH")

如果路徑搜索沒有找到可執行文件時,就會返回本錯誤。

2.2 type Error

type Error struct {
    Name string
    Err  error
}

Error類型記錄執行失敗的程序名和失敗的原因。對應的方法如下:

func (e *Error) Error() string

2.3 type ExitError

type ExitError struct {
    *os.ProcessState
}

ExitError 報告某個命令的一次未成功的返回。對應的方法如下:

func (e *ExitError) Error() string

2.4 func LookPath

func LookPath(file string) (string, error)

2.5 type cmd

    func Command(name string, arg ...string) *Cmd
    func (c *Cmd) StdinPipe() (io.WriteCloser, error)
    func (c *Cmd) StdoutPipe() (io.ReadCloser, error)
    func (c *Cmd) StderrPipe() (io.ReadCloser, error)
    func (c *Cmd) Run() error
    func (c *Cmd) Start() error
    func (c *Cmd) Wait() error
    func (c *Cmd) Output() ([]byte, error)
    func (c *Cmd) CombinedOutput() ([]byte, error)

3. 函數詳解

3.1 func LookPath

函數定義

func LookPath(file string) (string, error)

在環境變量 PATH 指定的目錄中搜索可執行文件,如 file 中有斜槓,則直接根據絕對路徑或者相對本目錄的相對路徑去查找。返回完整路徑或者相對於當前目錄的一個相對路徑。

默認在系統的環境變量裏查找給定的可執行命令文件。

使用示例:

package main

import (
	"fmt"
	"os/exec"
)

func main() {

	f, err := exec.LookPath("pwd")
	if err != nil {
		fmt.Println("Not find the cmd")
	}
	fmt.Printf("Binary file path is %s", f) // /bin/pwd
}

3.2 Cmd 結構體

Cmd 代表一個正在準備或者在執行中的外部命令。

type Cmd struct {
    // Path是將要執行的命令的路徑。
    //
    // 該字段不能爲空,如爲相對路徑會相對於Dir字段。
    Path string
    // Args保管命令的參數,包括命令名作爲第一個參數;如果爲空切片或者nil,相當於無參數命令。
    //
    // 典型用法下,Path和Args都應被Command函數設定。
    Args []string
    // Env指定進程的環境,如爲nil,則是在當前進程的環境下執行。
    Env []string
    // Dir指定命令的工作目錄。如爲空字符串,會在調用者的進程當前目錄下執行。
    Dir string
    // Stdin指定進程的標準輸入,如爲nil,進程會從空設備讀取(os.DevNull)
    Stdin io.Reader
    // Stdout和Stderr指定進程的標準輸出和標準錯誤輸出。
    //
    // 如果任一個爲nil,Run方法會將對應的文件描述符關聯到空設備(os.DevNull)
    //
    // 如果兩個字段相同,同一時間最多有一個線程可以寫入。
    Stdout io.Writer
    Stderr io.Writer
    // ExtraFiles指定額外被新進程繼承的已打開文件流,不包括標準輸入、標準輸出、標準錯誤輸出。
    // 如果本字段非nil,entry i會變成文件描述符3+i。
    //
    // BUG: 在OS X 10.6系統中,子進程可能會繼承不期望的文件描述符。
    // http://golang.org/issue/2603
    ExtraFiles []*os.File
    // SysProcAttr保管可選的、各操作系統特定的sys執行屬性。
    // Run方法會將它作爲os.ProcAttr的Sys字段傳遞給os.StartProcess函數。
    SysProcAttr *syscall.SysProcAttr
    // Process是底層的,只執行一次的進程。
    Process *os.Process
    // ProcessState包含一個已經存在的進程的信息,只有在調用Wait或Run後纔可用。
    ProcessState *os.ProcessState
    // 內含隱藏或非導出字段
}

注: exec 在執行調用系統命令時,會先對需要執行的操作進行一次封裝,然後再執行。封裝後的命令對象具有以上 struct 屬性。而封裝方式即使用下邊的 Command 函數。

3.3 func Command

func Command(name string, arg ...string) *Cmd

函數返回一個 *Cmd ,用於使用給出的參數執行 name 指定的程序。返回值只設定了 PathArgs 兩個參數。
如果 name 不含路徑分隔符,將使用 LookPath 獲取完整路徑;否則直接使用 name 。參數 arg 不應包含命令名。

使用示例:

func main() {
	cmd := exec.Command("go", "version")
	fmt.Printf("cmd.Path is %s, cmd.Args is %s", cmd.Path, cmd.Args)
    // cmd.Path is /usr/local/go/bin/go, cmd.Args is [go version]
}

注意:以上操作只會將命令進行封裝,相當於告訴系統將進行哪些操作,但是執行時無法獲取相關信息。所以說Cmd 代表一個正在準備或者在執行中的外部命令。

3.4 func (*Cmd) Run

func (c *Cmd) Run() error

Run 執行 c 包含的命令,並阻塞直到完成。

  • 如果命令成功執行, stdinstdoutstderr 的轉交沒有問題,並且返回狀態碼爲 0,方法的返回值爲 nil
  • 如果命令沒有執行或者執行失敗,會返回 *ExitError 類型的錯誤;否則返回的 error 可能是表示 I/O 問題。

使用示例:

func main() {
	cmd := exec.Command("go", "version")
	fmt.Printf("cmd.Path is %s, cmd.Args is %s", cmd.Path, cmd.Args)
	err := cmd.Run()
	if err != nil {
		fmt.Println("cmd run failed")
	}

}

3.5 func (*Cmd) Start

func (c *Cmd) Start() error

Start 開始執行 c 包含的命令,但並不會等待該命令完成即返回。 Wait 方法會返回命令的返回狀態碼並在命令返回後釋放相關的資源。

3.6 func (*Cmd) Wait

func (c *Cmd) Wait() error

Wait 會阻塞直到該命令執行完成,該命令必須是被 Start 方法開始執行的

  • 如果命令成功執行, stdinstdoutstderr 的轉交沒有問題,並且返回狀態碼爲 0,方法的返回值爲 nil
  • 如果命令沒有執行或者執行失敗,會返回 *ExitError 類型的錯誤;否則返回的 error 可能是表示 I/O 問題。 Wait 方法會在命令返回後釋放相關的資源。

使用示例:

func main() {
	cmd := exec.Command("go", "version")
	fmt.Printf("cmd.Path is %s, cmd.Args is %s", cmd.Path, cmd.Args)
	err := cmd.Start()
	if err != nil {
		fmt.Println("cmd run failed")
	}

	err = cmd.Wait()
	if err != nil {
		fmt.Println("cmd wait failed")
	}

}

3.7 Start 和 Run 的區別

使用示例:

func main() {
	cmd := exec.Command("sleep", "5")

	// err := cmd.Run() 執行 Run 會立即阻塞等待 5 秒種
	err := cmd.Start()
	if err != nil {
		fmt.Println("cmd run failed")
	}
	fmt.Println("waiting for cmd ...")
	// 執行 Start() 上面的打印會先執行,然後在 Wait() 方法阻塞等待 5s
	err = cmd.Wait()
	if err != nil {
		fmt.Println("cmd wait failed")
	}

}

注意:
一個命令只能使用 Start() 或者 Run() 中的一個啓動命令,不能兩個同時使用。

3.8 func CombinedOutput

func (c *Cmd) CombinedOutput() ([]byte, error)

執行命令並返回標準輸出和錯誤輸出合併的切片。

3.9 func Output

func (c *Cmd) Output() ([]byte, error)

執行命令並返回標準輸出的切片。

使用示例:

func main() {
	cmd := exec.Command("ls", "-a", "-l")

	// err := cmd.Start()
	// if err != nil {
	// 	fmt.Println("cmd run failed")
	// }
	// fmt.Println("waiting for cmd ...")

	// err = cmd.Wait()
	// if err != nil {
	// 	fmt.Println("cmd wait failed")
	// }

	out, err := cmd.Output()
	if err != nil {
		fmt.Println("cmd Output failed ", err)
	}
	fmt.Printf("out is %s", out)
}

注意:

  1. Output()CombinedOutput() 不能夠同時使用,因爲 command 的標準輸出只能有一個,同時使用的話便會定義了兩個,便會報錯;
  2. CommandOutput 之間不需要再增加 StartWait 方法,否則也會報錯;

3.10 func (*Cmd) StdinPipe

func (c *Cmd) StdinPipe() (io.WriteCloser, error)

StdinPipe 方法返回一個在命令 Start 後與命令標準輸入關聯的管道。 Wait 方法獲知命令結束後會關閉這個管道。必要時調用者可以調用 Close 方法來強行關閉管道,例如命令在輸入關閉後纔會執行返回時需要顯式關閉管道。

3.11 func (*Cmd) StdoutPipe

func (c *Cmd) StdoutPipe() (io.ReadCloser, error)

StdoutPipe 方法返回一個在命令 Start 後與命令標準輸出關聯的管道。 Wait 方法獲知命令結束後會關閉這個管道,一般不需要顯式的關閉該管道。但是在從管道讀取完全部數據之前調用 Wait 是錯誤的;同樣使用 StdoutPipe 方法時調用 Run 函數也是錯誤的。

3.12 func (*Cmd) StderrPipe

func (c *Cmd) StderrPipe() (io.ReadCloser, error)

StderrPipe 方法返回一個在命令 Start 後與命令標準錯誤輸出關聯的管道。 Wait 方法獲知命令結束後會關閉這個管道,一般不需要顯式的關閉該管道。但是在從管道讀取完全部數據之前調用 Wait 是錯誤的;同樣使用 StderrPipe 方法時調用 Run 函數也是錯誤的。

參考資料:

  1. https://blog.csdn.net/u013256816/article/details/99670090
  2. https://studygolang.com/static/pkgdoc/pkg/os_exec.htm
  3. https://www.cnblogs.com/sunailong/p/7852216.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章