Search code examples
gopipeoutputstdoutgoroutine

How to capture stdout output but also show progress


I have a function named print() that print numbers every 2 seconds, this function runs in a goroutine.

I need to pass its stdout printing to a variable and print it, but not one time, until it finish.
I need to have a some scanner in an infinite loop that will scan for the stdout and print it, once the function done the scanner will done too.

I tried to use this answer but it doesn't print anything.
This is what I tried to do:

package main

import (
    "bufio"
    "fmt"
    "os"
    "sync"
    "time"
)


func print() {

    for i := 0; i < 50; i++ {
        time.Sleep(2 * time.Second)
        fmt.Printf("hello number: %d\n", i)
    }
}

func main() {
    old := os.Stdout // keep backup of the real stdout

    defer func() { os.Stdout = old }()
    r, w, _ := os.Pipe()
    os.Stdout = w

    go print()


    var wg sync.WaitGroup

    c := make(chan struct{})
    wg.Add(1)


    defer wg.Done()
    for {
        <-c
        scanner := bufio.NewScanner(r)
        for scanner.Scan() {
            m := scanner.Text()
            fmt.Println("output: " + m)
        }

    }

    c <- struct{}{}

    wg.Wait()
    fmt.Println("DONE")

}  

I also tried to use io.Copy to read the buffer like that but it didn't work too:

package main

import (
    "bytes"
    "fmt"
    "io"
    "os"
    "time"
)


func print() {

    for i := 0; i < 50; i++ {
        time.Sleep(2 * time.Second)
        fmt.Printf("hello number: %d\n", i)
    }
}

// https://blog.kowalczyk.info/article/wOYk/advanced-command-execution-in-go-with-osexec.html
func main() {
    old := os.Stdout // keep backup of the real stdout

    defer func() { os.Stdout = old }()
    r, w, _ := os.Pipe()
    os.Stdout = w

    go print()

    fmt.Println("DONE 1")
    outC := make(chan string)

    for {

        var buf bytes.Buffer
        io.Copy(&buf, r)
        outC <- buf.String()

        out := <-outC
        fmt.Println("output: " + out)
    }

    // back to normal state
    w.Close()


    fmt.Println("DONE")

}

Solution

  • It is possible to run print() as a "blackbox" and capture its output though it is a little bit tricky and does not work on Go Playground.

    package main
    
    import (
        "bufio"
        "fmt"
        "os"
        "runtime"
        "time"
    )
    
    
    func print() {
        for i := 0; i < 50; i++ {
            time.Sleep(100 * time.Millisecond)
            fmt.Printf("hello number: %d\n", i)
        }
    }
    
    func main() {
    
        var ttyName string
        if runtime.GOOS == "windows" {
        fmt.Println("*** Using `con`")
            ttyName = "con"
        } else {
        fmt.Println("*** Using `/dev/tty`")
            ttyName = "/dev/tty"
        }   
    
        f, err := os.OpenFile(ttyName, os.O_WRONLY, 0644)
        if err != nil {
            panic(err)
        }
        defer f.Close()
    
        r, w, _ := os.Pipe()
        oldStdout := os.Stdout
        os.Stdout = w
        defer func() { 
            os.Stdout = oldStdout
            fmt.Println("*** DONE")
        }()
    
        fmt.Fprintln(f, "*** Stdout redirected")
    
        go func(){
           print()
           w.Close()
           r.Close()          
        }()
    
        c := make(chan struct{})
        go func(){c <- struct{}{}}()
        defer close(c)
    
        <-c
        scanner := bufio.NewScanner(r)
        for scanner.Scan() {
            m := scanner.Text()
            fmt.Fprintln(f, "output: " + m)
        }
    }