您好,登錄后才能下訂單哦!
這篇文章主要介紹“Go執行腳本命令的使用實例分析”,在日常操作中,相信很多人在Go執行腳本命令的使用實例分析問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Go執行腳本命令的使用實例分析”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
在開發中我們可能會遇到需要在程序中調用腳本的需求,或者涉及到兩個語言之間的交互,筆者之前就遇到了需要在go中調用python的需求,然后在代碼中應用了go-python3這個庫,實際上在go中調用python的腳本也是一個解決之法。這片文章將介紹在go中運行shell腳本的方法以及對其源碼的相應解析。
test_command.go
package learnimport ( "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/basha=$1b=$2echo $aecho $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}
到此,關于“Go執行腳本命令的使用實例分析”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。