Code前端首页关于Code前端联系我们

Go语言开发,调用外部命令的不同姿势

terry 2年前 (2023-09-24) 阅读数 66 #后端开发
Go语言开发,调用外部命令的几种姿势

介绍

在工作中我时不时就要在Go中调用外部命令。前段时间做了一个工具,在钉钉群里加了一个机器人。 @这个机器人可以执行多个编写的脚本来完成指定的任务。创造一个机器人并不难。按照钉钉开发者文档添加机器人,然后@这个机器人会向您指定的服务器发送POST请求,并且该请求会附有短信。所以我需要做的是建立一个网络服务器。我可以使用 go 的原生 net/http 包或 gin/fasthttp/fibre 等 Web 框架。收到请求后,检查附带文本中的关键字,调用相应的程序,然后返回结果。

go标准库中的os/exec包提供了对调用外部程序的支持。本文介绍了os/exec的使用。运行

命令

Linux中有一个cal命令可以显示指定年月的日历。如果不指定年月,则默认为当前时间对应的年月。如果您使用的是Windows,建议安装msys2。该软件包含最常见的 Linux 命令。 Go语言开发,调用外部命令的几种姿势Go语言开发,调用外部命令的几种姿势

那么,如何在Go代码中调用这个命令呢?其实很简单:

func main() {
  cmd := exec.Command("cal")
  err := cmd.Run()
  if err != nil {
    log.Fatalf("cmd.Run() failed: %v\n", err)
  }
}

首先我们调用exec.Command输入命令名称,创建一个命令对象exec.Cmd。然后调用命令对象的 Run() 方法来执行它。

当你真正运行的时候,你会发现什么也没有发生,哈哈。事实上,当你使用 OS /exec 执行命令时,默认情况下会拒绝标准输出和标准错误。

Displayoutput

exec.Cmd该对象有两个字段 Stdout 和 ,均为Stderr io.Writer 。我们可以将任意实现 io.Writer 接口的类型实例分配给这两个字段,然后实现标准输出和标准错误的重定向。 io.Writer 接口可以在 Go 标准库和第三方库中找到,例如 *os.File、❀ *os。康涅狄格州。因此我们可以将命令的输出重定向到文件、内存缓存,甚至将其发送到网络。

标准输出显示

设置 exec.Cmd 对象的 Stdout 和 的两个字段 对象到 os.Stdout ,然后将输出内容显示到标准输出:

func main() {
  cmd := exec.Command("cal")
  cmd.Stdout = os.Stdout
  cmd.Stderr = os.Stderr
  err := cmd.Run()
  if err != nil {
    log.Fatalf("cmd.Run() failed: %v\n", err)
  }
}

运行程序。我在git bash中运行,得到如下结果:Go语言开发,调用外部命令的几种姿势

表示中文。我检查了 LANG 环境变量的值。原来是zh_CN.UTF-8。如果要输出英文,可以将 LANG 环境变量设置为 en_US.UTF-8

$ echo $LANG
zh_CN.UTF-8
$ LANG=en_US.UTF-8 go run main.go

获取输出: Go语言开发,调用外部命令的几种姿势

输出到文件 或创建文件,打开然后将文件句柄赋值给 exec.Cmd 对象的两个字段即可实现输出到文件的功能。
func main() {
  f, err := os.OpenFile("out.txt", os.O_WRONLY|os.O_CREATE, os.ModePerm)
  if err != nil {
    log.Fatalf("os.OpenFile() failed: %v\n", err)
  }

  cmd := exec.Command("cal")
  cmd.Stdout = f
  cmd.Stderr = f
  err = cmd.Run()
  if err != nil {
    log.Fatalf("cmd.Run() failed: %v\n", err)
  }
}

os.OpenFile打开文件,指定os.O_CREATE标志让操作系统自动创建文件(如果文件不存在),并返回文件对象*os .File

*os.File 实现 io.Writer 接口。

运行程序:

$ go run main.go
$ cat out.txt
    November 2022   
Su Mo Tu We Th Fr Sa
       1  2  3  4  5
 6  7  8  9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30

发送到网络

现在我们编写一个日历服务,接收年份和月份信息并返回该月的日历。

func cal(w http.ResponseWriter, r *http.Request) {
  year := r.URL.Query().Get("year")
  month := r.URL.Query().Get("month")

  cmd := exec.Command("cal", month, year)
  cmd.Stdout = w
  cmd.Stderr = w

  err := cmd.Run()
  if err != nil {
    log.Fatalf("cmd.Run() failed: %v\n", err)
  }
}

func main() {
  http.HandleFunc("/cal", cal)
  http.ListenAndServe(":8080", nil)
}

为了简单起见,这里省略了错误处理。一般情况下,年份和月份参数都需要进行有效性验证。函数exec.Command接收字符串类型的变量参数作为命令的参数:

func Command(name string, arg ...string) *Cmd

