Over a million developers have joined DZone.

Start an Interactive Shell from Within Go

· DevOps Zone

The DevOps Zone is brought to you in partnership with Sonatype Nexus. The Nexus Suite helps scale your DevOps delivery with continuous component intelligence integrated into development tools, including Eclipse, IntelliJ, Jenkins, Bamboo, SonarQube and more. Schedule a demo today

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.

The DevOps Zone is brought to you in partnership with Sonatype Nexus. Use the Nexus Suite to automate your software supply chain and ensure you're using the highest quality open source components at every step of the development lifecycle. Get Nexus today

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 best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

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

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

{{ parent.tldr }}

{{ parent.urlSource.name }}