go语言执行外部程序-标准库os/exec使用举例

       go标准库中的os/exec包对调用外部程序提供了支持,os.exec是对os.StartProcess的包装,方便重新映射标准输入输出,连接io到管道等。
exec包不调用系统shell,并且不支持shell通配符,或其他的扩展,管道,重定向等。
如果需要这些功能,直接调用shell就可以,注意避免危险的输入,或者使用path/filepath包中的glob函数。如果需要扩展环境变量,使用os包的ExpandEnv。

目录

1.常用api演示

2.实战应用

2.1 运行命令

2.2 显示输出

  (1) 显示到标准输出

(2)输出到文件

(3)发送到网络

(4)保存到内存对象中

(5)输出到多个目的地

2.3 运行命令并获取输出

2.4 分别获取标准输出和标准错误

2.5 标准输入

2.6 环境变量

2.7 检查命令是否存在

2.8 封装


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")))
}

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值