return cmd stdout and stderr as string instead of printing to console in golang

10,165

Solution 1

Set the output to a strings.Builder (in Go versions 1.10 or later) or a bytes.Buffer

var outbuf, errbuf strings.Builder // or bytes.Buffer
cmd.Stdout = &outbuf
cmd.Stderr = &errbuf

After running the command, you can get the stdout and stderr as a string by calling the Builder.String() method:

stdout := outbuf.String()
stderr := errbuf.String()

Solution 2

You can simplify this quite a bit by using cmd.Run() instead of cmd.Start() to have it automatically wait for it to be finish, and use exec.CommandContext() to have it timeout. This will also output in the correct order, whereas the original program is out of order due to go routines.

Here's the exact same program simplified and using @Mello Marmot's answer:

package main
import (
    "bytes"
    "fmt"
    "log"
    "os"
    "os/exec"
    "time"
    "golang.org/x/net/context"
)
func main() {
    ctx := context.Background()
    ok, outString, errString := runBashCommandAndKillIfTooSlow(ctx, "ls -la", 2000*time.Millisecond)
    fmt.Println("ok")
    fmt.Println(ok)
    fmt.Println("outString")
    fmt.Println(outString)
    fmt.Println("errString")
    fmt.Println(errString)
}
/*
run bash command and kill it if it works longer than "killIn"
*/
func runBashCommandAndKillIfTooSlow(ctx context.Context, command string, killIn time.Duration) (okResult bool, stdout, stderr string) {
    fmt.Println("running bash command...")
    fmt.Println(command)
    ctx, _ = context.WithTimeout(ctx, killIn)
    cmd := exec.CommandContext(ctx, "sh", "-c", command)
    // Set output to Byte Buffers
    var outb, errb bytes.Buffer
    cmd.Stdout = &outb
    cmd.Stderr = &errb
    okResult = true
    err := cmd.Run()
    stdout = outb.String()
    stderr = errb.String()
    if err != nil {
        log.Fatal(err)
        okResult = false
    }
    return
}

Solution 3

Another option is strings.Builder:

package main
import (
   "os/exec"
   "strings"
)
func main() {
   b := new(strings.Builder)
   c := exec.Command("go", "version")
   c.Stdout = b
   c.Run()
   println(b.String() == "go version go1.16.3 windows/amd64\n")
}

https://golang.org/pkg/strings#Builder

Share:
10,165

Related videos on Youtube

Maxim Yefremov
Author by

Maxim Yefremov

Updated on July 31, 2022

Comments

  • Maxim Yefremov
    Maxim Yefremov 5 months

    I am executing bash commands from a golang application. Now the stdout and stderr go directly to console:

    cmd.Stdout = os.Stdout 
    cmd.Stderr = os.Stderr
    

    But I would like stdout and stderr to be returned as string variables from the runBashCommandAndKillIfTooSlow function without printing to the console immediately. How to implement this?

    The code:

    package main
    import (
        "fmt"
        "log"
        "os"
        "os/exec"
        "time"
    )
    func main() {
        ok, outString, errString := runBashCommandAndKillIfTooSlow("ls -la", 2000)
        fmt.Println("ok")
        fmt.Println(ok)
        fmt.Println("outString")
        fmt.Println(outString)
        fmt.Println("errString")
        fmt.Println(errString)
    }
    /*
    run bash command and kill it if it works longer than "killInMilliSeconds" milliseconds
    */
    func runBashCommandAndKillIfTooSlow(command string, killInMilliSeconds time.Duration) (okResult bool, stdout, stderr string) {
        fmt.Println("running bash command...")
        fmt.Println(command)
        cmd := exec.Command("sh", "-c", command)
        cmd.Stdout = os.Stdout // cmd.Stdout -> stdout
        cmd.Stderr = os.Stderr // cmd.Stderr -> stderr
        okResult = true
        err := cmd.Start()
        log.Printf("Waiting for command to finish...")
        done := make(chan error, 1)
        go func() {
            done <- cmd.Wait()
        }()
        select {
        case <-time.After(killInMilliSeconds * time.Millisecond):
            if err := cmd.Process.Kill(); err != nil {
                log.Fatal("failed to kill: ", err)
                okResult = false
            }
            <-done // allow goroutine to exit
            // log.Println("process killed")
        case err := <-done:
            if err != nil {
                log.Printf("process done with error = %v", err)
                okResult = false
            }
        }
        if err != nil {
            log.Fatal(err)
            okResult = false
        }
        return
    }
    

    By the way, the program should keep its ability to kill the bash command if it was too slow (killInMilliSeconds parameter).

Related