溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Go執(zhí)行腳本命令的使用實例分析

發(fā)布時間:2021-12-13 15:35:24 來源:億速云 閱讀:130 作者:iii 欄目:編程語言

這篇文章主要介紹“Go執(zhí)行腳本命令的使用實例分析”,在日常操作中,相信很多人在Go執(zhí)行腳本命令的使用實例分析問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Go執(zhí)行腳本命令的使用實例分析”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!                            

簡介

在開發(fā)中我們可能會遇到需要在程序中調(diào)用腳本的需求,或者涉及到兩個語言之間的交互,筆者之前就遇到了需要在go中調(diào)用python的需求,然后在代碼中應(yīng)用了go-python3這個庫,實際上在go中調(diào)用python的腳本也是一個解決之法。這片文章將介紹在go中運行shell腳本的方法以及對其源碼的相應(yīng)解析。

程序用例

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這個腳本,并且入?yún)⑹?,2。腳本里面寫的東西相對就比較簡單了,就是打印這兩個入?yún)?。其實問題的關(guān)鍵在于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環(huán)境變量命名的目錄中搜索一個名為file入?yún)⒌目蓤?zhí)行文件。如果文件包含一個斜線,就會直接嘗試,而不參考PATH。其結(jié)果可能是一個絕對路徑或相對于當(dāng)前目錄的路徑。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同名的可執(zhí)行命令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()源碼的分析我們可以得知,這個函數(shù)只是尋找與path名字相同的可執(zhí)行文件并且構(gòu)建了一個Cmd的對象返回。這里值得注意的是,當(dāng)我們輸入的path如果不是一個可執(zhí)行的文件的具體路徑,那么就會去PATH環(huán)境變量中的注冊的路徑中找尋與path相同名字的命令,如果這個時候沒有找到就會報錯。

那么接下來我們那看看這個Cmd是何方神圣呢,有什么用,怎么用呢。下面我們看看Cmd這個結(jié)構(gòu)體里都有些什么東西。

// Cmd結(jié)構(gòu)體代表一個準備或正在執(zhí)行的外部命令// 一個Cmd的對象不能在Run,Output或者CombinedOutput方法調(diào)用之后重復(fù)使用。type Cmd struct {
   // Path代表運行命令的路徑
   // 這個字段是唯一一個需要被賦值的字段,不能是空字符串,
   // 并且如果Path是相對路徑,那么參照的是Dir這個字段的所指向的目錄
   Path string

   // Args這個字段代表調(diào)用命令所需的參數(shù),其中Path在運行命令時以Args[0]的形式存在
   // 如果這個參數(shù)是空,那個就直接使用Path運行命令
   //
   // 在較為普遍普遍的場景里面,Path和Args這兩個參數(shù)在調(diào)用命令的時候都會被用到
   Args []string

   // Env代表當(dāng)前進程的環(huán)境變量
   // 每個Env數(shù)組中的條目都以“key=value”的形式存在
   // 如果Env是nil,那邊運行命令所創(chuàng)建的進程將使用當(dāng)前進程的環(huán)境變量
   // 如果Env中存在重復(fù)的key,那么會使用這個key中排在最后一個的值。
   // 在Windows中存在特殊的情況, 如果系統(tǒng)中缺失了SYSTEMROOT,或者這個環(huán)境變量沒有被設(shè)置成空字符串,那么它操作都是追加操作。
   Env []string

   // Dir代表命令的運行路徑
   // 如果Dir是空字符串,那么命令就會運行在當(dāng)前進程的運行路徑
   Dir string

   // Stdin代表的是系統(tǒng)的標準輸入流
   // 如果Stdin是一個*os.File,那么進程的標準輸入將被直接連接到該文件。
   Stdin io.Reader   // Stdout表示標準輸出流
   // 如果StdOut是一個*os.File,那么進程的標準輸入將被直接連接到該文件。
   // 值得注意的是如果StdOut和StdErr是同一個對象,那么同一時間只有一個協(xié)程可以調(diào)用Writer
   Stdout io.Writer
   Stderr io.Writer   // ExtraFiles指定由新進程繼承的額外開放文件。它不包括標準輸入、標準輸出或標準錯誤。如果不為零,第i項成為文件描述符3+i。
   // ExtraFiles前面三個元素分別放的是stdin,stdout,stderr
   // ExtraFiles在Windows上是不支持的
   ExtraFiles []*os.File

   SysProcAttr *syscall.SysProcAttr   // 當(dāng)命令運行之后,Process就是該命令運行所代表的進程
   Process *os.Process   // ProcessState包含關(guān)于一個退出的進程的信息,在調(diào)用Wait或Run后可用。
   ProcessState *os.ProcessState

   ctx             context.Context // ctx可以用來做超時控制
   lookPathErr     error           // 如果在調(diào)用LookPath尋找路徑的時候出錯了,就賦值到這個字段
   finished        bool                // 當(dāng)Wait被調(diào)用了一次之后就會被設(shè)置成True,防止被重復(fù)調(diào)用     
   childFiles      []*os.File
   closeAfterStart []io.Closer
   closeAfterWait  []io.Closer
   goroutine       []func() error  //一系列函數(shù),在調(diào)用Satrt開始執(zhí)行命令的時候會順帶一起執(zhí)行這些函數(shù)。每個函數(shù)分配一個goroutine執(zhí)行
   errch           chan error             // 與上一個字段聯(lián)合使用,通過這個chan將上面函數(shù)執(zhí)行的結(jié)果傳到當(dāng)前goroutine
   waitDone        chan struct{}}