运行程序,使用浏览器访问localhost:8080=21year=th&cal? 2get:Go语言开发,调用外部命令的几种姿势

存储在io.writer接口的内存对象

* bytes.buffer选项中。 Buffer

对象,并将其分配给exec.CmdStdoutexec这两个字段。 * 字节。 Buffer 对象中存储的是命令的输出。
func main() {
  buf := bytes.NewBuffer(nil)
  cmd := exec.Command("cal")
  cmd.Stdout = buf
  cmd.Stderr = buf
  err := cmd.Run()
  if err != nil {
    log.Fatalf("cmd.Run() failed: %v\n", err)
  }

  fmt.Println(buf.String())
}

运行:

$ go run main.go
    November 2022   
Su Mo Tu We Th Fr Sa
       1  2  3  4  5
 6  7  8  9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30

发出命令,然后获取输出的字符串或字节切片。这种模式非常常见且使用方便,因此包 os/exec 提供了一种便捷的方法:CombinedOutput

输出到多个目的地

有时我们希望能够输出到文件和网络,同时保存到内存对象。使用 go 中的 io.MultiWriter 可以轻松实现此要求。 io.MultiWriter 只是将多个 io.Writer 转换为一个 io.Writer❀。 ? ,然后将 cmd 对象中的 StdoutStderr 分配给此 erio.。这样,命令执行时的输出将被发送到 http.ResponseWriter*os.File》 运行命令并获取输出

如前所述,我们经常需要运行命令并返回输出。 exec.Cmd 对象提供了一个方便的方法:CombinedOutput()。此方法运行命令并将输出内容作为字节切片返回以供以后处理。因此,上面的输出程序可以简化为:

So easy!

CombinedOutput()该方法的实现非常简单。首先将标准输出和标准错误重定向到 *bytes.Buffer 对象,然后运行程序,最后返回对象中的字节切片:

func (c *Cmd) CombinedOutput() ([]byte, error) {
  if c.Stdout != nil {
    return nil, errors.New("exec: Stdout already set")
  }
  if c.Stderr != nil {
    return nil, errors.New("exec: Stderr already set")
  }
  var b bytes.Buffer
  c.Stdout = &b
  c.Stderr = &b
  err := c.Run()
  return b.Bytes(), err
}

CombinedOutput 该方法的前几行显示StdoutStderr 状态必须是。这其实很容易理解。一般情况下,如果你已经计划使用CombinedOutput的方法来获取输出内容,那么你就可以轻松设置Stdout和字段。

CombinedOutput类似,还有Output方法。不同的是输出仅输出运行命令产生的标准输出内容。

分别给出标准输出和标准错误

创建两个*bytes.Buffer对象并将它们分配给分别为对象❙Stdout。 和Stderr这两个字段,然后运行命令分别获取标准输出和标准错误。

func main() {
  cmd := exec.Command("cal", "15", "2012")
  var stdout, stderr bytes.Buffer
  cmd.Stdout = &stdout
  cmd.Stderr = &stderr
  err := cmd.Run()
  if err != nil {
    log.Fatalf("cmd.Run() failed: %v\n", err)
  }

  fmt.Printf("output:\n%s\nerror:\n%s\n", stdout.String(), stderr.String())
}

Stdin

exec.Cmd 对象具有类型为 .Reader 的字段 Stdin。当命令运行时,将从该 io.Reader 读取输入。我们先看一个最简单的例子:

func main() {
  cmd := exec.Command("cat")
  cmd.Stdin = bytes.NewBufferString("hello\nworld")
  cmd.Stdout = os.Stdout
  err := cmd.Run()
  if err != nil {
    log.Fatalf("cmd.Run() failed: %v\n", err)
  }
}

如果不带参数运行命令cat,就会进入交互模式。 cat 逐行读取输入并将其按原样发送到输出。

让我们看一个更复杂的例子。 Go标准库中的包compress/bzip2仅提供解压方法,不提供压缩方法。我们可以使用Linux命令bzip2来实现压缩。 bzip2 从标准输入读取数据,对其进行压缩,然后将其发送到标准输出。参数

func bzipCompress(d []byte) ([]byte, error) {
  var out bytes.Buffer
  cmd := exec.Command("bzip2", "-c", "-9")
  cmd.Stdin = bytes.NewBuffer(d)
  cmd.Stdout = &out
  err := cmd.Run()
  if err != nil {
    log.Fatalf("cmd.Run() failed: %v\n", err)
  }

  return out.Bytes(), nil
}

-c表示压缩,-9表示压缩级别,9为最高。为了检查函数的正确性,编写一个简单的程序,首先压缩“Hello World”字符串,然后解压,看是否得到原始字符串:

func main() {
  data := []byte("hello world")
  compressed, _ := bzipCompress(data)
  r := bzip2.NewReader(bytes.NewBuffer(compressed))
  decompressed, _ := ioutil.ReadAll(r)
  fmt.Println(string(decompressed))
}

