If I want to use Golang's os/exec functionality to run bash
in a subcommand, I can do this:
package main
import (
"os"
"os/exec"
)
func main() {
subcommand := exec.Command("bash")
subcommand.Stderr = os.Stderr
subcommand.Stdout = os.Stdout
subcommand.Stdin = os.Stdin
err := subcommand.Run()
if err != nil {
panic(err)
}
}
This works perfectly. My test case is:
- Type
echo Hello
and press enter - Press the up arrow on my keyboard and press enter
This results in:
myname@mycomputer:~/Desktop/thing$ echo Hello
Hello
myname@mycomputer:~/Desktop/thing$ echo Hello
Hello
However, my specific use case involves multiple sources writing to the subcommand's stdin. This requires a more complicated input than just connecting the subcommand.Stdin
to os.Stdin
; my research led me to io.MultiReader
, which seems to meet my needs. The first step in accomplishing my use case would seem to be using a stream for the subcommand's stdin, like this:
package main
import (
"io"
"os"
"os/exec"
)
func main() {
subcommand := exec.Command("bash")
subcommand.Stderr = os.Stderr
subcommand.Stdout = os.Stdout
pipeWrite, err := subcommand.StdinPipe()
if err != nil {
panic(err)
}
go func() {
// Copy os.Stdin to the subcommand's stdin pipe.
// Because this is a blocking call, I've put it in a goroutine.
_, err = io.Copy(pipeWrite, os.Stdin)
if err != nil {
panic(err)
}
}()
err = subcommand.Run()
if err != nil {
panic(err)
}
}
Running the same test case above results in this:
echo Hello
Hello
^[[A
bash: line 2: $'\E[A': command not found
As you can see, there is no prompt this time, and pressing the up arrow results in the escape sequence for the up arrow (I'm not sure if "escape sequence" is correct terminology) instead of going back to my previous command.
Clearly I'm doing something wrong here, but I don't know what it could be. I've tested a lot of things here, but including them all would make this question far too long. Some of the most important thoughts/ideas I've had are:
- The documentation mentions different handling for io.Reader versus a file, but that shouldn't matter here, since the pipe is still a file
- I tried looking into whether there was some way the subcommand decides whether it is interactive, like how
docker exec
has-it
flags to specify that, but I couldn't find an equivalent - I tried creating my own pipe with
os.Pipe
instead of usingsubcommand.StdinPipe
, but there were no differences - I tried taking a step back and using
io.Copy
from a separate stream and copying it toos.Stdin
(since that's my required use case), but that just resulted in the separate stream's contents printing to the terminal but not being detected by the subprocess for some reason - I wondered whether
os.Stdin
having a file descriptor of 0 could be important, but I couldn't think of a good way to replicate that behavior in a way that wouldn't require me to then useio.Copy
to copyos.Stdin
to that new stdin-ish file, which as mentioned in the previous bullet didn't work - It could be due to
os.Stdin
being a character special file (ModeDevice
andModeCharDevice
), whilepipeWrite
is just a named pipe file (ModeNamedPipe
), but trying to fix that has the same problem as the previous bullet
I am running Ubuntu 21.10
bash
—as most such programs do—checks at startup whether it is connected to a terminal (or a terminal emulator, which the X Window application in which you're executingbash
, is) and if so, enables its interactive features. In order to communicate with such applications in their interactive mode, you have to use a PTY. There exist 3rd-party packages for Go which implement such support. – kostix