Start an Interactive Shell from Within Go
Jul 11 2014
Looking around the web for information on creating a new shell from Go, I kept finding the same answer: "You can't do it." Actually, you can do it, and it's not hard.
My goal was to write a Go program that did some processing, set up a particular environment, and then opened an interactive UNIX shell for the user. I wanted the shell to have the following characteristics:
- Act like the user's regular shell
- Have certain extra environment variables set
- Start in a particular directory
- Return control to the Go program when the user types
exit
All of this can be accomplished readily with just a few Go functions (all from the core os
package) and a little UNIX knowledge.
This technique should work for most UNIX flavors, including OSX (my dev platform) and Linux.
package main
package main
import (
"os"
"os/user"
"fmt"
)
func main() {
// Get the current user.
me, err := user.Current()
if err != nil {
panic(err)
}
// Get the current working directory.
cwd, err := os.Getwd()
if err != nil {
panic(err)
}
// Set an environment variable.
os.Setenv("SOME_VAR", "1")
// Transfer stdin, stdout, and stderr to the new process
// and also set target directory for the shell to start in.
pa := os.ProcAttr {
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
Dir: cwd,
}
// Start up a new shell.
// Note that we supply "login" twice.
// -fpl means "don't prompt for PW and pass through environment."
fmt.Print(">> Starting a new interactive shell")
proc, err := os.StartProcess("/usr/bin/login", []string{"login", "-fpl", me.Username}, &pa)
if err != nil {
panic(err)
}
// Wait until user exits the shell
state, err := proc.Wait()
if err != nil {
panic(err)
}
// Keep on keepin' on.
fmt.Printf("<< Exited shell: %s\n", state.String())
}
There are a few things to mention about the code above:
- I use
login
instead of explicitly setting the shell. I do this because that ensures that all the usually profiles and scripts are executed. It's fine to use the user's existing shell, too. You can get it withos.Getenv("SHELL")
. - You really shouldn't panic on every error. I did that for convenience.
proc.Wait()
(as the name implies) waits until the shell is done before continuing.- If we omit the
proc.Wait()
part, the Go process will quite... and also terminate the shell. There may be a way around this, but I don't know it. - Doing this sort of thing in a program that uses goroutines may cause... interesting... side... effects.
- You can also use
"os/exec".LookPath()
to lookup the path tologin
instead of hard-coding the path as I did.