【问题标题】:How to pipe several commands in Go?如何在 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)
}

【问题讨论】:

    标签: go pipe


    【解决方案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()
    }
    

    【讨论】:

    • 我基本上使用相同的代码,但我经常收到“破管”错误。知道是什么原因造成的吗? stackoverflow.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)
      }
      

      借助这个库,您还可以配置标准错误转发和其他东西。

      【讨论】:

        猜你喜欢
        • 2014-09-11
        • 2020-12-03
        • 2016-05-13
        • 1970-01-01
        • 2021-06-01
        • 2020-11-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多