运行程序,输出“Hello world”。

环境变量

环境变量可以在一定程度上微调程序的行为。当然,这需要程序的支持。例如,设置 ENV=生产 会抑制调试日志输出。每个环境变量都是一个键值对。对象 exec.Cmd 具有类型为 []string 的字段 Env。我们可以更改它以在运行命令时检查环境变量。

package main

import (
  "fmt"
  "log"
  "os"
  "os/exec"
)

func main() {
  cmd := exec.Command("bash", "-c", "./test.sh")

  nameEnv := "NAME=darjun"
  ageEnv := "AGE=18"

  newEnv := append(os.Environ(), nameEnv, ageEnv)
  cmd.Env = newEnv

  out, err := cmd.CombinedOutput()
  if err != nil {
    log.Fatalf("cmd.Run() failed: %v\n", err)
  }

  fmt.Println(string(out))
}

上面的代码获取了系统环境变量,然后又添加了两个环境变量NAMEAGE。最后,使用bash执行脚本命令,我们希望检查必要的要执行的命令是否存在。如果存在,请立即运行。否则,将提示用户安装此命令。包os/exec提供了函数LookPath来获取命令所在目录。如果该命令不存在,则返回错误。

func main() {
  path, err := exec.LookPath("ls")
  if err != nil {
    fmt.Printf("no cmd ls: %v\n", err)
  } else {
    fmt.Printf("find ls in path:%s\n", path)
  }

  path, err = exec.LookPath("not-exist")
  if err != nil {
    fmt.Printf("no cmd not-exist: %v\n", err)
  } else {
    fmt.Printf("find not-exist in path:%s\n", path)
  }
}

运行:

$ go run main.go 
find ls in path:C:\Program Files\Git\usr\bin\ls.exe
no cmd not-exist: exec: "not-exist": executable file not found in %PATH%

封装了

,执行外部命令的流程相对固定:

  • 调用exec.Command()❀exec.Command() 致电 C md.Run( )执行命令

如果想得到输出,需要手动调用之类的方法或者自己创建bytes.Buffer对象并赋值到 exec .CmdStdout

字段。为了方便使用,我写了一个包goexec

接口如下:

// 执行命令,丢弃标准输出和标准错误
func RunCommand(cmd string, arg []string, opts ...Option) error
// 执行命令,以[]byte类型返回输出
func CombinedOutput(cmd string, arg []string, opts ...Option) ([]byte, error)
// 执行命令,以string类型返回输出
func CombinedOutputString(cmd string, arg []string, opts ...Option) (string, error)
// 执行命令,以[]byte类型返回标准输出
func Output(cmd string, arg []string, opts ...Option) ([]byte, error)
// 执行命令,以string类型返回标准输出
func OutputString(cmd string, arg []string, opts ...Option) (string, error)
// 执行命令,以[]byte类型分别返回标准输出和标准错误
func SeparateOutput(cmd string, arg []string, opts ...Option) ([]byte, []byte, error)
// 执行命令,以string类型分别返回标准输出和标准错误
func SeparateOutputString(cmd string, arg []string, opts ...Option) (string, string, error)

与直接使用 os/exec 包相比,我更喜欢在函数调用中获取结果。对于设置输入和环境变量等功能,我通过选项模式提供支持。

type Option func(*exec.Cmd)

func WithStdin(stdin io.Reader) Option {
  return func(c *exec.Cmd) {
    c.Stdin = stdin
  }
}

func Without(stdout io.Writer) Option {
  return func(c *exec.Cmd) {
    c.Stdout = stdout
  }
}

func WithStderr(stderr io.Writer) Option {
  return func(c *exec.Cmd) {
    c.Stderr = stderr
  }
}

func WithOutWriter(out io.Writer) Option {
  return func(c *exec.Cmd) {
    c.Stdout = out
    c.Stderr = out
  }
}

func WithEnv(key, value string) Option {
  return func(c *exec.Cmd) {
    c.Env = append(os.Environ(), fmt.Sprintf("%s=%s", key, value))
  }
}

func applyOptions(cmd *exec.Cmd, opts []Option) {
  for _, opt := range opts {
    opt(cmd)
  }
}

非常容易使用:

func main() {
  fmt.Println(goexec.CombinedOutputString("cal", nil, goexec.WithEnv("LANG", "en_US.UTF-8")))
}

我不满意的一件事是使用选项模式,可以使用可变参数来传递命令参数。现在您只能使用磁盘。尽管您不需要指定任何参数,但您确实需要传递它们。输入 null。我还没有想到更优雅的解决方案。

总结

本文介绍了使用os/exec标准库调用外部命令的各种位置。同时为了方便使用,我写了一个goexec包,封装了对os/exec的调用。这个包目前我使用没有任何问题。如果您有其他需求,可以创建主题或自行编辑。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门