簡介
在開發中我們可能會遇到需要在程式中呼叫指令碼的需求,或者涉及到兩個語言之間的互動,筆者之前就遇到了需要在go中呼叫python的需求,然後在程式碼中應用了go-python3這個庫,實際上在go中呼叫python的指令碼也是一個解決之法。這片文章將介紹在go中執行shell指令碼的方法以及對其原始碼的相應解析。
程式用例
test_command.go
package learn
import (
"fmt"
"os/exec"
"testing"
)
func TestCmd(t *testing.T) {
if o, e := exec.Command("./test.sh", "1", "2").Output(); e != nil {
fmt.Println(e)
} else {
fmt.Println(string(o))
}
}
test.sh
#!/bin/bash
a=$1
b=$2
echo $a
echo $b
上面這個例子的意思是要執行test.sh這個指令碼,並且入參是1,2。指令碼里面寫的東西相對就比較簡單了,就是列印這兩個入參。其實問題的關鍵在於exec.Command()這個方法,下面我們來刨根問底,一探究竟。
原始碼解析
func Command(name string, arg ...string) *Cmd {
cmd := &Cmd{
Path: name,
Args: append([]string{name}, arg...),
}
if filepath.Base(name) == name {
if lp, err := LookPath(name); err != nil {
cmd.lookPathErr = err
} else {
cmd.Path = lp
}
}
return cmd
}
// Base返回path的最後一個元素。
// 在提取最後一個元素之前,將刪除尾部的路徑分隔符。
// 如果路徑為空,Base返回"."。
// 如果路徑完全由分隔符組成,Base返回單個分隔符。
func Base(path string) string {
if path == "" {
return "."
}
// Strip trailing slashes.
for len(path) > 0 && os.IsPathSeparator(path[len(path)-1]) {
path = path[0 : len(path)-1]
}
// Throw away volume name
path = path[len(VolumeName(path)):]
// Find the last element
i := len(path) - 1
for i >= 0 && !os.IsPathSeparator(path[i]) {
i--
}
if i >= 0 {
path = path[i+1:]
}
// If empty now, it had only slashes.
if path == "" {
return string(Separator)
}
return path
}
//LookPath在由PATH環境變數命名的目錄中搜尋一個名為file入參的可執行檔案。如果檔案包含一個斜線,就會直接嘗試,而不參考PATH。其結果可能是一個絕對路徑或相對於當前目錄的路徑。
func LookPath(file string) (string, error) {
if strings.Contains(file, "/") {
err := findExecutable(file)
if err == nil {
return file, nil
}
return "", &Error{file, err}
}
path := os.Getenv("PATH")
for _, dir := range filepath.SplitList(path) {
if dir == "" {
// Unix shell semantics: path element "" means "."
dir = "."
}
path := filepath.Join(dir, file)
if err := findExecutable(path); err == nil {
return path, nil
}
}
return "", &Error{file, ErrNotFound}
}
// 尋找file同名的可執行命令
func findExecutable(file string) error {
d, err := os.Stat(file)
if err != nil {
return err
}
if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
return nil
}
return os.ErrPermission
}
通過上面對exec.Command()原始碼的分析我們可以得知,這個函式只是尋找與path名字相同的可執行檔案並且構建了一個Cmd的物件返回。這裡值得注意的是,當我們輸入的path如果不是一個可執行的檔案的具體路徑,那麼就會去PATH環境變數中的註冊的路徑中找尋與path相同名字的命令,如果這個時候沒有找到就會報錯。
那麼接下來我們那看看這個Cmd是何方神聖呢,有什麼用,怎麼用呢。下面我們看看Cmd這個結構體裡都有些什麼東西。
// Cmd結構體代表一個準備或正在執行的外部命令
// 一個Cmd的物件不能在Run,Output或者CombinedOutput方法呼叫之後重複使用。
type Cmd struct {
// Path代表執行命令的路徑
// 這個欄位是唯一一個需要被賦值的欄位,不能是空字串,
// 並且如果Path是相對路徑,那麼參照的是Dir這個欄位的所指向的目錄
Path string
// Args這個欄位代表呼叫命令所需的引數,其中Path在執行命令時以Args[0]的形式存在
// 如果這個引數是空,那個就直接使用Path執行命令
//
// 在較為普遍普遍的場景裡面,Path和Args這兩個引數在呼叫命令的時候都會被用到
Args []string
// Env代表當前程式的環境變數
// 每個Env陣列中的條目都以“key=value”的形式存在
// 如果Env是nil,那邊執行命令所建立的程式將使用當前程式的環境變數
// 如果Env中存在重複的key,那麼會使用這個key中排在最後一個的值。
// 在Windows中存在特殊的情況, 如果系統中缺失了SYSTEMROOT,或者這個環境變數沒有被設定成空字串,那麼它操作都是追加操作。
Env []string
// Dir代表命令的執行路徑
// 如果Dir是空字串,那麼命令就會執行在當前程式的執行路徑
Dir string
// Stdin代表的是系統的標準輸入流
// 如果Stdin是一個*os.File,那麼程式的標準輸入將被直接連線到該檔案。
Stdin io.Reader
// Stdout表示標準輸出流
// 如果StdOut是一個*os.File,那麼程式的標準輸入將被直接連線到該檔案。
// 值得注意的是如果StdOut和StdErr是同一個物件,那麼同一時間只有一個協程可以呼叫Writer
Stdout io.Writer
Stderr io.Writer
// ExtraFiles指定由新程式繼承的額外開放檔案。它不包括標準輸入、標準輸出或標準錯誤。如果不為零,第i項成為檔案描述符3+i。
// ExtraFiles前面三個元素分別放的是stdin,stdout,stderr
// ExtraFiles在Windows上是不支援的
ExtraFiles []*os.File
SysProcAttr *syscall.SysProcAttr
// 當命令執行之後,Process就是該命令執行所代表的程式
Process *os.Process
// ProcessState包含關於一個退出的程式的資訊,在呼叫Wait或Run後可用。
ProcessState *os.ProcessState
ctx context.Context // ctx可以用來做超時控制
lookPathErr error // 如果在呼叫LookPath尋找路徑的時候出錯了,就賦值到這個欄位
finished bool // 當Wait被呼叫了一次之後就會被設定成True,防止被重複呼叫
childFiles []*os.File
closeAfterStart []io.Closer
closeAfterWait []io.Closer
goroutine []func() error //一系列函式,在呼叫Satrt開始執行命令的時候會順帶一起執行這些函式。每個函式分配一個goroutine執行
errch chan error // 與上一個欄位聯合使用,通過這個chan將上面函式執行的結果傳到當前goroutine
waitDone chan struct{}
}
上面我們對Cmd這個結構體的一些欄位做了解析,可以理解為Cmd就是對一個命令生命週期內的抽象。下面我們來分析Cmd的一下方法,看看他是怎麼使用的。
// Run方法開始執行這個命令並等待它執行結束
// 如果命令執行,在複製stdin、stdout和stder時沒有問題,並且以零退出狀態退出,則返回的錯誤為nil。
// 如果命令啟動但沒有成功完成,錯誤型別為型別為*ExitError。在其他情況下可能會返回其他錯誤型別。
// 如果呼叫的goroutine已經用runtime.LockOSThread鎖定了作業系統執行緒,並修改了任何可繼承的OS級 執行緒狀態(例如,Linux或Plan 9名稱空間),新的 程式將繼承呼叫者的執行緒狀態。
func (c *Cmd) Run() error {
if err := c.Start(); err != nil {
return err
}
return c.Wait()
}
// Start方法啟動指定的命令,但不等待它完成。
//
// 如果Start成功返回,c.Process欄位將被設定。
//
// 一旦命令執行完成,Wait方法將返回退出程式碼並釋放相關資源。
func (c *Cmd) Start() error {
if c.lookPathErr != nil {
c.closeDescriptors(c.closeAfterStart)
c.closeDescriptors(c.closeAfterWait)
return c.lookPathErr
}
if runtime.GOOS == "windows" {
lp, err := lookExtensions(c.Path, c.Dir)
if err != nil {
c.closeDescriptors(c.closeAfterStart)
c.closeDescriptors(c.closeAfterWait)
return err
}
c.Path = lp
}
if c.Process != nil {
return errors.New("exec: already started")
}
if c.ctx != nil {
select {
case <-c.ctx.Done():
c.closeDescriptors(c.closeAfterStart)
c.closeDescriptors(c.closeAfterWait)
return c.ctx.Err()
default:
}
}
//初始化並填充ExtraFiles
c.childFiles = make([]*os.File, 0, 3+len(c.ExtraFiles))
type F func(*Cmd) (*os.File, error)
//在這裡會呼叫stdin,stdout和stderr方法,如果Cmd的StdIn,StdOut,StdErr不是nil,就會將相關的copy任務封裝成func放在goroutine欄位中,等待在Start方法執行的時候呼叫。
for _, setupFd := range []F{(*Cmd).stdin, (*Cmd).stdout, (*Cmd).stderr} {
fd, err := setupFd(c)
if err != nil {
c.closeDescriptors(c.closeAfterStart)
c.closeDescriptors(c.closeAfterWait)
return err
}
c.childFiles = append(c.childFiles, fd)
}
c.childFiles = append(c.childFiles, c.ExtraFiles...)
// 如果cmd的Env沒有賦值,那麼就用當前程式的環境變數
envv, err := c.envv()
if err != nil {
return err
}
// 會用這個命令啟動一個新的程式
// 在Linux的系統上,底層是呼叫了Frok來建立另一個程式,由於文章篇幅有限,就不對此處進行詳細分析了,詳情可看延伸閱讀
c.Process, err = os.StartProcess(c.Path, c.argv(), &os.ProcAttr{
Dir: c.Dir,
Files: c.childFiles,
Env: addCriticalEnv(dedupEnv(envv)),
Sys: c.SysProcAttr,
})
if err != nil {
c.closeDescriptors(c.closeAfterStart)
c.closeDescriptors(c.closeAfterWait)
return err
}
c.closeDescriptors(c.closeAfterStart)
// 除非有goroutine要啟動,否則不會申請Chan
if len(c.goroutine) > 0 {
c.errch = make(chan error, len(c.goroutine))
for _, fn := range c.goroutine {
go func(fn func() error) {
c.errch <- fn()
}(fn)
}
}
// 超時控制
if c.ctx != nil {
c.waitDone = make(chan struct{})
go func() {
select {
case <-c.ctx.Done(): //如果超時了,就Kill掉執行命令的程式
c.Process.Kill()
case <-c.waitDone:
}
}()
}
return nil
}
func (c *Cmd) stdin() (f *os.File, err error) {
if c.Stdin == nil {
f, err = os.Open(os.DevNull)
if err != nil {
return
}
c.closeAfterStart = append(c.closeAfterStart, f)
return
}
if f, ok := c.Stdin.(*os.File); ok {
return f, nil
}
//Pipe返回一對相連的Files;從r讀出的資料返回寫到w的位元組。
pr, pw, err := os.Pipe()
if err != nil {
return
}
c.closeAfterStart = append(c.closeAfterStart, pr)
c.closeAfterWait = append(c.closeAfterWait, pw)
//將相關的任務新增到goroutine中
c.goroutine = append(c.goroutine, func() error {
_, err := io.Copy(pw, c.Stdin)
if skip := skipStdinCopyError; skip != nil && skip(err) {
err = nil
}
if err1 := pw.Close(); err == nil {
err = err1
}
return err
})
return pr, nil
}
func (c *Cmd) stdout() (f *os.File, err error) {
return c.writerDescriptor(c.Stdout)
}
func (c *Cmd) stderr() (f *os.File, err error) {
if c.Stderr != nil && interfaceEqual(c.Stderr, c.Stdout) {
return c.childFiles[1], nil
}
return c.writerDescriptor(c.Stderr)
}
func (c *Cmd) writerDescriptor(w io.Writer) (f *os.File, err error) {
if w == nil {
f, err = os.OpenFile(os.DevNull, os.O_WRONLY, 0)
if err != nil {
return
}
c.closeAfterStart = append(c.closeAfterStart, f)
return
}
if f, ok := w.(*os.File); ok {
return f, nil
}
pr, pw, err := os.Pipe()
if err != nil {
return
}
c.closeAfterStart = append(c.closeAfterStart, pw)
c.closeAfterWait = append(c.closeAfterWait, pr)
//將相關的任務新增到goroutine中
c.goroutine = append(c.goroutine, func() error {
_, err := io.Copy(w, pr)
pr.Close() // in case io.Copy stopped due to write error
return err
})
return pw, nil
}
// 等待命令退出,並等待任何複製到stdin或從stdout或stderr複製的完成。
// 在呼叫Wait之前,Start方法必須被呼叫
// 如果命令執行,在複製stdin、stdout和stder時沒有問題,並且以零退出狀態退出,則返回的錯誤為nil。
// 如果命令執行失敗或沒有成功完成,錯誤型別為*ExitError。對於I/O問題可能會返回其他錯誤型別。
// 如果c.Stdin、c.Stdout或c.Stderr中的任何一個不是*os.File,Wait也會等待各自的I/O迴圈複製到程式中或從程式中複製出來
//
// Wait釋放與Cmd相關的任何資源。
func (c *Cmd) Wait() error {
if c.Process == nil {
return errors.New("exec: not started")
}
if c.finished {
return errors.New("exec: Wait was already called")
}
c.finished = true
//等待程式執行完畢並退出
state, err := c.Process.Wait()
if c.waitDone != nil {
close(c.waitDone)
}
c.ProcessState = state
//檢查goroutine欄位上面的函式執行有沒有錯誤
var copyError error
for range c.goroutine {
if err := <-c.errch; err != nil && copyError == nil {
copyError = err
}
}
c.closeDescriptors(c.closeAfterWait)
if err != nil {
return err
} else if !state.Success() {
return &ExitError{ProcessState: state}
}
return copyError
}
// 輸出執行該命令並返回其標準輸出。
// 任何返回的錯誤通常都是*ExitError型別的。
// OutPut實際上是封裝了命令的執行流程並且制定了命令的輸出流
func (c *Cmd) Output() ([]byte, error) {
if c.Stdout != nil {
return nil, errors.New("exec: Stdout already set")
}
var stdout bytes.Buffer
c.Stdout = &stdout
captureErr := c.Stderr == nil
if captureErr {
c.Stderr = &prefixSuffixSaver{N: 32 << 10}
}
err := c.Run()
if err != nil && captureErr {
if ee, ok := err.(*ExitError); ok {
ee.Stderr = c.Stderr.(*prefixSuffixSaver).Bytes()
}
}
return stdout.Bytes(), err
}
在上面的方法分析之中我們可以看出執行一個命令的流程是Run-> Start->Wait,等待命令執行完成。並且在Start的時候會起來一個新的程式來執行命令。基於上面我們對Cmd的一頓分析,筆者感覺在文章開頭寫的測試程式碼實在是乏善可陳,因為Cmd封裝了挺多東西的,我們在工作中完全可以充分利用他封裝的功能,比如設定超時時間,設定標準輸入流或者標準輸出流,還可以定製化設定這個命令執行的環境變數等等。。。。
延伸閱讀
- 關於fork和exec:www.cnblogs.com/hicjiajia/archive/...
本作品採用《CC 協議》,轉載必須註明作者和本文連結