Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

go

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

like image 748
Maxim Yefremov Avatar asked Jun 28 '15 02:06

Maxim Yefremov


3 Answers

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()
like image 106
Bayta Darell Avatar answered Oct 15 '22 21:10

Bayta Darell


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
}
like image 22
Travis Reeder Avatar answered Oct 15 '22 21:10

Travis Reeder


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

like image 1
Zombo Avatar answered Oct 15 '22 20:10

Zombo