Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Start an Interactive Shell from Within Go

DZone's Guide to

Start an Interactive Shell from Within Go

· DevOps Zone
Free Resource

Download “The DevOps Journey - From Waterfall to Continuous Delivery” to learn about the importance of integrating automated testing into the DevOps workflow, brought to you in partnership with Sauce Labs.

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.

Discover how to optimize your DevOps workflows with our cloud-based automated testing infrastructure, brought to you in partnership with Sauce Labs

Topics:

Published at DZone with permission of Matt Butcher, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}