简介
在开发中我们可能会遇到需要在程序中调用脚本的需求,或者涉及到两个语言之间的交互,笔者之前就遇到了需要在go中调用python的需求,然后在代码中应用了go-python3这个库,实际上在go中调用python的脚本也是一个解决之法。这片文章将介绍在go中运行shell脚本的方法以及对其源码的相应解析。
程序用例
test_command.go
package learn | |
import ( | |
"fmt" | |
"os/exec" | |
"testing" | |
) | |
func TestCmd(t *testing.T) { | |
if o, e := exec.Command("./test.sh", "1", "2").Output(); e != nil { | |
fmt.Println(e) | |
} else { | |
fmt.Println(string(o)) | |
} | |
} |
test.sh
a=$1 | |
b=$2 | |
echo $a | |
echo $b |
上面这个例子的意思是要运行test.sh这个脚本,并且入参是1,2。脚本里面写的东西相对就比较简单了,就是打印这两个入参。其实问题的关键在于exec.Command()这个方法,下面我们来刨根问底,一探究竟。
源码解析
func Command(name string, arg ...string) *Cmd { | |
cmd := &Cmd{ | |
Path: name, | |
Args: append([]string{name}, arg...), | |
} | |
if filepath.Base(name) == name { | |
if lp, err := LookPath(name); err != nil { | |
cmd.lookPathErr = err | |
} else { | |
cmd.Path = lp | |
} | |
} | |
return cmd | |
} | |
// Base返回path的最后一个元素。 | |
// 在提取最后一个元素之前,将删除尾部的路径分隔符。 | |
// 如果路径为空,Base返回"."。 | |
// 如果路径完全由分隔符组成,Base返回单个分隔符。 | |
func Base(path string) string { | |
if path == "" { | |
return "." | |
} | |
// Strip trailing slashes. | |
for len(path) > 0 && os.IsPathSeparator(path[len(path)-1]) { | |
path = path[0 : len(path)-1] | |
} | |
// Throw away volume name | |
path = path[len(VolumeName(path)):] | |
// Find the last element | |
i := len(path) - 1 | |
for i >= 0 && !os.IsPathSeparator(path[i]) { | |
i-- | |
} | |
if i >= 0 { | |
path = path[i+1:] | |
} | |
// If empty now, it had only slashes. | |
if path == "" { | |
return string(Separator) | |
} | |
return path | |
} | |
//LookPath在由PATH环境变量命名的目录中搜索一个名为file入参的可执行文件。如果文件包含一个斜线,就会直接尝试,而不参考PATH。其结果可能是一个绝对路径或相对于当前目录的路径。 | |
func LookPath(file string) (string, error) { | |
if strings.Contains(file, "/") { | |
err := findExecutable(file) | |
if err == nil { | |
return file, nil | |
} | |
return "", &Error{file, err} | |
} | |
path := os.Getenv("PATH") | |
for _, dir := range filepath.SplitList(path) { | |
if dir == "" { | |
// Unix shell semantics: path element "" means "." | |
dir = "." | |
} | |
path := filepath.Join(dir, file) | |
if err := findExecutable(path); err == nil { | |
return path, nil | |
} | |
} | |
return "", &Error{file, ErrNotFound} | |
} | |
// 寻找file同名的可执行命令 | |
func findExecutable(file string) error { | |
d, err := os.Stat(file) | |
if err != nil { | |
return err | |
} | |
if m := d.Mode(); !m.IsDir() && m&0111 != 0 { | |
return nil | |
} | |
return os.ErrPermission | |
} |
通过上面对exec.Command()源码的分析我们可以得知,这个函数只是寻找与path名字相同的可执行文件并且构建了一个Cmd的对象返回。这里值得注意的是,当我们输入的path如果不是一个可执行的文件的具体路径,那么就会去PATH环境变量中的注册的路径中找寻与path相同名字的命令,如果这个时候没有找到就会报错。
那么接下来我们那看看这个Cmd是何方神圣呢,有什么用,怎么用呢。下面我们看看Cmd这个结构体里都有些什么东西。
// Cmd结构体代表一个准备或正在执行的外部命令 | |
// 一个Cmd的对象不能在Run,Output或者CombinedOutput方法调用之后重复使用。 | |
type Cmd struct { | |
// Path代表运行命令的路径 | |
// 这个字段是唯一一个需要被赋值的字段,不能是空字符串, | |
// 并且如果Path是相对路径,那么参照的是Dir这个字段的所指向的目录 | |
Path string | |
// Args这个字段代表调用命令所需的参数,其中Path在运行命令时以Args[0]的形式存在 | |
// 如果这个参数是空,那个就直接使用Path运行命令 | |
// | |
// 在较为普遍普遍的场景里面,Path和Args这两个参数在调用命令的时候都会被用到 | |
Args []string | |
// Env代表当前进程的环境变量 | |
// 每个Env数组中的条目都以“key=value”的形式存在 | |
// 如果Env是nil,那边运行命令所创建的进程将使用当前进程的环境变量 | |
// 如果Env中存在重复的key,那么会使用这个key中排在最后一个的值。 | |
// 在Windows中存在特殊的情况, 如果系统中缺失了SYSTEMROOT,或者这个环境变量没有被设置成空字符串,那么它操作都是追加操作。 | |
Env []string | |
// Dir代表命令的运行路径 | |
// 如果Dir是空字符串,那么命令就会运行在当前进程的运行路径 | |
Dir string | |
// Stdin代表的是系统的标准输入流 | |
// 如果Stdin是一个*os.File,那么进程的标准输入将被直接连接到该文件。 | |
Stdin io.Reader | |
// Stdout表示标准输出流 | |
// 如果StdOut是一个*os.File,那么进程的标准输入将被直接连接到该文件。 | |
// 值得注意的是如果StdOut和StdErr是同一个对象,那么同一时间只有一个协程可以调用Writer | |
Stdout io.Writer | |
Stderr io.Writer | |
// ExtraFiles指定由新进程继承的额外开放文件。它不包括标准输入、标准输出或标准错误。如果不为零,第i项成为文件描述符3+i。 | |
// ExtraFiles前面三个元素分别放的是stdin,stdout,stderr | |
// ExtraFiles在Windows上是不支持的 | |
ExtraFiles []*os.File | |
SysProcAttr *syscall.SysProcAttr | |
// 当命令运行之后,Process就是该命令运行所代表的进程 | |
Process *os.Process | |
// ProcessState包含关于一个退出的进程的信息,在调用Wait或Run后可用。 | |
ProcessState *os.ProcessState | |
ctx context.Context // ctx可以用来做超时控制 | |
lookPathErr error // 如果在调用LookPath寻找路径的时候出错了,就赋值到这个字段 | |
finished bool // 当Wait被调用了一次之后就会被设置成True,防止被重复调用 | |
childFiles []*os.File | |
closeAfterStart []io.Closer | |
closeAfterWait []io.Closer | |
goroutine []func() error //一系列函数,在调用Satrt开始执行命令的时候会顺带一起执行这些函数。每个函数分配一个goroutine执行 | |
errch chan error // 与上一个字段联合使用,通过这个chan将上面函数执行的结果传到当前goroutine | |
waitDone chan struct{} | |
} |
上面我们对Cmd这个结构体的一些字段做了解析,可以理解为Cmd就是对一个命令生命周期内的抽象。下面我们来分析Cmd的一下方法,看看他是怎么使用的。
// Run方法开始执行这个命令并等待它运行结束 | |
// 如果命令运行,在复制stdin、stdout和stder时没有问题,并且以零退出状态退出,则返回的错误为nil。 | |
// 如果命令启动但没有成功完成,错误类型为类型为*ExitError。在其他情况下可能会返回其他错误类型。 | |
// 如果调用的goroutine已经用runtime.LockOSThread锁定了操作系统线程,并修改了任何可继承的OS级 线程状态(例如,Linux或Plan 9名称空间),新的 进程将继承调用者的线程状态。 | |
func (c *Cmd) Run() error { | |
if err := c.Start(); err != nil { | |
return err | |
} | |
return c.Wait() | |
} | |
// Start方法启动指定的命令,但不等待它完成。 | |
// | |
// 如果Start成功返回,c.Process字段将被设置。 | |
// | |
// 一旦命令运行完成,Wait方法将返回退出代码并释放相关资源。 | |
func (c *Cmd) Start() error { | |
if c.lookPathErr != nil { | |
c.closeDescriptors(c.closeAfterStart) | |
c.closeDescriptors(c.closeAfterWait) | |
return c.lookPathErr | |
} | |
if runtime.GOOS == "windows" { | |
lp, err := lookExtensions(c.Path, c.Dir) | |
if err != nil { | |
c.closeDescriptors(c.closeAfterStart) | |
c.closeDescriptors(c.closeAfterWait) | |
return err | |
} | |
c.Path = lp | |
} | |
if c.Process != nil { | |
return errors.New("exec: already started") | |
} | |
if c.ctx != nil { | |
select { | |
case <-c.ctx.Done(): | |
c.closeDescriptors(c.closeAfterStart) | |
c.closeDescriptors(c.closeAfterWait) | |
return c.ctx.Err() | |
default: | |
} | |
} | |
//初始化并填充ExtraFiles | |
c.childFiles = make([]*os.File, 0, 3+len(c.ExtraFiles)) | |
type F func(*Cmd) (*os.File, error) | |
//在这里会调用stdin,stdout和stderr方法,如果Cmd的StdIn,StdOut,StdErr不是nil,就会将相关的copy任务封装成func放在goroutine字段中,等待在Start方法执行的时候调用。 | |
for _, setupFd := range []F{(*Cmd).stdin, (*Cmd).stdout, (*Cmd).stderr} { | |
fd, err := setupFd(c) | |
if err != nil { | |
c.closeDescriptors(c.closeAfterStart) | |
c.closeDescriptors(c.closeAfterWait) | |
return err | |
} | |
c.childFiles = append(c.childFiles, fd) | |
} | |
c.childFiles = append(c.childFiles, c.ExtraFiles...) | |
// 如果cmd的Env没有赋值,那么就用当前进程的环境变量 | |
envv, err := c.envv() | |
if err != nil { | |
return err | |
} | |
// 会用这个命令启动一个新的进程 | |
// 在Linux的系统上,底层是调用了Frok来创建另一个进程,由于文章篇幅有限,就不对此处进行详细分析了,详情可看延伸阅读 | |
c.Process, err = os.StartProcess(c.Path, c.argv(), &os.ProcAttr{ | |
Dir: c.Dir, | |
Files: c.childFiles, | |
Env: addCriticalEnv(dedupEnv(envv)), | |
Sys: c.SysProcAttr, | |
}) | |
if err != nil { | |
c.closeDescriptors(c.closeAfterStart) | |
c.closeDescriptors(c.closeAfterWait) | |
return err | |
} | |
c.closeDescriptors(c.closeAfterStart) | |
// 除非有goroutine要启动,否则不会申请Chan | |
if len(c.goroutine) > 0 { | |
c.errch = make(chan error, len(c.goroutine)) | |
for _, fn := range c.goroutine { | |
go func(fn func() error) { | |
c.errch <- fn() | |
}(fn) | |
} | |
} | |
// 超时控制 | |
if c.ctx != nil { | |
c.waitDone = make(chan struct{}) | |
go func() { | |
select { | |
case <-c.ctx.Done(): //如果超时了,就Kill掉执行命令的进程 | |
c.Process.Kill() | |
case <-c.waitDone: | |
} | |
}() | |
} | |
return nil | |
} | |
func (c *Cmd) stdin() (f *os.File, err error) { | |
if c.Stdin == nil { | |
f, err = os.Open(os.DevNull) | |
if err != nil { | |
return | |
} | |
c.closeAfterStart = append(c.closeAfterStart, f) | |
return | |
} | |
if f, ok := c.Stdin.(*os.File); ok { | |
return f, nil | |
} | |
//Pipe返回一对相连的Files;从r读出的数据返回写到w的字节。 | |
pr, pw, err := os.Pipe() | |
if err != nil { | |
return | |
} | |
c.closeAfterStart = append(c.closeAfterStart, pr) | |
c.closeAfterWait = append(c.closeAfterWait, pw) | |
//将相关的任务添加到goroutine中 | |
c.goroutine = append(c.goroutine, func() error { | |
_, err := io.Copy(pw, c.Stdin) | |
if skip := skipStdinCopyError; skip != nil && skip(err) { | |
err = nil | |
} | |
if err1 := pw.Close(); err == nil { | |
err = err1 | |
} | |
return err | |
}) | |
return pr, nil | |
} | |
func (c *Cmd) stdout() (f *os.File, err error) { | |
return c.writerDescriptor(c.Stdout) | |
} | |
func (c *Cmd) stderr() (f *os.File, err error) { | |
if c.Stderr != nil && interfaceEqual(c.Stderr, c.Stdout) { | |
return c.childFiles[1], nil | |
} | |
return c.writerDescriptor(c.Stderr) | |
} | |
func (c *Cmd) writerDescriptor(w io.Writer) (f *os.File, err error) { | |
if w == nil { | |
f, err = os.OpenFile(os.DevNull, os.O_WRONLY, 0) | |
if err != nil { | |
return | |
} | |
c.closeAfterStart = append(c.closeAfterStart, f) | |
return | |
} | |
if f, ok := w.(*os.File); ok { | |
return f, nil | |
} | |
pr, pw, err := os.Pipe() | |
if err != nil { | |
return | |
} | |
c.closeAfterStart = append(c.closeAfterStart, pw) | |
c.closeAfterWait = append(c.closeAfterWait, pr) | |
//将相关的任务添加到goroutine中 | |
c.goroutine = append(c.goroutine, func() error { | |
_, err := io.Copy(w, pr) | |
pr.Close() // in case io.Copy stopped due to write error | |
return err | |
}) | |
return pw, nil | |
} | |
// 等待命令退出,并等待任何复制到stdin或从stdout或stderr复制的完成。 | |
// 在调用Wait之前,Start方法必须被调用 | |
// 如果命令运行,在复制stdin、stdout和stder时没有问题,并且以零退出状态退出,则返回的错误为nil。 | |
// 如果命令运行失败或没有成功完成,错误类型为*ExitError。对于I/O问题可能会返回其他错误类型。 | |
// 如果c.Stdin、c.Stdout或c.Stderr中的任何一个不是*os.File,Wait也会等待各自的I/O循环复制到进程中或从进程中复制出来 | |
// | |
// Wait释放与Cmd相关的任何资源。 | |
func (c *Cmd) Wait() error { | |
if c.Process == nil { | |
return errors.New("exec: not started") | |
} | |
if c.finished { | |
return errors.New("exec: Wait was already called") | |
} | |
c.finished = true | |
//等待进程运行完毕并退出 | |
state, err := c.Process.Wait() | |
if c.waitDone != nil { | |
close(c.waitDone) | |
} | |
c.ProcessState = state | |
//检查goroutine字段上面的函数运行有没有错误 | |
var copyError error | |
for range c.goroutine { | |
if err := <-c.errch; err != nil && copyError == nil { | |
copyError = err | |
} | |
} | |
c.closeDescriptors(c.closeAfterWait) | |
if err != nil { | |
return err | |
} else if !state.Success() { | |
return &ExitError{ProcessState: state} | |
} | |
return copyError | |
} | |
// 输出运行该命令并返回其标准输出。 | |
// 任何返回的错误通常都是*ExitError类型的。 | |
// OutPut实际上是封装了命令的执行流程并且制定了命令的输出流 | |
func (c *Cmd) Output() ([]byte, error) { | |
if c.Stdout != nil { | |
return nil, errors.New("exec: Stdout already set") | |
} | |
var stdout bytes.Buffer | |
c.Stdout = &stdout | |
captureErr := c.Stderr == nil | |
if captureErr { | |
c.Stderr = &prefixSuffixSaver{N: 32 << 10} | |
} | |
err := c.Run() | |
if err != nil && captureErr { | |
if ee, ok := err.(*ExitError); ok { | |
ee.Stderr = c.Stderr.(*prefixSuffixSaver).Bytes() | |
} | |
} | |
return stdout.Bytes(), err | |
} |
在上面的方法分析之中我们可以看出运行一个命令的流程是Run-> Start->Wait,等待命令运行完成。并且在Start的时候会起来一个新的进程来执行命令。基于上面我们对Cmd的一顿分析,笔者感觉在文章开头写的测试代码实在是乏善可陈,因为Cmd封装了挺多东西的,我们在工作中完全可以充分利用他封装的功能,比如设置超时时间,设置标准输入流或者标准输出流,还可以定制化设置这个命令执行的环境变量等等。。。。
延伸阅读
- 关于fork和exec:www.cnblogs.com/hicjiajia/archive/...