Search code examples
goexecbackground-process

How to run a foreground or background shell command in Go


In Go, I need to be able to run a shell command from the user, and to only block for commands run in the foreground (not run with &), while retrieving the output of the command.

For example, in Bash we can have:

#!/bin/bash
while read -p 'enter a command: ' cmd; do
    /bin/sh -c "$cmd" >(sed 's/^/line of output: /')
done

The user can enter a foreground command like sleep 5; echo hi and it will block and the prompt won't reappear instantly, or they can enter a background command like { sleep 5; echo hi; } & and it won't block and the prompt will reappear instantly as the command runs in the background.

How would I recreate this in Go? Here is my attempt:

package main

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

func main() {
    input := bufio.NewScanner(os.Stdin)
    for {
        print("enter a command: ")
        input.Scan()
        cmd := exec.Command("/bin/sh", "-c", input.Text())
        stdout, err := cmd.StdoutPipe()
        if err != nil {
            panic(err)
        }
        go func() {
            output := bufio.NewScanner(stdout)
            for output.Scan() {
                println("line of output: " + output.Text())
            }
            if output.Err() != nil {
                panic(output.Err())
            }
        }()
        if err := cmd.Start(); err != nil {
            panic(err)
        }
        if err := cmd.Wait(); err != nil {
            panic(err)
        }
    }
}

Running sleep 5; echo hi blocks and works, but { sleep 5; echo hi; } & errors with:

panic: read |0: file already closed

For context, I'm porting voice control to Go, where the user can configure actions such as runtype, where their command is run through the shell and keys are simulated to type the output. The user can use a background command so the voice control continues along with the command, for example, they can have it launch a menu when they say "menu" and still be able to use the voice control while it's open with: menu: runtype printf %s\\n this that | dmenu &.


Solution

  • Setting stdout to a named pipe achieved the behavior.