Get output of os.exec to to show progress executed command

Viewed 543

I have a function like below to execute a command and show output to stdout (terminal):

package main

// https://blog.kowalczyk.info/article/wOYk/advanced-command-execution-in-go-with-osexec.html

import (
    "bytes"
    "io"
    "fmt"
    "log"
    "os"
    "os/exec"
)

func main() {
    cmd := exec.Command("./test.sh")

    var stdoutBuf, stderrBuf bytes.Buffer
    cmd.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf)
    cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf)

    err := cmd.Run()
    if err != nil {
        log.Fatalf("cmd.Run() failed with %s\n", err)
    }
    outStr, errStr := string(stdoutBuf.Bytes()), string(stderrBuf.Bytes())
    fmt.Printf("\nout:\n%s\nerr:\n%s\n", outStr, errStr)
}

And this is the bash script to test:

#!/bin/bash

declare -a arr=("frontend" "backend" "middleware" "apigateway")
for service in "${arr[@]}"
do
  sleep 1
  echo $service-prod-secret.yaml
done

For now, I only have the output when the command run print out to the os.Stdout (terminal), but I want to get this output line by line and push to some where (EX: socket) to show progress of executed command, because I have a command take long time to finish, it 's hard to only see the white screen and have no idea about the progress of the command.

Output:

frontend-prod-secret.yaml
backend-prod-secret.yaml
middleware-prod-secret.yaml
apigateway-prod-secret.yaml

out:
frontend-prod-secret.yaml
backend-prod-secret.yaml
middleware-prod-secret.yaml
apigateway-prod-secret.yaml

I need to wait the command to finish to get the output, but this take long time.

How can I reference the output line by line (in real time) to push to some where I want?

2 Answers

You should listen on the stdout and stderr buffers, and print them as they get new data.

You can use Scanner to scan the buffers:

package main

import (
    "bufio"
    "fmt"
    "io"
    "os/exec"
)

func main() {
    cmd := exec.Command("test.sh")
    
    stdout, _ := cmd.StdoutPipe()
    stderr, _ := cmd.StderrPipe()
    _ = cmd.Start()

    scanner := bufio.NewScanner(io.MultiReader(stdout, stderr))
    scanner.Split(bufio.ScanWords)
    for scanner.Scan() {
        m := scanner.Text()
        fmt.Println(m)
    }
    _ = cmd.Wait()
}

Prints the data as it appears in the buffers (so there is a sleep between lines)

frontend-prod-secret.yaml
backend-prod-secret.yaml
middleware-prod-secret.yaml
apigateway-prod-secret.yaml

os.StdOut is looking for an Interface which has Write method, when writing its output it will call this method.

I want to get this output line by line and push to some where (EX: socket)

let me give an example

import (
    "flag"
    "github.com/gorilla/websocket"
    "net/url"
)

var addr = flag.String("addr", "localhost:12345", "http service address")

// this function allows transmitting of output
// and command as chat message strings
type CommandChat struct {
    Conn *websocket.Conn
}

func ChatConnect() (c *CommandChat, err error) {
    u := url.URL{Scheme: "ws", Host: *addr, Path: "/echo"}
    conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
    c = &CommandChat{Conn: conn}
    return
}

func (c *CommandChat) Write(input []byte) (n int, err error) {
    err = c.Conn.WriteMessage(websocket.TextMessage, input)
    return
}
func (c *CommandChat) Rx(resp []byte) (err error) {
    return
}
func (c *CommandChat) Close() (err error) { return }

now you can assign CommandChat to os.StdOut

chat, err := ChatConnect()
cmd.Stdout = chat

reference

Related