首页 后端开发 Golang Go执行脚本命令的使用实例以及源码解析

Go执行脚本命令的使用实例以及源码解析

Dec 13, 2021 pm 03:16 PM
go golang

本文由golang教程栏目给大家介绍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执行脚本命令的使用实例以及源码解析的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

如何使用 Golang 安全地读取和写入文件? 如何使用 Golang 安全地读取和写入文件? Jun 06, 2024 pm 05:14 PM

在Go中安全地读取和写入文件至关重要。指南包括:检查文件权限使用defer关闭文件验证文件路径使用上下文超时遵循这些准则可确保数据的安全性和应用程序的健壮性。

如何为 Golang 数据库连接配置连接池? 如何为 Golang 数据库连接配置连接池? Jun 06, 2024 am 11:21 AM

如何为Go数据库连接配置连接池?使用database/sql包中的DB类型创建数据库连接;设置MaxOpenConns以控制最大并发连接数;设置MaxIdleConns以设定最大空闲连接数;设置ConnMaxLifetime以控制连接的最大生命周期。

如何在 Golang 单元测试中使用 gomega 进行断言? 如何在 Golang 单元测试中使用 gomega 进行断言? Jun 05, 2024 pm 10:48 PM

如何在Golang单元测试中使用Gomega进行断言在Golang单元测试中,Gomega是一个流行且功能强大的断言库,它提供了丰富的断言方法,使开发人员可以轻松验证测试结果。安装Gomegagoget-ugithub.com/onsi/gomega使用Gomega进行断言以下是使用Gomega进行断言的一些常用示例:1.相等断言import"github.com/onsi/gomega"funcTest_MyFunction(t*testing.T){

Golang框架与Go框架:内部架构与外部特性对比 Golang框架与Go框架:内部架构与外部特性对比 Jun 06, 2024 pm 12:37 PM

GoLang框架与Go框架的区别体现在内部架构和外部特性上。GoLang框架基于Go标准库,扩展其功能,而Go框架由独立库组成,实现特定目的。GoLang框架更灵活,Go框架更容易上手。GoLang框架在性能上稍有优势,Go框架的可扩展性更高。案例:gin-gonic(Go框架)用于构建RESTAPI,而Echo(GoLang框架)用于构建Web应用程序。

如何在 Golang 中将 JSON 数据保存到数据库中? 如何在 Golang 中将 JSON 数据保存到数据库中? Jun 06, 2024 am 11:24 AM

可以通过使用gjson库或json.Unmarshal函数将JSON数据保存到MySQL数据库中。gjson库提供了方便的方法来解析JSON字段,而json.Unmarshal函数需要一个目标类型指针来解组JSON数据。这两种方法都需要准备SQL语句和执行插入操作来将数据持久化到数据库中。

如何找出 Golang 正则表达式匹配的第一个子字符串? 如何找出 Golang 正则表达式匹配的第一个子字符串? Jun 06, 2024 am 10:51 AM

FindStringSubmatch函数可找出正则表达式匹配的第一个子字符串:该函数返回包含匹配子字符串的切片,第一个元素为整个匹配字符串,后续元素为各个子字符串。代码示例:regexp.FindStringSubmatch(text,pattern)返回匹配子字符串的切片。实战案例:可用于匹配电子邮件地址中的域名,例如:email:="user@example.com",pattern:=@([^\s]+)$获取域名match[1]。

从前端转型后端开发,学习Java还是Golang更有前景? 从前端转型后端开发,学习Java还是Golang更有前景? Apr 02, 2025 am 09:12 AM

后端学习路径:从前端转型到后端的探索之旅作为一名从前端开发转型的后端初学者,你已经有了nodejs的基础,...

如何用 Golang 使用预定义时区? 如何用 Golang 使用预定义时区? Jun 06, 2024 pm 01:02 PM

Go语言中使用预定义时区包括以下步骤:导入"time"包。通过LoadLocation函数加载特定时区。在创建Time对象、解析时间字符串等操作中使用已加载的时区,进行日期和时间转换。使用不同时区的日期进行比较,以说明预定义时区功能的应用。

See all articles