如何在 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.Readerio.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 中传递多个命令?的主要内容,如果未能解决你的问题,请参考以下文章

如何在运行配置中传递多个 pytest 命令行选项

如何在erl中为单个标志设置多个命令行参数

ansible:如何传递多个命令

如何通过 c# 传递多个命令行参数

如何将time.Duration类型传递给go函数?

如何让 perl 正确传递具有多个参数和复杂文件路径(空格和符号)的命令行参数?