Go语言中调用外部命令的方法总结
作者:darjun 发布时间:2024-05-13 10:44:09
引子
在工作中,我时不时地会需要在Go中调用外部命令。前段时间我做了一个工具,在钉钉群中添加了一个机器人,@这个机器人可以让它执行一些写好的脚本程序完成指定的任务。机器人倒是不难,照着钉钉开发者文档添加好机器人,然后@这个机器人就会向一个你指定的服务器发送一个POST请求,请求中会附带文本消息。所以我要做的就是搭一个Web服务器,可以用go原生的net/http包,也可以用gin/fasthttp/fiber这些Web框架。收到请求之后,检查附带文本中的关键字去调用对应的程序,然后返回结果。
go标准库中的os/exec包对调用外部程序提供了支持,本文详细介绍os/exec的使用姿势。
运行命令
Linux中有个cal
命令,它可以显示指定年、月的日历,如果不指定年、月,默认为当前时间对应的年月。如果使用的是Windows,推荐安装msys2,这个软件包含了绝大多数的Linux常用命令。
那么,在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执行命令,标准输出和标准错误默认会被丢弃。
显示输出
exec.Cmd
对象有两个字段Stdout
和Stderr
,类型皆为io.Writer
。我们可以将任意实现了io.Writer
接口的类型实例赋给这两个字段,继而实现标准输出和标准错误的重定向。io.Writer
接口在 Go 标准库和第三方库中随处可见,例如*os.File
、*bytes.Buffer
、net.Conn
。所以我们可以将命令的输出重定向到文件、内存缓存甚至发送到网络中。
显示到标准输出
将exec.Cmd
对象的Stdout
和Stderr
这两个字段都设置为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运行,得到如下结果:
输出了中文,检查一下环境变量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
得到输出:
输出到文件
打开或创建文件,然后将文件句柄赋给exec.Cmd
对象的Stdout
和Stderr
这两个字段即可实现输出到文件的功能。
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)
}
这里为了简单,错误处理都省略了。正常情况下,year和month参数都需要做合法性校验。exec.Command
函数接收一个字符串类型的可变参数作为命令的参数:
func Command(name string, arg ...string) *Cmd
运行程序,使用浏览器请求localhost:8080/cal?year=2021&month=2
得到:
保存到内存对象中
*bytes.Buffer
同样也实现了io.Writer
接口,故如果我们创建一个*bytes.Buffer
对象,并将其赋给exec.Cmd
的Stdout
和Stderr
这两个字段,那么命令执行之后,该*bytes.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
。
我们稍微修改上面的web程序:
func cal(w http.ResponseWriter, r *http.Request) {
year := r.URL.Query().Get("year")
month := r.URL.Query().Get("month")
f, _ := os.OpenFile("out.txt", os.O_CREATE|os.O_WRONLY, os.ModePerm)
buf := bytes.NewBuffer(nil)
mw := io.MultiWriter(w, f, buf)
cmd := exec.Command("cal", month, year)
cmd.Stdout = mw
cmd.Stderr = mw
err := cmd.Run()
if err != nil {
log.Fatalf("cmd.Run() failed: %v\n", err)
}
fmt.Println(buf.String())
}
调用io.MultiWriter
将多个io.Writer
整合成一个io.Writer
,然后将cmd对象的Stdout
和Stderr
都赋值为这个io.Writer
。这样,命令运行时产出的输出会分别送往http.ResponseWriter
、*os.File
以及*bytes.Buffer
。
运行命令,获取输出
前面提到,我们常常需要运行命令,返回输出。exec.Cmd
对象提供了一个便捷方法:CombinedOutput()
。该方法运行命令,将输出内容以一个字节切片返回便于后续处理。所以,上面获取输出的程序可以简化为:
func main() {
cmd := exec.Command("cal")
output, err := cmd.CombinedOutput()
if err != nil {
log.Fatalf("cmd.Run() failed: %v\n", err)
}
fmt.Println(string(output))
}
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
方法前几行判断表明,Stdout
和Stderr
必须是未设置状态。这其实很好理解,一般情况下,如果已经打算使用CombinedOutput
方法获取输出内容,不会再自找麻烦地再去设置Stdout
和Stderr
字段了。
与CombinedOutput
类似的还有Output
方法,区别是Output
只会返回运行命令产出的标准输出内容。
分别获取标准输出和标准错误
创建两个*bytes.Buffer
对象,分别赋给exec.Cmd
对象的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())
}
标准输入
exec.Cmd
对象有一个类型为io.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=production
会抑制调试日志的输出。每个环境变量都是一个键值对。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))
}
上面代码获取系统的环境变量,然后又添加了两个环境变量NAME
和AGE
。最后使用bash
运行脚本test.sh
:
#!/bin/bash
echo $NAME
echo $AGE
echo $GOPATH
程序运行结果:
$ go run main.go
darjun
18
D:\workspace\code\go
检查命令是否存在
一般在运行命令之前,我们通过希望能检查要运行的命令是否存在,如果存在则直接运行,否则提示用户安装此命令。os/exec
包提供了函数LookPath
可以获取命令所在目录,如果命令不存在,则返回一个error。
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()
创建命令对象;调用
Cmd.Run()
执行命令
如果要获取输出,需要调用CombinedOutput/Output
之类的方法,或者手动创建bytes.Buffer
对象并赋值给exec.Cmd
的Stdout
和Stderr
字段。为了使用方便,我编写了一个包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
包,我倾向于一次函数调用就能获得结果。对输入、设置环境变量这些功能,我通过Option模式来提供支持。
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")))
}
有一点我不太满意,为了使用Option模式,本来可以用可变参数来传递命令参数,现在只能用切片了,即使不需要指定参数,也必须要传入一个nil
。暂时还没有想到比较优雅的解决方法。
来源:https://juejin.cn/post/7161077523592249374
猜你喜欢
- 当您的库中删除了大量的数据后,您可能会发现数据文件尺寸并没有减小。这是因为删 除操作后在数据文件中留下碎片所致。Discuz! 在系统数设置
- pandas批量处理体测成绩import numpy as npimport pandas as pdfrom pandas import
- 这篇文章主要介绍了python3获取文件中url内容并下载代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价
- 多个if语句是每次单独判断比如:例子一a = 5if a < 6: #条件1 &
- Seconds_Behind_Master对于mysql主备实例,seconds_behind_master是衡量master与slave之
- 打开终端输入以下命令 --> 回车 -->输入密码 -->回车 -->结束:sudo rm -rf /usr/loc
- Residual BlockResNet中最重要的组件是残差块(residual block),也称为残差单元(residual unit)
- 本文分析了mysql登录报错提示:ERROR 1045 (28000)的解决方法。分享给大家供大家参考,具体如下:一、问题:公司linux系
- 使用说明:需要引入插件calendar.js/calendar.min.js须要引入calendar.css 样式表,可以自定义自己想要的皮
- 最近pytorch出了visdom,也没有怎么去研究它,主要是觉得tensorboardX已经够用,而且用起来也十分的简单pip insta
- Python 是一门优雅的语言,简洁的语法,强大的功能。当然丰富的第三方库,更能加速开发。那么问题来了,如何安装这些第三方库(包)呢?安装第
- 1. 真值测试所谓真值测试,是指当一种类型对象出现在if或者while条件语句中时,对象值表现为True或者False。弄清楚各种情况下的真
- 常在读写文件之前,需要判断文件或目录是否存在,不然某些处理方法可能会使程序出错。所以最好在做任何操作之前,先判断文件是否存在。这里将介绍三种
- 本文实例为大家分享了Vue日期时间选择器组件的具体代码,供大家参考,具体内容如下1.效果图如下单选日期选择器多选日期选择器日期时间选择器2.
- Gtalk 软件的最下方有个很好又很实用的功能,就是 Gmail 邮件提醒功能。会定时更新你 Gmail 中未读新邮件的数量。试想
- 目前为止,我们一直没有对函数声明和函数表达式加以区别。而实际上,解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁。解析器会
- 最近朋友需要一个可以识别图片中的文字的程序,以前做过java验证码识别的程序;刚好最近在做一个python项目,所以顺便用Python练练手
- 系统环境为server20121、下载mysql解压版,解压安装包到指定目录2、在以上目录中,复制一份my-default.ini文件,重命
- 一、环境介绍操作系统: win10 64位python版本: 3.8IDE: 采用vscode用到的相关安装包CSDN打包下载地址: htt
- 这将为我们的团队节省每天重复的数据处理时间......简介如果你目前在一个数据或商业智能团队工作,你的任务之一可能是制作一些每日、每周或每月