Wie leite ich mehrere Befehle in Go weiter?

75

Wie kann ich in Go mehrere externe Befehle zusammenleiten? Ich habe diesen Code ausprobiert, erhalte jedoch eine Fehlermeldung 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)
}
Kevin Burke
quelle

Antworten:

53

StdoutPipe gibt eine Pipe zurück, die beim Start des Befehls mit der Standardausgabe des Befehls verbunden wird. Die Pipe wird automatisch geschlossen, nachdem Wait den Befehl beendet hat.

(von http://golang.org/pkg/os/exec/#Cmd.StdinPipe )

Die Tatsache, dass Sie dies tun, c1.Waitschließt das stdoutPipe.

Ich habe ein funktionierendes Beispiel gemacht (nur eine Demo, Fehlerbehebung hinzufügen!):

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)
}
Denys Séguret
quelle
6
Warum verwenden Sie io.Pipe anstelle von exec.Cmd.StdoutPipe?
Ich mag io.Pipe auch, aber es funktioniert besser für mich, den c1-Start in eine separate Goroutine zu setzen. Siehe meine modifizierte Version unten.
WeakPointer
@WeakPointer Wann verwenden os.Pipe()? weil io.Pipe()IPC ohne
Probleme
@overexchange Sorry, verstehe die Frage nicht. Es ist Jahre her, seit ich mir dieses Zeug intensiv angesehen habe, aber sie haben sehr unterschiedliche Signaturen, nicht wahr? os.Pipe verbindet eine * os.File mit einer anderen. io.Pipe () gibt zwei Elemente zurück, eines kann io.Read auf einem Byte-Slice ausführen und man kann io.Write auf einem Byte-Slice ausführen.
WeakPointer
@WeakPointer Am verwechseln mit Rückgabetypen von os.Pipe()vs io.Pipe(). os.Pipe()kehrt zurück File*und die Dokumentation sagt: Pipe returns a connected pair of Files; reads from r return bytes written to w. It returns the files and an error, if any.Also, wie unterscheidet sich das von io.Reader& io.Writerdas io.Pipe()zurück?
Überaustausch
115

Für einfache Szenarien können Sie diesen Ansatz verwenden:

bash -c "echo 'your command goes here'"

Diese Funktion ruft beispielsweise den Namen des CPU-Modells mithilfe von Piped-Befehlen ab:

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)
}
Stefan Saru
quelle
Es ist jedoch zu beachten, dass dies fehlschlägt, wenn der cmd zu lang ist. Insbesondere lang , wenn es über 131.072 Bytes ist, dann bringen Sie wahrscheinlich so etwas wie fork/exec /bin/bash: argument list too longfinden Sie hier . In diesem Fall müssen Sie möglicherweise Ihren Befehl ändern oder aufteilen oder auf die umfangreicheren io.PipeMethoden zurückgreifen, die an anderer Stelle in den Antworten dieser Frage aufgeführt sind.
Henrywallace
Aus dieser Frage / Antwort ergibt sich ein weiterer wichtiger Aspekt. Was für ein Befehl ist in Go? Es steht für eine ausführbare Datei und nicht wie erwartet für einen 'Shell-Befehl'. Der Befehl hier lautet also bashmit einer Option ( -c) und einem Argument 'shell command'. Man könnte argumentieren, dass dies bashmöglicherweise nicht auf dem System verfügbar ist, und das ist viel wahrscheinlicher als ein 100-KB-Befehl, um diese Lösung zu brechen. Ein Bündel von Pipes und Puffern + Dutzend Codezeilen zum Sammeln einer einzeiligen Shell-Befehlsausgabe (die nicht einmal mehr als einzeilig gelesen wird) ist einfach inakzeptabel. Ich denke, das sollte akzeptiert werden.
Tishma
Dies sollte die einfachste Antwort sein, auch wenn es darauf ankommt bash. Das ist gut!
Marcello de Sales
54
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()
}
Matt
quelle
Ich verwende im Grunde den gleichen Code, aber oft erhalte ich den Fehler "Rohrbruch". Irgendeine Idee, was das verursachen könnte? stackoverflow.com/q/26122072/4063955
Anthony Hunt
@AnthonyHat: Bitte setzen Sie diesen Kommentar auf Ihre neue Frage, damit wir sehen können, dass Sie diese gesehen haben und sie bei Ihnen nicht funktioniert hat.
RickyA
Eine unterbrochene Pipe tritt auf, wenn ein Prozess versucht, in eine Pipe zu schreiben, die andere Seite der Pipe jedoch bereits geschlossen wurde. Wenn zum Beispiel das "wc -l" beendet wird, bevor das "ls" im obigen Beispiel beendet ist, würde das "ls" einen Fehler / ein Signal mit gebrochener Leitung erhalten.
Matt
Wie kann ich dieses Programm gleichzeitig machen, da hier io passiert (stdin / stdout) !!
user7044
4
@ user7044, ich bin mir nicht sicher was du meinst. In diesem Beispiel werden die beiden Befehle "ls" und "wc -l" gleichzeitig ausgeführt, wobei die Ausgabe von ls an wc weitergeleitet wird, wodurch die Ausgabe von ls gelesen werden kann, bevor ls das Schreiben beendet hat.
Matt
7

