有时我们在写程序的时候会需要调用系统的某个命令来完成一些任务。Go语言os/exec标准库就提供这种调用外部命令的功能。
如下面的代码调用ls命令来查看指定目录下面的文件:
import (
"os"
"os/exec"
)
func ls(path string) error {
cmd := exec.Command("ls", path)
cmd.Stdout = os.Stdout
return cmd.Run()
}
func main() {
if err := ls("/"); err != nil {
panic(err)
}
}
再举一个例子,将小写转成大写:
import (
"bytes"
"fmt"
"log"
"os/exec"
"strings"
)
func main() {
cmd := exec.Command("tr", "a-z", "A-Z")
cmd.Stdin = strings.NewReader("some input")
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
fmt.Printf("in all caps: %q\n", out.String())
}
概述
golang下的os/exec包执行外部命令,它将os.StartProcess进行包装使得它更容易映射到stdin/stdout、管道I/O。
与C语言或者其他语言中的“系统”库调用不同,os/exec包并不调用系统shell,也不展开任何glob模式,也不处理通常由shell完成的其他扩展、管道或重定向。这个包的行为更像C语言的“exec”函数家族。要展开glob模式,可以直接调用shell,注意避免任何危险的输入,或者使用path/filepath包的glob函数。如果要展开环境变量,请使用package os的ExpandEnv。
所谓的 glob 模式是指 shell 所使用的简化了的正则表达式。星号(*)匹配零个或多个任意字符;[abc]匹配任何一个列在方括号中的字符(这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c);问号(?)只匹配一个任意字符;如果在方括号中使用短划线分隔两个字符,表示所有在这两个字符范围内的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的数字)。
注意,这个包中的示例假设是Unix系统。它们不能在Windows上运行,也不能在golang.org和godoc.org使用的Go Playground上运行。
相关函数定义如下:
Variables
func LookPath(file string) (string, error)
type Cmd
//方法返回一个*Cmd, 用于执行name指定的程序(携带arg参数)
func Command(name string, arg ...string) *Cmd
//
func CommandContext(ctx context.Context, name string, arg ...string) *Cmd
//执行Cmd中包含的命令,并返回标准输出与标准错误合并后的切片
func (c *Cmd) CombinedOutput() ([]byte
, error)
//执行Cmd中包含的命令,并返回标准输出的切片
func (c *Cmd) Output() ([]byte, error)
//执行Cmd中包含的命令,阻塞直到命令执行完成
func (c *Cmd) Run() error
//执行Cmd中包含的命令,该方法立即返回,并不等待命令执行完成
func (c *Cmd) Start() error
//返回一个管道,该管道会在Cmd中的命令被启动后连接到其标准输入
func (c *Cmd) StderrPipe() (io.ReadCloser, error)
//返回一个管道,该管道会在Cmd中的命令被启动后连接到其标准输出
func (c *Cmd) StdinPipe() (io.WriteCloser, error)
//返回一个管道,该管道会在Cmd中的命令被启动后连接到其标准错误
func (c *Cmd) StdoutPipe() (io.ReadCloser, error)
//
func (c *Cmd) String() string
//该方法会阻塞直到Cmd中的命令执行完成,但该命令必须是被Start方法开始执行的
func (c *Cmd) Wait() error
type Error
func (e *Error) Error() string
func (e *Error) Unwrap() error
type ExitError
func (e *ExitError) Error() string
各个函数详解
func LookPath
函数定义:
func LookPath(file string) (string, error)
在环境变量PATH指定的目录中搜索可执行文件,如过file中有文件分隔符(斜杠),则只在当前目录搜索。即:默认在系统的环境变量里查找给定的可执行命令文件,如果查找到返回路径,否则报错𝑤𝑠是PATH$。可提供相对路径下进行查找,并返回相对路径。
示例:
import (
"fmt"
"os/exec"
)
func main() {
f, err := exec.LookPath("ls")
if err != nil {
fmt.Println(err)
}
fmt.Println(f)
}
struct Cmd
Cmd代表一个正在准备或者在执行中的外部命令。
type Cmd struct
{
Path string
Args []string
Env []string
Dir string
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
ExtraFiles []*os.File
SysProcAttr *syscall.SysProcAttr
Process *os.Process
ProcessState *os.ProcessState
ctx context.Context
lookPathErr error
finished bool
childFiles []*os.File
closeAfterStart []io.Closer
closeAfterWait []io.Closer
goroutine []func() error
errch chan error
waitDone chan struct{}
}
注:exec在执行调用系统命令时,会先对需要执行的操作进行一次封装,然后在执行。封装后的命令对象具有以上struct属性。而封装方式即使用下边的Command函数。
func Command
函数定义:
func Command(name string, arg ...string) *Cmd
函数返回一个*Cmd,用于使用给出的参数执行name指定的程序。返回值只设定了Path和Args两个参数。
如果name不含路径分隔符(如果不是相对路径),将使用LookPath获取完整路径(就是用默认的全局变量路径);否则直接使用name。参数arg不应包含命令名。
示例:
func main() {
cmd := exec.Command("go", "version")
fmt.Println(cmd.Path, cmd.Args)
//输出: /usr/local/go/bin/go [go version]
}
注:在调用命令执行封装时,如果不提供相对路径,系统会使用LookPath获取完整路径;即这里可以给一个相对路径。
以上操作只会将命令进行封装,相当于告诉系统将进行哪些操作,但是执行时无法获取相关信息。
func Run
函数定义:
func (c *Cmd) Run() error
Run执行c包含的命令,并阻塞直到完成。
如果命令成功执行,stdin、stdout、stderr的转交没有问题,并且返回状态码为0,方法的返回值为nil(执行Run函数的返回状态,正确执行Run函数,并不代表正确执行了命令);如果函数没有执行或者执行失败,会返回*ExitError类型的错误;否则返回的error可能是表示I/O问题。
即:该命令只会执行且阻塞到执行结束,如果执行函数有错则返回报错信息,没错则返回nil,并不会返回执行结果。
func Start
函数定义:
func (c *Cmd) Start() error
Start开始执行c包含的命令,但并不会等待该命令完成即返回。
func Wait
函数定义:
func
(c *Cmd) Wait() error
Wait会阻塞直到该命令执行完成,该命令必须是被Start方法开始执行的。
如果命令成功执行,stdin、stdout、stderr的转交没有问题,并且返回状态码为0,方法的返回值为nil;如果命令没有执行或者执行失败,会返回*ExitError类型的错误;否则返回的error,可能是表示I/O问题。Wait方法会在命令返回后释放相关的资源。
Start和Run的区别
示例:
func main() {
cmd := exec.Command("sleep", "5")
err := cmd.Start()
if err != nil {
fmt.Println(err)
}
fmt.Printf("Waiting for command to finish...")
err = cmd.Wait()
fmt.Printf("Command finished with error: %v", err)
}
注:一个命令只能使用Start()或者Run()中的一个启动命令,不能两个同时使用。
func CombinedOutput
函数定义:
func (c *Cmd) CombinedOutput() ([]byte, error)
执行Cmd中包含的命令,并返回标准输出与标准错误合并后的切片。
示例:
func main() {
cmd := exec.Command("ls", "-a", "-l")
out, err := cmd.CombinedOutput()
if err != nil {
fmt.Println(err)
}
fmt.Println(string(out))
}
func Output
函数定义:
func (c *Cmd) Output() ([]byte, error)
执行Cmd中包含的命令,并返回标准输出的切片。
示例:
import (
"fmt"
"os/exec"
)
func main() {
cmd := exec.Command("ls", "-a", "-l")
out, err := cmd.Output()
if err != nil {
fmt.Println(err)
}
fmt.Println(string(out))
}
注:Output()和CombinedOutput()不能够同时使用,因为command的标准输出只能有一个,同时使用的话便会定义了两个,便会报错。
我们还可以通过指定一个对象连接到对应的管道进行传输参数(stdinpipe),获取输出(stdoutpipe),获取错误(stderrpipe)
func StdinPipe
函数定义:
func (c *Cmd) StdinPipe() (io.WriteCloser, error)
err 返回的是执行函数时的错误。StdinPipe方法返回一个在命令Start后与命令标准输入关联的管道。当命令退出时,Wait方法将关闭这个管道。必要时调用者可以调用Close方法来强行关闭管道,例如命令在输入关闭后才会执行返回时需要显式关闭管道。
示例:
func
main() {
cmd := exec.Command("cat")
stdin, err := cmd.StdinPipe()
if err != nil {
fmt.Println(err)
}
_, err = stdin.Write([]byte("tmp.txt"))
if err != nil {
fmt.Println(err)
}
stdin.Close()
cmd.Stdout = os.Stdout
cmd.Start()
}
func StdoutPipe
函数定义:
func (c *Cmd) StdoutPipe() (io.ReadCloser, error)
StdoutPipe方法返回一个在命令Start后与命令标准输出关联的管道。当命令退出时,Wait方法将关闭这个管道。但是在从管道读取完全部数据之前调用Wait是错误的;同样使用StdoutPipe方法时调用Run函数也是错误的。
示例:
func main() {
cmd := exec.Command("ls", "-a", "-l")
stdout, err := cmd.StdoutPipe()
if err != nil {
fmt.Println(err)
}
defer stdout.Close()
if err := cmd.Start(); err != nil {
fmt.Println(err)
}
content, err := ioutil.ReadAll(stdout)
if err != nil {
fmt.Println(err)
}
fmt.Println(string(content))
}
输出重定向到文件:
import (
"fmt"
"os"
"os/exec"
)
func main() {
cmd := exec.Command("ls", "-a", "-l")
stdout, err := os.OpenFile("stdout.log", os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
fmt.Println(err)
}
defer stdout.Close()
cmd.Stdout = stdout
if err := cmd.Start(); err != nil {
fmt.Println(err)
}
}
func StderrPipe
函数定义:
func (c *Cmd) StderrPipe() (io.ReadCloser, error)
StderrPipe方法返回一个在命令Start后与命令标准错误输出关联的管道。当命令退出时,Wait方法将关闭这个管道,一般不需要显式的关闭该管道。但是在从管道读取完全部数据之前调用Wait是错误的;同样使用StderrPipe方法时调用Run函数也是错误的。
示例:
func main() {
cmd := exec.Command("mv", "hello")
i, err := cmd.StderrPipe()
if err != nil {
fmt.Printf("Error:%s\n", err)
}
b, _ := ioutil.ReadAll(i)
if err := cmd.Wait(); err != nil {
fmt.Printf("Error: %s\n", err)
}
fmt.Println(string(b))
}
应用示例
项目中需要执行shell命令,虽然exec包提供了CombinedOutput()方法,在shell运行结束会返回shell执行的输出,但是用户在发起一次任务时,可能在不停的刷新log,想达到同步查看log的目的,但是CombinedOutput()方法只能在命令完全执行结束才返回整个shell的输出,所以肯定达不到效果,所以,需要寻找其它方法达到命令一边执行log一边输出的目的。
使用重定向
如果你的shell比较简单,并且log的文件路径也很容易确定,那么直接对shell执行的命令添加重定向最简单不过了,程序参考如下:
import (
"fmt"
"os/exec"
)
func main() {
cmdStr := `
#!/bin/bash
for var in {1..10}
do
sleep 1
echo "Hello, Welcome ${var} times "
done`
cmd := exec.Command("bash", "-c", cmdStr+" >> file.log")
if err := cmd.Start(); err != nil {
fmt.Println(err)
}
if err := cmd.Wait(); err != nil {
fmt.Println(err)
}
}
上面程序定义了一个每秒1次的shell,但是在shell执行前,对shell进行了拼接,使用了重定向,所以我们可以在另外一个终端中实时的看到 log 的变化
指定shell执行时的输出
使用exec.Command创建一个Shell后,就具有了两个变量:Stdout io.Writer和Stderr io.Writer。
这两个变量是用来指定程序的标准输出和标准错误输出的位置,所以我们就利用这两个变量,直接打开文件,然后将打开的文件指针赋值给这两个变量即可将程序的输出直接输出到文件中,也能达到相同的效果,参考程序如下:
import (
"fmt"
"os"
"os/exec"
)
func main() {
cmdStr := `
#!/bin/bash
for var in {1..10}
do
sleep 1
echo "Hello, Welcome ${var} times "
done`
cmd := exec.Command("bash", "-c", cmdStr)
file, _ := os.OpenFile("file.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
defer file.Close()
//指定输出位置
cmd.Stderr = file
cmd.Stdout = file
if err := cmd.Start(); err != nil {
fmt.Println(err)
}
if err := cmd.Wait(); err != nil {
fmt.Println(err)
}
}
从shell执行结果的管道中获取输出