Feed multiple input (stdin) to a command executed with exec.Command

Hello Gopher,

So here is my general goal :

Under linux, I have two terminals opened, A and B. I want to feed arbitrary shell command to terminal B whenever a specific event happened in terminal A.

My typical use case would be to change current working directory of B to match CWD of A whenever A changed its location.

I first tried to do it in pure bash, but the result was not great.

So I now try to do it in Go. My attempt to do so was not successful either.

To simplify the problem, I focused only on the program that should be running on terminal B.

My requirements for it are :

  • it should yield a fully interactive bash shell the user could use
  • it could accept arbitrary command coming from another sources, a goroutine for example

Unfortunately, my attempts never satisfied both of the requirements.

To get a fully interactive shell, I coud do :

	cmd := exec.Command("bash", "-i")
	cmd.Stdin = os.Stdin
	cmd.Stderr = os.Stderr
	cmd.Stdout = os.Stdout

then run cmd.Run() later

To write to stdin, in another goroutine I could do :

go func(){
		time.Sleep(1 * time.Second)
		io.WriteString(os.Stdin, "echo hello\n")

		time.Sleep(5 * time.Second)
		io.WriteString(os.Stdin, "echo hello again\n")
	}()

cmd.Stdin being equal to os.Stdin, somehow, it should work, right (?)
It does not, I get the hellos on screen but this is not interpreted by the executed bash command

So, what about using stdin,err := cmd.StdinPipe() instead of cmd.Stdin = os.Stdin
And then :

go func() {
		defer stdin.Close()
		time.Sleep(5 * time.Second)
		io.WriteString(stdin, "echo hello\n")
	}

Now I do not have interactive shell anymore, and it exits the shell after the echo command (that one work btw), probably due to an EOF being piped to the executed file, somehow.

I could avoid that EOF by not closing stdin, but then, I’m stuck with a non interactive shell expecting input from the pipe, which is not great.

I tried to feed os.Stdin back to the pipe with a scanner on os.Stdin. It did not work.

cmd.Stdin is an io.Reader, so why not create my custom io.Reader. I started simple :

type customReader struct{}

func (r customReader) Read(p []byte) (n int, err error) {
	return os.Stdin.Read(p)
}

Unfortunately, this didn’t work either. Somehow, after the first command (that work great), an EOF is sent, Not sure why.

My final thought was to create a file that receives both os.Stdin and my custom command into it, and then assign cmd.Stdin = myCustomFile
But I’m not sure how to do this.

So, to my questions :

  1. Is feeding both stdin and some arbitrary input to a command actually possible ?
  2. Should I take another approach to solve my initial problem ?

For reference, here is my code : https://play.golang.org/p/8xpz5GzLvL

PS : sorry for this long text

Yes, I actually tried to use mkfifo with my first attempts in bash. It gave the same kind of problem as go.
For example, in terminal B :

mkfifo mypipe
tail -f -n1 mypipe | bash -i

and in terminal A :

echo 'echo hello'

In that case, my command is correctly piped from A to B but I don’t have interactive bash (as, it is waiting some input from the pipe only)

I also tried redirection, with no better success

Seems to me a surprisingly difficult problem, unless I am just too ignorant of UNIX.

To change the CWD of a running shell from outside the shell, I think the shell itself has to have some side-channel. Bash does not have it, thus I am not sure you can achieve this with bash. You might write a shell in Go which also listens to RPC events, and use these to change the working directory.

Why do you need this? Is it just an exercise (and a cool one!) or you have a specific problem to solve? In tmux, when you open a new window, it starts from the CWD of the current window. That’s very handy.

Why do you need this? Is it just an exercise (and a cool one!) or you have a specific problem to solve? In tmux, when you open a new window, it starts from the CWD of the current window.

Well as I’m a beginner in Go, I thought it could be an interesting problem that would involve some concurrency and would force me to dig a little bit.
Plus it could be useful to me in my go setup. I’m working on i3, which is a tiling window manager (that’s why I’m not using tmux), and I have typically a few terminals open at the same time (one for vim, one to run my program/debbuging, another one to do the current task, for example). And I really don’t like to cd multiple time the same thing whenever I change CWD.
And also, it could be something very powerful if you’re able to execute an arbitrary command from one terminal to another, you could ls the new directory in one of your terminal and it would look somewhat like a file browser.
And that would be nice :slight_smile:

You might write a shell in Go which also listens to RPC events, and use these to change the working directory.

Indeed, I could do something like that : http://play.golang.org/p/eE_TJy7k4J

This is not great, because I don’t have a fully interactive shell and all the environnement variable.
But… In my rudimentary shell, I could type that :
bash -i

And then build on that like this : http://play.golang.org/p/AZ9HzMoiJb

This is, however, far from perfect :

  • Both my arbitrary command and my rudimentary shell share the same stdin (which is an issue if I want to feed command that need a user input).
  • the command I send does not know anything about the bash environnement, which might have weird side effect.

Here, I’m really executing two commands side-by-side, not remotely controlling a shell on demand.

Yep, you should lock writing and reading to stdin and stdout. They are locked already by Go, but on a coarse level, so that you might get interleaved lines between different programs and I suppose you don’t like that. You can also set env variables before executing.

If I understand correctly, your second example is closer to what you want to achieve. The background goroutine that sleeps and call commands should just be a rpc[1] listener to get commands from other shells. Other shells will send the rpc commands that interest you (like “cd”, which by the way is not an executable command.)

[1] https://golang.org/pkg/net/rpc/

@rot13 Thanks for the shells suggestion, I didn’t know them.

Yes, that’s an issue. I’m not sure how to lock stdin and stdout easily. If I could switch the value of cmd.Stdin/cmd.Stdout during execution, it would be doable, but I don’t think this is possible.
Or if there was some pause method in os/exec.
I guess I could use some sort of signaling directly to the involve processes to tell them to stop temporarily execution (?)

It looks indeed like what I need. Didn’t know about rpc, thanks.

Yes, that’s another big issue. I can’t use built-in bash (or any other shell) command.
That means that I need to implement every built-in command that I want in my program.
Hum…
(For a simple cd, it should not be super hard, but if I want this to be more complete, well… :disappointed_relieved:)

  • it should yield a fully interactive bash shell the user could use
    Start “bash”, “-i”, as before
  • it could accept arbitrary command coming from another sources, a goroutine for example
    Read from the arbitrary source and os.Stdin: have two goroutines, each reading its source, and writing the cmd.StdinPipe.
    If you want other sources/destinations, you’ll have to start another “go io.Copy(w, r)” to handle the byte moving.
    Use CloseWithError for pipes to close both ends of the pipe on error.

Thanks and sorry for the late reply.

So I tried to do the simplest case using bash -i as my base cmd then read in another goroutine from os.Stdin and writing to cmd.StdinPipe : http://play.golang.org/p/8JHUjqQOO-
But the result is not as expected. For me, somehow, the interactive bash is sent to the background immediately after my first input. I can fg if I want but then, soon after, the program exits and my terminal closes, probably due to some EOF.

Is the pasted code what you actually have? You are ignoring the error in write to stdin…

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.