上面我們對Cmd這個結(jié)構(gòu)體的一些字段做了解析,可以理解為Cmd就是對一個命令生命周期內(nèi)的抽象。下面我們來分析Cmd的一下方法,看看他是怎么使用的。

// Run方法開始執(zhí)行這個命令并等待它運行結(jié)束// 如果命令運行,在復(fù)制stdin、stdout和stder時沒有問題,并且以零退出狀態(tài)退出,則返回的錯誤為nil。// 如果命令啟動但沒有成功完成,錯誤類型為類型為*ExitError。在其他情況下可能會返回其他錯誤類型。// 如果調(diào)用的goroutine已經(jīng)用runtime.LockOSThread鎖定了操作系統(tǒng)線程,并修改了任何可繼承的OS級 線程狀態(tài)(例如,Linux或Plan 9名稱空間),新的 進程將繼承調(diào)用者的線程狀態(tài)。func (c *Cmd) Run() error {
   if err := c.Start(); err != nil {
      return err   }
   return c.Wait()}// Start方法啟動指定的命令,但不等待它完成。//// 如果Start成功返回,c.Process字段將被設(shè)置。//// 一旦命令運行完成,Wait方法將返回退出代碼并釋放相關(guān)資源。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)
  //在這里會調(diào)用stdin,stdout和stderr方法,如果Cmd的StdIn,StdOut,StdErr不是nil,就會將相關(guān)的copy任務(wù)封裝成func放在goroutine字段中,等待在Start方法執(zhí)行的時候調(diào)用。
    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沒有賦值,那么就用當(dāng)前進程的環(huán)境變量
    envv, err := c.envv()
    if err != nil {
        return err    }

  // 會用這個命令啟動一個新的進程
  // 在Linux的系統(tǒng)上,底層是調(diào)用了Frok來創(chuàng)建另一個進程,由于文章篇幅有限,就不對此處進行詳細分析了,詳情可看延伸閱讀
    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)

    // 除非有g(shù)oroutine要啟動,否則不會申請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掉執(zhí)行命令的進程
                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讀出的數(shù)據(jù)返回寫到w的字節(jié)。
    pr, pw, err := os.Pipe()
    if err != nil {
        return
    }

    c.closeAfterStart = append(c.closeAfterStart, pr)
    c.closeAfterWait = append(c.closeAfterWait, pw)
  //將相關(guān)的任務(wù)添加到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)
  //將相關(guān)的任務(wù)添加到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}// 等待命令退出,并等待任何復(fù)制到stdin或從stdout或stderr復(fù)制的完成。// 在調(diào)用Wait之前,Start方法必須被調(diào)用// 如果命令運行,在復(fù)制stdin、stdout和stder時沒有問題,并且以零退出狀態(tài)退出,則返回的錯誤為nil。// 如果命令運行失敗或沒有成功完成,錯誤類型為*ExitError。對于I/O問題可能會返回其他錯誤類型。// 如果c.Stdin、c.Stdout或c.Stderr中的任何一個不是*os.File,Wait也會等待各自的I/O循環(huán)復(fù)制到進程中或從進程中復(fù)制出來//// Wait釋放與Cmd相關(guān)的任何資源。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字段上面的函數(shù)運行有沒有錯誤
    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實際上是封裝了命令的執(zhí)行流程并且制定了命令的輸出流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}

到此,關(guān)于“Go執(zhí)行腳本命令的使用實例分析”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

go
AI