首页后端开发其他后端知识Go执行脚本命令的简单用例有哪些,怎样做

Go执行脚本命令的简单用例有哪些,怎样做

时间2024-03-26 05:20:03发布访客分类其他后端知识浏览901
导读:在这篇文章中,我们将学习“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}
    

在上面的方法分析之中我们可以看出运行一个命令的流程是Run-> Start-> Wait,等待命令运行完成。并且在Start的时候会起来一个新的进程来执行命令。基于上面我们对Cmd的一顿分析,笔者感觉在文章开头写的测试代码实在是乏善可陈,因为Cmd封装了挺多东西的,我们在工作中完全可以充分利用他封装的功能,比如设置超时时间,设置标准输入流或者标准输出流,还可以定制化设置这个命令执行的环境变量等等。。。。·


到此这篇关于“Go执行脚本命令的简单用例有哪些,怎样做”的文章就介绍到这了,感谢各位的阅读,更多相关Go执行脚本命令的简单用例有哪些,怎样做内容,欢迎关注网络资讯频道,小编将为大家输出更多高质量的实用文章!

声明:本文内容由网友自发贡献,本站不承担相应法律责任。对本内容有异议或投诉,请联系2913721942#qq.com核实处理,我们将尽快回复您,谢谢合作!

gogolang

若转载请注明出处: Go执行脚本命令的简单用例有哪些,怎样做
本文地址: https://pptw.com/jishu/653285.html
JavaScript和HTML5的关系和区别是什么 JavaScript中布尔值为false的情况有哪些

游客 回复需填写必要信息