Wie die erste Antwort, aber mit dem ersten Befehl gestartet und in einer Goroutine gewartet. Das macht die Pfeife glücklich.

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()
}
Schwacher Zeiger
quelle
1
Ohne die Goroutine würde es wahrscheinlich gut funktionieren, wenn os.Pipe () anstelle von io.Pipe () verwendet würde. Lassen Sie das Betriebssystem das Byte-Shuffling selbst durchführen.
Jason Stewart
1
@ JasonStewart Dieser Rat scheint richtig zu sein, danke. Ich habe bisher damit begonnen, es ohne negative Auswirkungen zu verwenden.
WeakPointer
@WeakPointer Wann verwenden os.Pipe()... wenn io.Pipe()IPC durchgeführt werden kann? albertoleal.me/posts/golang-pipes.html
Überaustausch
4

Dies ist ein voll funktionsfähiges Beispiel. Die ExecuteFunktion nimmt eine beliebige Anzahl von exec.CmdInstanzen (unter Verwendung einer variadischen Funktion ) und durchläuft sie dann korrekt, wobei die Ausgabe von stdout korrekt an die stdin des nächsten Befehls angehängt wird . Dies muss erfolgen, bevor eine Funktion aufgerufen wird.

Die Aufruffunktion ruft dann die Befehle in einer Schleife auf, verwendet Verzögerungen, um rekursiv aufzurufen, und stellt das ordnungsgemäße Schließen von Pipes sicher

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)
}

Verfügbar in diesem Kern

https://gist.github.com/tyndyll/89fbb2c2273f83a074dc

Ein guter Punkt zu wissen ist, dass Shell-Variablen wie ~ nicht interpoliert werden

Tyndyll
quelle
Aktualisiert - zu meiner Verteidigung hatte ich es um 5 Uhr morgens beantwortet, nachdem ich ein paar Stunden daran gearbeitet hatte :)
Tyndyll
1
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)
}

Ich habe einen guten Tag damit verbracht, die Antwort von Denys Séguret zu verwenden, um einen Wrapper für mehrere zu entwickeln, exec.Commandbevor ich auf dieses nette Paket von b4b4r07 gestoßen bin .

eriel marimon
quelle
Ich habe gerade festgestellt, dass die Implementierung dieses Pakets mit der Antwort von @Tyndyll oben übereinstimmt. Nur zur
Kenntnis nehmen
Ich weiß nicht, vielleicht ist es für alle ganz offensichtlich, aber für mich war es nicht so offensichtlich, und ich habe auf die harte Tour gelernt, dass Sie das Ergebnis nicht erhalten, wenn Sie am Ende tatsächlich io.Copy () aufrufen, weil es so ist schon bei & b :)
toudi