go标准库中的os/exec包对调用外部程序提供了支持,os.exec是对os.StartProcess的包装,方便重新映射标准输入输出,连接io到管道等。
exec包不调用系统shell,并且不支持shell通配符,或其他的扩展,管道,重定向等。
如果需要这些功能,直接调用shell就可以,注意避免危险的输入,或者使用path/filepath包中的glob函数。如果需要扩展环境变量,使用os包的ExpandEnv。
目录
1.常用api演示
(1)LookPath
func LookPath(file string) (string, error)
说明:在环境变量搜索可执行文件全路径。结果可能是绝对路径或相对于当前目录的相对路径。
package main
import (
"fmt"
"log"
"os/exec"
)
func main() {
path, err := exec.LookPath("ls")
if err != nil {
log.Fatal("ls not found")
}
fmt.Printf("ls at %s\n", path)
}
(2)Cmd定义
type Cmd struct {
// 要运行的命令路径,如/bin/ls
Path string
// 命令参数
Args []string
// 环境变量,键值对形式
// 如果键重复,则最后一个生效
// 如果没有设置Env,则使用当前进程的环境变量
Env []string
// 指定工作目录,如果为空则为当前目录
Dir string
// 指定标准输入
// 如果为空则从os.DevNull读取
// 如果是*os.File,则读取该文件
// 默认情况,会有一个单独的goroutine从标准输入读取数据并通过管道传递给cmd。
// Wait不会停止,知道goroutine停止复制,到达标准输入结束(EOF或读取错误)
Stdin io.Reader
// 指定标准输出和标准输入
// 如果为空,Run的时候连接到os.DevNull
// 如果是*os.File,则连接到该文件
Stdout io.Writer
Stderr io.Writer
// ExtraFiles specifies additional open files to be inherited by the
// new process. It does not include standard input, standard output, or
// standard error. If non-nil, entry i becomes file descriptor 3+i.
// Windows不支持
ExtraFiles []*os.File
// 可选的特定于操作系统的属性SysProcAttr holds optional, operating system-specific attributes.
// Run把它作为os.ProcAttr的Sys字段传递给os.StartProcess
SysProcAttr *syscall.SysProcAttr
// 进程启动后的*os.Process对象
Process *os.Process
// 包含已退出的进程信息,可在调用Wait或者Run后获得
ProcessState *os.ProcessState
}(3)Command
func Command(name string, arg ...string) *Cmd
说明:执行,参数仅设置cmd的path和args。如果name不包含路径分隔符,则调用LookPath查找完整路径,rg不应包含命令本身,设置命令执行时的环境变量
package main
import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
)
func main() {
cmd := exec.Command("ls", "-ih")
var out bytes.Buffer
cmd.Stdout = &out
cmd.Env = append(os.Environ(),
"FOO=duplicate_value", // 重复被忽略
"FOO=actual_value", // 实际被使用
)
err := cmd.Run()
if err != nil {
log.Fatal(err)
}
fmt.Println(out.String())
fmt.Printf("out: %q\n", out.String())
}
(4) CommandContext
func CommandContext(ctx context.Context, name string, arg ...string) *Cmd
说明:包含上下文的*Cmd,如果上下文在命令完成之前完成,则提供的上下文通过os.Process.Kill终止进程,常用于为命令设置超时。
package main
import (
"context"
"fmt"
"os/exec"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
cmd := exec.CommandContext(ctx, "sleep", "5")
if err := cmd.Run(); err != nil {
fmt.Println(cmd.ProcessState)
}
}
(5)CombinedOutput
func (c *Cmd) CombinedOutput() ([]byte, error)
说明:运行命令并返回组合到一起的标准输出和标准错误
package main
import (
"fmt"
"log"
"os/exec"
)
func main() {
cmd := exec.Command("sh", "-c", "echo 'hello ok'; echo 1>&2 'hello error'")
out_err, err := cmd.CombinedOutput()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", out_err)
}
(6)Output
说明:运行命令并返回标准输出
package main
import (
"fmt"
"log"
"os/exec"
)
func main() {
//out, err := exec.Command("sh", "-c", "sleep 5; echo 123456").Output()
out, err := exec.Command("date").Output()
//out, err := exec.Command("date", "-h").Output()
if err != nil {
log.Fatal(err)
}
fmt.Printf("out is %s\n", out)
}
(7)Run
func (c *Cmd) Run() error
说明:运行命令,并等待,返回是否成功
package main
import (
"log"
"os/exec"
)
func main() {
cmd := exec.Command("sleep", "5")
log.Printf("run command to finish...")
err := cmd.Run()
log.Printf("command finished with error: %v", err)
}
(8)Start
func (c *Cmd) Start() error
说明:启动执行命令,但不等待,如果启动成功返回,会设置c.Process字段
一旦命令结束,Wait方法将返回退出代码并释放资源,也就是通过Wait来等待进程结束
package main
import (
"log"
"os/exec"
)
func main() {
cmd := exec.Command("sleep", "5")
err := cmd.Start()
if err != nil {
log.Fatal(err)
}
log.Printf("Waiting for command to finish...")
err = cmd.Wait()
log.Printf("Command finished with error: %v", err)
}
(9)StderrPipe
func (*Cmd) StderrPipe
获得标准输入输出错误的管道
(10)String
func (*Cmd) String
func (c *Cmd) String() string
返回人可读的C描述,仅用于输出,不适合作为shell输入
(11)Wait
func (c *Cmd) Wait() error
等待命令退出,等待所有标准输入输出错误复制完成,必须通过start启动
一个简单的交互shell
package main
import (
"bufio"
"fmt"
"os"
"os/exec"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("> ")
// Read the keyboad input.
input, err := reader.ReadString('\n')
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
// Handle the execution of the input.
if err = execInput(input); err != nil {
fmt.Fprintln(os.Stderr, err)
}
}
}
func execInput(input string) error {
// Remove the newline character.
input = strings.TrimSuffix(input, "\n")
// Prepare the command to execute.
cmd := exec.Command(input)
// Set the correct output device.
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
// Execute the command and return the error.
return cmd.Run()
}
命令组合,管道连接命令输入输出
package main
import (
"os"
"os/exec"
)
func main() {
c1 := exec.Command("grep", "perror", "/home/tiger/cpp/tree_server.cpp")
c2 := exec.Command("wc", "-l")
c2.Stdin, _ = c1.StdoutPipe()
c2.Stdout = os.Stdout
_ = c2.Start()
_ = c1.Run()
_ = c2.Wait()
}
2.实战应用
2.1 运行命令
package main
import (
"log"
"os/exec"
)
func main() {
cmd := exec.Command("cal")
err := cmd.Run()
if err != nil {
log.Fatalf("cmd.Run() failed: %v\n", err)
}
}
运行上面程序会发现没有任何输出,这是因为使用os/exec执行命令,标准输出和标准错误默认会被丢弃。
如何显示输出呢?
2.2 显示输出
exec.Cmd对象有两个字段Stdout和Stderr,类型皆为io.Writer。我们可以将任意实现了io.Writer接口的类型实例赋给这两个字段,继而实现标准输出和标准错误的重定向。
io.Writer接口在 Go 标准库和第三方库中随处可见,例如*os.File、*bytes.Buffer、net.Conn。所以我们可以将命令的输出重定向到文件、内存缓存甚至发送到网络中。
(1) 显示到标准输出
将exec.Cmd对象的Stdout和Stderr这两个字段都设置为os.Stdout,那么输出内容都将显示到标准输出。
package main
import (
"log"
"os"
"os/exec"
)
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)
}
}
(2)输出到文件
打开或创建文件,然后将文件句柄赋给exec.Cmd对象的Stdout和Stderr这两个字段即可实现输出到文件的功能。下面演示程序os.OpenFile打开一个文件,指定os.O_CREATE标志让操作系统在文件不存在时自动创建一个,返回该文件对象*os.File。*os.File实现了io.Writer接口。
package main
import (
"log"
"os"
"os/exec"
)
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)
}
}
(3)发送到网络
package main
import (
"fmt"
"log"
"net/http"
"os/exec"
)
// http://127.0.0.1:8585/cal?year=2023&month=6
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)
}
fmt.Fprintf(w, "显示日历")
}
func main() {
http.HandleFunc("/cal", cal)
http.ListenAndServe(":8585", nil)
}
(4)保存到内存对象中
*bytes.Buffer同样也实现了io.Writer接口,故如果我们创建一个*bytes.Buffer对象,并将其赋给exec.Cmd的Stdout和Stderr这两个字段,那么命令执行之后,
该*bytes.Buffer对象中保存的就是命令的输出。os/exec包还提供了一个更便捷方法:CombinedOutput。
package main
import (
"bytes"
"fmt"
"log"
"os/exec"
)
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())
}
(5)输出到多个目的地
有时我们希望能输出到文件和网络,同时保存到内存对象。使用go提供的io.MultiWriter可以很容易实现这个需求。
io.MultiWriter很方便地将多个io.Writer转为一个io.Writer。
如下代码调用io.MultiWriter将多个io.Writer整合成一个io.Writer,然后将cmd对象的Stdout和Stderr都赋值为这个io.Writer。
这样命令运行时产出的输出会分别送往http.ResponseWriter、*os.File以及*bytes.Buffer。
package main
import (
"bytes"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
)
// http://127.0.0.1:8585/cal?year=2023&month=6
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())
}
func main() {
http.HandleFunc("/cal", cal)
http.ListenAndServe(":8585", nil)
}
2.3 运行命令并获取输出
我们常常需要运行命令返回输出。exec.Cmd对象提供了一个便捷方法:CombinedOutput()。该方法运行命令将输出内容以一个字节切片返回便于后续处理。
package main
import (
"fmt"
"log"
"os/exec"
)
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))
}
CombinedOutput()方法实现原理:
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只会返回运行命令产出的标准输出内容。
2.4 分别获取标准输出和标准错误
创建两个*bytes.Buffer对象,分别赋给exec.Cmd对象的Stdout和Stderr这两个字段,然后运行命令即可分别获取标准输出和标准错误。
package main
import (
"bytes"
"fmt"
"log"
"os/exec"
)
func main() {
//cmd := exec.Command("cal", "13", "2023")
cmd := exec.Command("cal", "6", "2023")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
fmt.Printf("error:\n%s\n", stderr.String())
log.Fatalf("cmd.Run() failed: %v\n", err)
}
fmt.Printf("output:\n%s\nerror:\n%s\n", stdout.String(), stderr.String())
}
2.5 标准输入
exec.Cmd对象有一个类型为io.Reader的字段Stdin。命令运行时会从这个io.Reader读取输入
如果不带参数运行cat命令,则进入交互模式,cat按行读取输入,并且原样发送到输出。
package main
import (
"bytes"
"log"
"os"
"os/exec"
)
func main() {
cmd := exec.Command("cat")
cmd.Stdin = bytes.NewBufferString("hello\nworld\n")
cmd.Stdout = os.Stdout
err := cmd.Run()
if err != nil {
log.Fatalf("cmd.Run() failed: %v\n", err)
}
}
Go标准库中compress/bzip2包只提供解压方法,并没有压缩方法。我们可以利用Linux命令bzip2实现压缩。bzip2从标准输入中读取数据,将其解压缩压缩,并发送到标准输出。
参数-c表示压缩,-9表示压缩等级,9为最高。为了验证函数的正确性,写个简单的程序,先压缩"hello world"字符串,然后解压,看看是否能得到原来的字符串。
package main
import (
"bytes"
"compress/bzip2"
"fmt"
"io/ioutil"
"log"
"os/exec"
)
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
}
func main() {
data := []byte("hello world")
compressed, _ := bzipCompress(data)
r := bzip2.NewReader(bytes.NewBuffer(compressed))
decompressed, _ := ioutil.ReadAll(r)
fmt.Println(string(decompressed))
}
2.6 环境变量
环境变量可以在一定程度上微调程序的行为,当然这需要程序的支持。例如,设置ENV=production会抑制调试日志的输出。每个环境变量都是一个键值对。
exec.Cmd对象中有一个类型为[]string的字段Env。我们可以通过修改它来达到控制命令运行时的环境变量的目的。
2.7 检查命令是否存在
一般在运行命令之前,我们通过希望能检查要运行的命令是否存在,如果存在则直接运行,否则提示用户安装此命令。os/exec包提供了函数LookPath可以获取命令所在目录,如果命令不存在,则返回一个error。
package main
import (
"fmt"
"os/exec"
)
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)
}
}
2.8 封装
执行外部命令的流程比较固定:
调用exec.Command()创建命令对象;
调用Cmd.Run()执行命令
如果要获取输出,需要调用CombinedOutput/Output之类的方法,或者手动创建bytes.Buffer对象并赋值给exec.Cmd的Stdout和Stderr字段。
为了使用方便,我编写了一个包goexec。
GOROOT的src下新建目录当成包,在其下编写go模块
例如:
package goexec
import (
"fmt"
"io"
"os"
"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)
}
}
主模块调用:
package main
import (
"fmt"
"goexec"
)
func main() {
fmt.Println(goexec.CombinedOutputString("cal", nil, goexec.WithEnv("LANG", "en_US.UTF-8")))
}