Golang 中执行一次性命令是比较简单的直接调用 exec.Cmd.Run() 即可,那怎么执行交互式的命令比如 python 我们想动态的执行一些 python 的命令并获取他们的结果,而不是直接执行一个 .py 的 python 脚本。这样可以让程序有更多的灵活性。
进程创建
在 Unix 中,创建一个进程,通过系统调用 fork 实现(及其一些变种,如 vfork、clone)。在 Go 语言中,Linux 下创建进程使用的系统调用是 clone。
很多时候,系统调用 fork、execve、wait 和 exit 会在一起出现。此处先简要介绍这 4 个系统调用及其典型用法。
fork:允许一进程(父进程)创建一新进程(子进程)。具体做法是,新的子进程几近于对父进程的翻版:子进程获得父进程的栈、数据段、堆和执行文本段的拷贝。可将此视为把父进程一分为二。
exit(status):终止一进程,将进程占用的所有资源(内存、文件描述符等)归还内核,交其进行再次分配。参数 status 为一整型变量,表示进程的退出状态。父进程可使用系统调用 wait() 来获取该状态。
wait(&status) 目的有二:其一,如果子进程尚未调用 exit() 终止,那么 wait 会挂起父进程直至子进程终止;其二,子进程的终止状态通过 wait 的 status 参数返回。
execve(pathname, argv, envp) 加载一个新程序(路径名为 pathname,参数列表为 argv,环境变量列表为 envp)到当前进程的内存。这将丢弃现存的程序文本段,并为新程序重新创建栈、数据段以及堆。通常将这一动作称为执行一个新程序。
在 Go 语言中,没有直接提供 fork 系统调用的封装,而是将 fork 和 execve 合二为一,提供了 syscall.ForkExec。如果想只调用 fork,得自己通过 syscall.Syscall(syscall.SYS_FORK, 0, 0, 0) 实现。
Go 创建子进程
os.StartProcess 更底层
exec.Cmd() 比 StartProcess 做了一些封装方便调用
查找可执行程序
exec.LookPath 函数在 PATH 指定目录中搜索可执行程序,如 file 中有 /,则只在当前目录搜索。该函数返回完整路径或相对于当前路径的一个相对路径。
func LookPath(file string) (string, error)
如果在 PATH 中没有找到可执行文件,则返回 exec.ErrNotFound
exec CMD
exec 包里面的 CMD 包括了下面一些常用的使用方法
Start
func (c *Cmd) Start() error
开始执行 c 包含的命令,但并不会等待该命令完成即返回。Wait 方法会返回命令的退出状态码并在命令执行完后释放相关的资源。内部调用 os.StartProcess,执行 forkExec。
Wait
func (c *Cmd) Wait() error
Wait 会阻塞直到该命令执行完成,该命令必须是先通过 Start 执行。
如果命令成功执行,stdin、stdout、stderr 数据传递没有问题,并且返回状态码为 0,方法的返回值为 nil;如果命令没有执行或者执行失败,会返回 *ExitError 类型的错误;否则返回的 error 可能是表示 I/O 问题。
如果 c.Stdin 不是 *os.File 类型,Wait 会等待,直到数据从 c.Stdin 拷贝到进程的标准输入。
Wait 方法会在命令返回后释放相关的资源。
Output
除了 Run() 是 Start+Wait 的简便写法,Output() 更是 Run() 的简便写法,外加获取外部命令的输出。
func (c *Cmd) Output() ([]byte, error)
它要求 c.Stdout 必须是 nil,内部会将 bytes.Buffer 赋值给 c.Stdout,在 Run() 成功返回后,会将 Buffer 的结果返回(stdout.Bytes())。
CombinedOutput
Output() 只返回 Stdout 的结果,而 CombinedOutput 组合 Stdout 和 Stderr 的输出,即 Stdout 和 Stderr 都赋值为同一个 bytes.Buffer。
StdoutPipe、StderrPipe 和 StdinPipe
除了上面介绍的 Output 和 CombinedOutput 直接获取命令输出结果外,还可以通过 StdoutPipe 返回 io.ReadCloser 来获取输出;相应的 StderrPipe 得到错误信息;而 StdinPipe 则可以往命令写入数据。
func (c *Cmd) StdoutPipe() (io.ReadCloser, error)
StdoutPipe 方法返回一个在命令 Start 执行后与命令标准输出关联的管道。Wait 方法会在命令结束后会关闭这个管道,所以一般不需要手动关闭该管道。但是在从管道读取完全部数据之前调用 Wait 出错了,则必须手动关闭。
func (c *Cmd) StderrPipe() (io.ReadCloser, error)
StderrPipe 方法返回一个在命令 Start 执行后与命令标准错误输出关联的管道。Wait 方法会在命令结束后会关闭这个管道,一般不需要手动关闭该管道。但是在从管道读取完全部数据之前调用 Wait 出错了,则必须手动关闭。
func (c *Cmd) StdinPipe() (io.WriteCloser, error)
StdinPipe 方法返回一个在命令 Start 执行后与命令标准输入关联的管道。Wait 方法会在命令结束后会关闭这个管道。必要时调用者可以调用 Close 方法来强行关闭管道。例如,标准输入已经关闭了,命令执行才完成,这时调用者需要显示关闭管道。
因为 Wait 之后,会将管道关闭,所以,要使用这些方法,只能使用 Start+Wait 组合,不能使用 Run。
交互式命令实现
直接接管输入输出
Go
Copy
cmd := exec.Command("python", "-i")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Start()
cmd.Wait()
提前把交互数据写好,写入 stdin
Go
Copy
cmd := exec.Command("python", "-i")
cmd.Stdin = strings.NewReader("print(1+1)\r\n")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
或者使用 bytes buffer 替换 string.Reader
Go
Copy
cmd := exec.Command("python", "-i")
in := &bytes.Buffer{}
in.WriteString("print(1+1)\n")
cmd.Stdin = in
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
注意:在 cmd 命令 run 或者 start 以前需要讲数据写入
实现启动命令后持续往里面写数据
Go
Copy
cmd := exec.Command("python", "-i")
in := &bytes.Buffer{}
cmd.Stdin = in
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Start()
go func() {
for i := 0; i < 10; i++ {
in.WriteString("print(1+1)\n")
}
}()
time.Sleep(time.Second * 10)
cmd.Wait()
使用 bytes buffer 时只能一次批量写入,如果间隔了 sleep 会导致数据写入不成功
比如 in.WriteString("print(1+1)\n") 后面 sleep 后再跟一个 writeString
使用管道持续读写
Go
Copy
cmd := exec.Command("python", "-i")
in, _ := cmd.StdinPipe()
cmd.Stdout = os.Stdout
cmd.Start()
go func() {
for i := 0; i < 10; i++ {
io.WriteString(in, "print(1+1)\n")
time.Sleep(time.Second)
}
io.WriteString(in, "exit()\n")
}()
cmd.Wait()
注意:cmd := exec.Command("python", "-i") 如果不带 -i 参数会让子进程 python 读取不到 stdin 的参数
Go
Copy
-i : inspect interactively after running script; forces a prompt even
if stdin does not appear to be a terminal; also PYTHONINSPECT=x
同理对于其它类似的交互子进程需要注意是否有对 stdin 做了特殊处理
利用 bash 管道往 stdin 里面写内容
Go
Copy
cmd := exec.Command("bash", "-c", `python3 << eeooff
print(1+1)
eeooff
`)
out, err := cmd.Output()
这种情况比较像往 buffer 里面提前写一部分数据,但是不同在于部分命令没有像 python 提供 -i 参数,只能读取 terminal 里面的 stdin,这个时候通过 bash 管道就可以很好的避免这个问题
读取交互管道里面的内容
Go
Copy
stdout, _ := cmd.StdoutPipe()
defer stdout.Close()
go func() {
s := bufio.NewScanner(stdout)
for s.Scan() {
fmt.Println("Program says:" + s.Text())
}
}()
参考: