👨‍💻 簡介

昨天講到 os package,今天繼續補充 os package底下的 exec package,這個package主要用來執行外部指令和處理指令的輸入和輸出,包括如何設定指令、執行指令以及處理輸出等等。

設定指令

要執行外部指令,首先需要設定要執行的主要指令:

  • exec.Command:接受一個指令名稱和多個參數,並返回一個 Cmd 對象。

語法如下:

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

第一個參數是放要執行的命令名稱,第二個則是放參數。 接著來看看exec.Cmd是甚麼東西:

type Cmd struct {
	Path string        // the path of the command to run
	Args []string      // holds command line arguments
	Env []string       // the environment of the process
	Dir string         // the working directory of the command.
	Stdin io.Reader    // the process's standard input.
        Stdout io.Writer   // the process's standard output and error.
        Stderr io.Writer
    ....
}

可以看到exec.Command會回傳一個Cmd的struct。

以下是一個設定指令的簡單範例:

package main

import (
	"fmt"
	"os/exec"
)

func main() {

	cmd := exec.Command("ls", "-al")
	err := cmd.Run()
	if err != nil {
		fmt.Printf("failed to call cmd.Run(): %v", err)
	}
}

執行完沒有任何輸出是正常的,因為這個範例並沒有將標準輸出印出。如果要印出執行結果將cmd.Run改為cmd.Output即可:

package main

import (
	"fmt"
	"os/exec"
)

func main() {

	cmd := exec.Command("ls", "-al")
	output, err := cmd.Output()
	if err != nil {
		fmt.Printf("failed to call cmd.Output(): %v", err)
		return
	}

	fmt.Println("Command Output:")
	fmt.Println(string(output))
}

接著來快速介紹Cmd struct常用的欄位:

package main

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

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

	// 設定執行位置
	cmd.Dir = "/"
	
	// 將標準輸出和標準錯誤輸出到終端機
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	// 執行並等待其完成
	err := cmd.Run()
	if err != nil {
		fmt.Println(err)
		return
	}
}

執行指令

  • cmd.Run:執行指令並會等待指令完成,是cmd.Startcmd.Wait的結合。
  • cmd.Start:執行指令但不會等待指令完成。
  • cmd.Wait:等待已經執行的指令,會阻塞程式,職到指令執行完畢。
package main

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

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

	// 執行指令
	err := cmd.Start()
	if err != nil {
		fmt.Println(err)
		return
	}

	// 等待指令完成
	err = cmd.Wait()
	if err != nil {
	    log.Fatal(err)
	}
}

處理輸出

  • cmd.Output:取得執行的標準輸出。
  • cmd.CombinedOutput:取得執行的標準輸出和標準錯誤的合併結果。

取得標準輸出

package main

import (
	"fmt"
	"os/exec"
)

func main() {
	cmd := exec.Command("echo", "Hello, World!")

	// 取得執行的標準輸出
	output, err := cmd.Output()
	if err != nil {
		fmt.Println("failed to call cmd.Output(): %v", err)
		return
	}

	fmt.Println("Command Output:")
	fmt.Println(string(output))
}

取得標準輸出和標準錯誤的合併結果

package main

import (
	"fmt"
	"os/exec"
)

func main() {
	cmd := exec.Command("ls", "non-existent-directory")

	// 取得執行的標準輸出和標準錯誤的合併結果
	output, err := cmd.CombinedOutput()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Println("Command Output:")
	fmt.Println(string(output))
}

輸出的多種方式

因為cmd 的struct包含以下三個欄位,分別代表執行的標準輸入,標準輸出以及標準錯誤輸出,而io.Reader以及io.Writer皆為interface,因此只要滿足interface method的struct,即可完成讀取跟寫入

type Cmd struct {
	Stdin io.Reader    // the process's standard input.
        Stdout io.Writer   // the process's standard output and error.
        Stderr io.Writer
    ....
}
  • 寫入檔案
package main

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

