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 with os.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 to login instead of hard-coding the path as I did.