如何在 Go 中传递多个命令?
Posted
技术标签:
【中文标题】如何在 Go 中传递多个命令?【英文标题】:How to pipe several commands in Go? 【发布时间】:2012-06-02 15:15:39 【问题描述】:如何在 Go 中将多个外部命令连接在一起?我已经尝试过这段代码,但我收到一个错误,上面写着exit status 1
。
package main
import (
"io"
"log"
"os"
"os/exec"
)
func main()
c1 := exec.Command("ls")
stdout1, err := c1.StdoutPipe()
if err != nil
log.Fatal(err)
if err = c1.Start(); err != nil
log.Fatal(err)
if err = c1.Wait(); err != nil
log.Fatal(err)
c2 := exec.Command("wc", "-l")
c2.Stdin = stdout1
stdout2, err := c2.StdoutPipe()
if err != nil
log.Fatal(err)
if err = c2.Start(); err != nil
log.Fatal(err)
if err = c2.Wait(); err != nil
log.Fatal(err)
io.Copy(os.Stdout, stdout2)
【问题讨论】:
【参考方案1】:对于简单的场景,您可以使用这种方法:
bash -c "echo 'your command goes here'"
例如,此函数使用管道命令检索 CPU 型号名称:
func getCPUmodel() string
cmd := "cat /proc/cpuinfo | egrep '^model name' | uniq | awk 'print substr($0, index($0,$4))'"
out, err := exec.Command("bash","-c",cmd).Output()
if err != nil
return fmt.Sprintf("Failed to execute command: %s", cmd)
return string(out)
【讨论】:
但是需要注意的是,如果 cmd 太长,这将失败。特别是如果它的长度超过 131072 字节,那么您可能会得到类似fork/exec /bin/bash: argument list too long
的信息,请参阅 here。在这种情况下,您可能会更改或拆分您的命令,或者求助于本问题答案中其他地方列出的更强大的 io.Pipe
方法。
这个问题/答案还有另一个关键点。 Go 中的“命令”是什么?正如人们所期望的那样,它代表可执行文件,而不是“shell 命令”。所以,这里的命令是bash
,带有一个选项(-c
)和一个“shell 命令”参数。有人可能会争辩说,bash
在系统上可能不可用,这比 100KB 的“命令”更有可能破坏这个解决方案。一堆管道和缓冲区 + 十几行代码来收集单行 shell 命令输出(甚至不再读取为单行),这是完全不可接受的。我认为这应该是公认的。
这应该是最简单的答案,尽管它取决于bash
。这很好!
我应该注意,在大多数情况下,CombinedOutput() 可能比 Output() 更好,因为它包含程序的 STDERR 输出,因此您可以查看是否发生错误,而不是出现静默错误
将有一个换行符作为Output()
的一部分,作为out
中的最后一个字节存储。可以通过重新切片将其剥离,即out = out[:len(out)-1]
【参考方案2】:
StdoutPipe 返回一个将连接到命令的管道 命令启动时的标准输出。管道将关闭 Wait 看到命令退出后自动退出。
(来自http://golang.org/pkg/os/exec/#Cmd.StdinPipe)
您执行c1.Wait
的事实关闭了stdoutPipe
。
我做了一个工作示例(只是一个演示,添加错误捕获!):
package main
import (
"bytes"
"io"
"os"
"os/exec"
)
func main()
c1 := exec.Command("ls")
c2 := exec.Command("wc", "-l")
r, w := io.Pipe()
c1.Stdout = w
c2.Stdin = r
var b2 bytes.Buffer
c2.Stdout = &b2
c1.Start()
c2.Start()
c1.Wait()
w.Close()
c2.Wait()
io.Copy(os.Stdout, &b2)
【讨论】:
为什么使用 io.Pipe 而不是 exec.Cmd.StdoutPipe? 我也喜欢 io.Pipe,但是将 c1 start 放入单独的 goroutine 对我来说效果更好。请参阅下面的修改版本。 @WeakPointer 何时使用os.Pipe()
?因为io.Pipe()
在上面的代码中执行IPC没有任何问题
@overexchange 对不起,不明白这个问题。我已经有好几年没有认真看过这些东西了,但它们有非常不同的签名,不是吗? os.Pipe 将一个 *os.File 连接到另一个。 io.Pipe() 返回两项,一项可以在字节切片上执行 io.Read,一项可以在字节切片上执行 io.Write。
@WeakPointer 我对os.Pipe()
和io.Pipe()
的返回类型感到困惑。 os.Pipe()
返回 File*
并且文档说,Pipe returns a connected pair of Files; reads from r return bytes written to w. It returns the files and an error, if any.
那么,这与 io.Pipe()
返回的 io.Reader
和 io.Writer
有何不同?【参考方案3】:
package main
import (
"os"
"os/exec"
)
func main()
c1 := exec.Command("ls")
c2 := exec.Command("wc", "-l")
c2.Stdin, _ = c1.StdoutPipe()
c2.Stdout = os.Stdout
_ = c2.Start()
_ = c1.Run()
_ = c2.Wait()
【讨论】:
我基本上使用相同的代码,但我经常收到“破管”错误。知道是什么原因造成的吗? ***.com/q/26122072/4063955 @AnthonyHat:请将此评论放在您的新问题上,这样我们就可以看到您看到了这个问题,但它对您不起作用。 当一个进程尝试写入管道但管道的另一侧已关闭时,会发生管道损坏。例如,如果“wc -l”在上例中的“ls”完成之前退出,“ls”将收到 Broken Pipe 错误/信号。 @user7044,我不确定你的意思。在本例中,“ls”和“wc -l”这两个命令同时运行,ls 的输出通过管道传送到 wc,wc 可以在 ls 完成全部写入之前开始读取 ls 的输出。 这个答案似乎不正确。文档说“出于同样的原因,在使用 StdoutPipe 时调用 Run 是不正确的。”见pkg.go.dev/os/exec#Cmd.StdoutPipe 可能这也解释了@AnthonyHunt 的破损管道。【参考方案4】:与第一个答案一样,但第一个命令在 goroutine 中启动并等待。这让管道保持快乐。
package main
import (
"io"
"os"
"os/exec"
)
func main()
c1 := exec.Command("ls")
c2 := exec.Command("wc", "-l")
pr, pw := io.Pipe()
c1.Stdout = pw
c2.Stdin = pr
c2.Stdout = os.Stdout
c1.Start()
c2.Start()
go func()
defer pw.Close()
c1.Wait()
()
c2.Wait()
【讨论】:
如果它使用 os.Pipe() 而不是 io.Pipe(),它可能会在没有 goroutine 的情况下正常工作。让操作系统自己进行字节洗牌。 @JasonStewart 这个建议似乎是正确的,谢谢。到目前为止,我已经开始使用它,没有任何不良影响。 @WeakPointer 什么时候使用os.Pipe()
...如果io.Pipe()
可以进行IPC?albertoleal.me/posts/golang-pipes.html【参考方案5】:
这是一个完整的示例。 Execute
函数接受任意数量的exec.Cmd
实例(使用variadic function),然后循环它们正确地将stdout 的输出附加到下一个命令的stdin。这必须在调用任何函数之前完成。
call 函数然后在循环中调用命令,使用 defers 递归调用并确保正确关闭管道
package main
import (
"bytes"
"io"
"log"
"os"
"os/exec"
)
func Execute(output_buffer *bytes.Buffer, stack ...*exec.Cmd) (err error)
var error_buffer bytes.Buffer
pipe_stack := make([]*io.PipeWriter, len(stack)-1)
i := 0
for ; i < len(stack)-1; i++
stdin_pipe, stdout_pipe := io.Pipe()
stack[i].Stdout = stdout_pipe
stack[i].Stderr = &error_buffer
stack[i+1].Stdin = stdin_pipe
pipe_stack[i] = stdout_pipe
stack[i].Stdout = output_buffer
stack[i].Stderr = &error_buffer
if err := call(stack, pipe_stack); err != nil
log.Fatalln(string(error_buffer.Bytes()), err)
return err
func call(stack []*exec.Cmd, pipes []*io.PipeWriter) (err error)
if stack[0].Process == nil
if err = stack[0].Start(); err != nil
return err
if len(stack) > 1
if err = stack[1].Start(); err != nil
return err
defer func()
if err == nil
pipes[0].Close()
err = call(stack[1:], pipes[1:])
()
return stack[0].Wait()
func main()
var b bytes.Buffer
if err := Execute(&b,
exec.Command("ls", "/Users/tyndyll/Downloads"),
exec.Command("grep", "as"),
exec.Command("sort", "-r"),
); err != nil
log.Fatalln(err)
io.Copy(os.Stdout, &b)
在此要点中可用
https://gist.github.com/tyndyll/89fbb2c2273f83a074dc
要知道的一点是,像 ~ 这样的 shell 变量不会被插值
【讨论】:
已更新 - 在我的辩护中,经过几个小时的工作,我会在凌晨 5 点回答这个问题 :)【参考方案6】:package main
import (
...
pipe "github.com/b4b4r07/go-pipe"
)
func main()
var b bytes.Buffer
pipe.Command(&b,
exec.Command("ls", "/Users/b4b4r07/Downloads"),
exec.Command("grep", "Vim"),
)
io.Copy(os.Stdout, &b)
在遇到this neat package by b4b4r07 之前,我花了一天时间尝试使用Denys Séguret 回答来为多个exec.Command
提供一个包装器。
【讨论】:
我刚刚意识到这个包的实现与上面@Tyndyll 的答案相同。只是注意到... 我不知道,也许这对每个人来说都很明显,但对我来说并不那么明显,我学到了当你实际调用 io.Copy() 最后你不会得到结果,因为它已经在 &b :)【参考方案7】:我想将一些视频和音频传输到 FFplay。这对我有用:
package main
import (
"io"
"os/exec"
)
func main()
ffmpeg := exec.Command(
"ffmpeg", "-i", "247.webm", "-i", "251.webm", "-c", "copy", "-f", "webm", "-",
)
ffplay := exec.Command("ffplay", "-")
ffplay.Stdin, ffmpeg.Stdout = io.Pipe()
ffmpeg.Start()
ffplay.Run()
https://golang.org/pkg/io#Pipe
【讨论】:
【参考方案8】:因为构建这样的命令链可能很复杂,所以我决定为此目的实现一个 litte go 库:https://github.com/rainu/go-command-chain
package main
import (
"bytes"
"fmt"
"github.com/rainu/go-command-chain"
)
func main()
output := &bytes.Buffer
err := cmdchain.Builder().
Join("ls").
Join("wc", "-l").
Finalize().WithOutput(output).Run()
if err != nil
panic(err)
fmt.Printf("Errors found: %s", output)
借助这个库,您还可以配置标准错误转发和其他东西。
【讨论】:
以上是关于如何在 Go 中传递多个命令?的主要内容,如果未能解决你的问题,请参考以下文章