func main() {
	// 建立一個檔案,用於存儲標準輸出
	outFile, err := os.Create("output.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer outFile.Close()

	// 建立一個檔案,用於存儲標準錯誤
	errFile, err := os.Create("error.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer errFile.Close()

	// 設定執行指令,將標準輸出和標準錯誤重定向到檔案
	cmd := exec.Command("ls", "-l")
	cmd.Stdout = outFile
	cmd.Stderr = errFile

	// 執行並等待其完成
	err = cmd.Run()
	if err != nil {
		fmt.Println(err)
		return
	}
}
  • 寫到終端
package main

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

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

	// 將標準輸出和標準錯誤輸出到終端機
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	// 執行指令
	err := cmd.Start()
	if err != nil {
		fmt.Println(err)
		return
	}

	// 等待指令完成
	err = cmd.Wait()
	if err != nil {
	    log.Fatal(err)
	}
}

因os.Create、os.Stdout和os.Stderr都會返回一個File struct,而該struct有滿足io.Writer的method -> Write,因此可以將標準輸出以及標準錯誤輸出給寫入檔案或是終端裡。

處理輸入

透過wc指令讀取main.go檔案,並計算總共多少行。

package main

import (
	"log"
	"os"
	"os/exec"
)

func main() {
	stdin, err := os.Open("main.go")
	if err != nil {
		log.Fatalf("failed to open file: %v", err)
	}
	cmd := exec.Command("wc", "-l")
	cmd.Stdin = stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err = cmd.Run()
	if err != nil {
		log.Fatalf("failed to call cmd.Run(): %v", err)
	}
}

Pipeline操作

  • cmd.StderrPipe:返回一個連接到執行的標準錯誤的pipeline。
  • cmd.StdinPipe:返回一個連接到執行的標準輸入的pipeline。
  • cmd.StdoutPipe:返回一個連接到執行的標準輸出的pipeline。
package main

import (
	"log"
	"os"
	"os/exec"
)

func main() {
	cmdCat := exec.Command("cat", "main.go")
	cmdWC := exec.Command("wc", "-l")

	// 建立一個pipeline,將 cat main.go 的輸出內容存放到 catout
	catout, err := cmdCat.StdoutPipe()
	if err != nil {
		log.Fatalf("failed to get StdoutPipe of cat: %v", err)
	}

	cmdWC.Stdin = catout     // 將 catout 當作 wc -l 的輸入
	cmdWC.Stdout = os.Stdout // 將 wc -l 的結果印到終端機上

	err = cmdCat.Start()
	if err != nil {
		log.Fatalf("failed to call cmdCat.Start():%v", err)
	}

	err = cmdWC.Start()
	if err != nil {
		log.Fatalf("failed to call cmdWC.Start():%v", err)
	}

	cmdCat.Wait()
	cmdWC.Wait()
}

通用pipeline方法

package main

import (
	"fmt"
	"os/exec"
)

func main() {
	cmdCat := exec.Command("cat", "main.go")
	cmdWC := exec.Command("wc", "-l")
	data, err := pipeCommands(cmdCat, cmdWC)
	if err != nil {
		fmt.Printf("failed to call pipeCommands(): %v", err)
	}
	fmt.Printf("output: %s", data)
}
func pipeCommands(commands ...*exec.Cmd) ([]byte, error) {
	for i, command := range commands[:len(commands)-1] {
		out, err := command.StdoutPipe()
		if err != nil {
			return nil, err
		}
		command.Start()
		commands[i+1].Stdin = out
	}
	final, err := commands[len(commands)-1].Output()
	if err != nil {
		return nil, err
	}
	return final, nil
}

bash pipeline

cmd := exec.Command("bash", "-c", "cat main.go| wc -l")
data, err := cmd.CombinedOutput()
if err != nil {
	log.Fatalf("failed to call pipeCommands(): %v", err)
}
   log.Printf("output: %s", data)

📚 參考資料