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

介绍
在工作中我时不时就要在Go中调用外部命令。前段时间做了一个工具,在钉钉群里加了一个机器人。 @这个机器人可以执行多个编写的脚本来完成指定的任务。创造一个机器人并不难。按照钉钉开发者文档添加机器人,然后@这个机器人会向您指定的服务器发送POST请求,并且该请求会附有短信。所以我需要做的是建立一个网络服务器。我可以使用 go 的原生 net/http 包或 gin/fasthttp/fibre 等 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 执行命令时,默认情况下会拒绝标准输出和标准错误。
Displayoutput
exec.Cmd
该对象有两个字段 Stdout
和 ,均为Stderr io.Writer 。我们可以将任意实现 io.Writer
接口的类型实例分配给这两个字段,然后实现标准输出和标准错误的重定向。 io.Writer
接口可以在 Go 标准库和第三方库中找到,例如 *os.File
、❀ *os。康涅狄格州
。因此我们可以将命令的输出重定向到文件、内存缓存,甚至将其发送到网络。
标准输出显示
设置 运行程序。我在git bash中运行,得到如下结果: 表示中文。我检查了 LANG 环境变量的值。原来是 获取输出: 运行程序: 现在我们编写一个日历服务,接收年份和月份信息并返回该月的日历。 为了简单起见,这里省略了错误处理。一般情况下,年份和月份参数都需要进行有效性验证。函数 运行程序,使用浏览器访问 运行: 发出命令,然后获取输出的字符串或字节切片。这种模式非常常见且使用方便,因此包 有时我们希望能够输出到文件和网络,同时保存到内存对象。使用 go 中的 如前所述,我们经常需要运行命令并返回输出。 与 创建两个 如果不带参数运行命令 让我们看一个更复杂的例子。 Go标准库中的包 运行程序,输出“Hello world”。 环境变量可以在一定程度上微调程序的行为。当然,这需要程序的支持。例如,设置 上面的代码获取了系统环境变量,然后又添加了两个环境变量 运行: ,执行外部命令的流程相对固定: 如果想得到输出,需要手动调用之类的方法或者自己创建 字段。为了方便使用,我写了一个包 接口如下: 与直接使用 非常容易使用: 我不满意的一件事是使用选项模式,可以使用可变参数来传递命令参数。现在您只能使用磁盘。尽管您不需要指定任何参数,但您确实需要传递它们。输入 本文介绍了使用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)
}
}
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
对象的两个字段即可实现输出到文件的功能。 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? 2
get:存储在
io.writer
接口的内存对象 * bytes.buffer
选项中。 Bufferexec.Cmd
Stdout
和exec这两个字段。
* 字节。 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
。 输出到多个目的地
io.MultiWriter
可以轻松实现此要求。 io.MultiWriter
只是将多个 io.Writer
转换为一个 io.Writer❀。 ?
,然后将 cmd 对象中的 Stdout
和 Stderr
分配给此 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
该方法的前几行显示Stdout
和 Stderr
状态必须是。这其实很容易理解。一般情况下,如果你已经计划使用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
逐行读取输入并将其按原样发送到输出。 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))
}
环境变量
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))
}
NAME
和AGE
。最后,使用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 .Cmd
的 Stdout
和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前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。