Skip to content
This repository has been archived by the owner on Sep 26, 2021. It is now read-only.

shell selection for env #1033

Merged
merged 9 commits into from Apr 30, 2015
25 changes: 25 additions & 0 deletions commands/commands.go
@@ -1,6 +1,7 @@
package commands

import (
"errors"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -33,6 +34,10 @@ import (
"github.com/docker/machine/utils"
)

var (
ErrUnknownShell = errors.New("unknown shell")
)

type machineConfig struct {
machineName string
machineDir string
Expand Down Expand Up @@ -228,6 +233,10 @@ var Commands = []cli.Command{
Name: "swarm",
Usage: "Display the Swarm config instead of the Docker daemon",
},
cli.StringFlag{
Name: "shell",
Usage: "Force environment to be configured for specified shell",
},
cli.BoolFlag{
Name: "unset, u",
Usage: "Unset variables instead of setting them",
Expand Down Expand Up @@ -644,3 +653,19 @@ func getCertPathInfo(c *cli.Context) libmachine.CertPathInfo {
ClientKeyPath: clientKeyPath,
}
}

func detectShell() (string, error) {
// attempt to get the SHELL env var
shell := filepath.Base(os.Getenv("SHELL"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this wrapped in filepath.Base so that we get the value we expect in case SHELL contains something like /usr/bin/zsh? Just making sure I understand correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct -- this is the current behavior today.

// none detected; check for windows env
if shell == "." && os.Getenv("windir") != "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this check should be:

if runtime.GOOS == "windows"

although shell == "." && os.Getenv("windir") != "" implies the same thing, you should be calling filepath.Base after if windows b/c it's not used in windows case at all.

also don't rely on env['windir'] :) People can run docker.exe with unsetting the env. GOOS is safer to use.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

log.Printf("On Windows, please specify either cmd or powershell with the --shell flag.\n\n")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be nice to put cmd, powershell strings in single quotes for readability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point -- thx!

return "", ErrUnknownShell
}

if shell == "" {
return "", ErrUnknownShell
}

return shell, nil
}
120 changes: 105 additions & 15 deletions commands/env.go
Expand Up @@ -4,23 +4,81 @@ import (
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"text/template"

log "github.com/Sirupsen/logrus"

"github.com/codegangsta/cli"
"github.com/docker/machine/utils"
)

const (
envTmpl = `{{ .Prefix }}DOCKER_TLS_VERIFY{{ .Delimiter }}{{ .DockerTLSVerify }}{{ .Suffix }}{{ .Prefix }}DOCKER_HOST{{ .Delimiter }}{{ .DockerHost }}{{ .Suffix }}{{ .Prefix }}DOCKER_CERT_PATH{{ .Delimiter }}{{ .DockerCertPath }}{{ .Suffix }}{{ .UsageHint }}`
)

type ShellConfig struct {
Prefix string
Delimiter string
Suffix string
DockerCertPath string
DockerHost string
DockerTLSVerify string
UsageHint string
}

func cmdEnv(c *cli.Context) {
userShell := filepath.Base(os.Getenv("SHELL"))
userShell := c.String("shell")
if userShell == "" {
shell, err := detectShell()
if err != nil {
log.Fatal(err)
}
userShell = shell
}

t := template.New("envConfig")

usageHint := generateUsageHint(c.App.Name, c.Args().First(), userShell)

shellCfg := ShellConfig{
DockerCertPath: "",
DockerHost: "",
DockerTLSVerify: "",
}

// unset vars
if c.Bool("unset") {
switch userShell {
case "fish":
fmt.Printf("set -e DOCKER_TLS_VERIFY;\nset -e DOCKER_CERT_PATH;\nset -e DOCKER_HOST;\n")
shellCfg.Prefix = "set -e "
shellCfg.Delimiter = ""
shellCfg.Suffix = ";\n"
case "powershell":
shellCfg.Prefix = "Remove-Item Env:\\\\"
shellCfg.Delimiter = ""
shellCfg.Suffix = "\n"
case "cmd":
// since there is no way to unset vars in cmd just reset to empty
shellCfg.DockerCertPath = ""
shellCfg.DockerHost = ""
shellCfg.DockerTLSVerify = ""
shellCfg.Prefix = "set "
shellCfg.Delimiter = "="
shellCfg.Suffix = "\n"
default:
fmt.Println("unset DOCKER_TLS_VERIFY DOCKER_CERT_PATH DOCKER_HOST")
shellCfg.Prefix = "unset "
shellCfg.Delimiter = " "
shellCfg.Suffix = "\n"
}

tmpl, err := t.Parse(envTmpl)
if err != nil {
log.Fatal(err)
}

if err := tmpl.Execute(os.Stdout, shellCfg); err != nil {
log.Fatal(err)
}
return
}
Expand All @@ -31,7 +89,7 @@ func cmdEnv(c *cli.Context) {
}

if cfg.machineUrl == "" {
log.Fatalf("%s is not running. Please start this with docker-machine start %s", cfg.machineName, cfg.machineName)
log.Fatalf("%s is not running. Please start this with %s start %s", cfg.machineName, c.App.Name, cfg.machineName)
}

dockerHost := cfg.machineUrl
Expand Down Expand Up @@ -83,32 +141,64 @@ func cmdEnv(c *cli.Context) {
}
}

usageHint := generateUsageHint(c.Args().First(), userShell)
shellCfg = ShellConfig{
DockerCertPath: cfg.machineDir,
DockerHost: dockerHost,
DockerTLSVerify: "1",
UsageHint: usageHint,
}

switch userShell {
case "fish":
fmt.Printf("set -x DOCKER_TLS_VERIFY 1;\nset -x DOCKER_CERT_PATH %q;\nset -x DOCKER_HOST %s;\n\n%s\n",
cfg.machineDir, dockerHost, usageHint)
shellCfg.Prefix = "set -x "
shellCfg.Suffix = "\";\n"
shellCfg.Delimiter = " \""
case "powershell":
shellCfg.Prefix = "$Env:"
shellCfg.Suffix = "\"\n"
shellCfg.Delimiter = " = \""
case "cmd":
shellCfg.Prefix = "set "
shellCfg.Suffix = "\n"
shellCfg.Delimiter = "="
default:
fmt.Printf("export DOCKER_TLS_VERIFY=1\nexport DOCKER_CERT_PATH=%q\nexport DOCKER_HOST=%s\n\n%s\n",
cfg.machineDir, dockerHost, usageHint)
shellCfg.Prefix = "export "
shellCfg.Suffix = "\"\n"
shellCfg.Delimiter = "=\""
}

tmpl, err := t.Parse(envTmpl)
if err != nil {
log.Fatal(err)
}

if err := tmpl.Execute(os.Stdout, shellCfg); err != nil {
log.Fatal(err)
}
}

func generateUsageHint(machineName string, userShell string) string {
func generateUsageHint(appName, machineName, userShell string) string {
cmd := ""
switch userShell {
case "fish":
if machineName != "" {
cmd = fmt.Sprintf("eval (docker-machine env %s)", machineName)
cmd = fmt.Sprintf("eval (%s env %s)", appName, machineName)
} else {
cmd = fmt.Sprintf("eval (%s env)", appName)
}
case "powershell":
if machineName != "" {
cmd = fmt.Sprintf("%s env --shell=powershell %s | Invoke-Expression", appName, machineName)
} else {
cmd = "eval (docker-machine env)"
cmd = fmt.Sprintf("%s env --shell=powershell | Invoke-Expression", appName)
}
case "cmd":
cmd = "copy and paste the above values into your command prompt"
default:
if machineName != "" {
cmd = fmt.Sprintf("eval \"$(docker-machine env %s)\"", machineName)
cmd = fmt.Sprintf("eval \"$(%s env %s)\"", appName, machineName)
} else {
cmd = "eval \"$(docker-machine env)\""
cmd = fmt.Sprintf("eval \"$(%s env)\"", appName)
}
}

Expand Down
116 changes: 112 additions & 4 deletions commands/env_test.go
Expand Up @@ -83,6 +83,9 @@ func TestCmdEnvBash(t *testing.T) {

set := flag.NewFlagSet("config", 0)
c := cli.NewContext(nil, set, set)
c.App = &cli.App{
Name: "docker-machine-test",
}
cmdEnv(c)

w.Close()
Expand All @@ -103,9 +106,9 @@ func TestCmdEnvBash(t *testing.T) {
testMachineDir := filepath.Join(store.GetPath(), "machines", host.Name)

expected := map[string]string{
"DOCKER_TLS_VERIFY": "1",
"DOCKER_TLS_VERIFY": "\"1\"",
"DOCKER_CERT_PATH": fmt.Sprintf("\"%s\"", testMachineDir),
"DOCKER_HOST": "unix:///var/run/docker.sock",
"DOCKER_HOST": "\"unix:///var/run/docker.sock\"",
}

for k, v := range envvars {
Expand Down Expand Up @@ -181,6 +184,9 @@ func TestCmdEnvFish(t *testing.T) {

set := flag.NewFlagSet("config", 0)
c := cli.NewContext(nil, set, set)
c.App = &cli.App{
Name: "docker-machine-test",
}
cmdEnv(c)

w.Close()
Expand All @@ -201,9 +207,111 @@ func TestCmdEnvFish(t *testing.T) {
testMachineDir := filepath.Join(store.GetPath(), "machines", host.Name)

expected := map[string]string{
"DOCKER_TLS_VERIFY": "1",
"DOCKER_TLS_VERIFY": "\"1\"",
"DOCKER_CERT_PATH": fmt.Sprintf("\"%s\"", testMachineDir),
"DOCKER_HOST": "\"unix:///var/run/docker.sock\"",
}

for k, v := range envvars {
if v != expected[k] {
t.Fatalf("Expected %s == <%s>, but was <%s>", k, expected[k], v)
}
}
}

func TestCmdEnvPowerShell(t *testing.T) {
stdout := os.Stdout
shell := os.Getenv("SHELL")
r, w, _ := os.Pipe()

os.Stdout = w
os.Setenv("MACHINE_STORAGE_PATH", TestStoreDir)
os.Setenv("SHELL", "")

defer func() {
os.Setenv("MACHINE_STORAGE_PATH", "")
os.Setenv("SHELL", shell)
os.Stdout = stdout
}()

if err := clearHosts(); err != nil {
t.Fatal(err)
}

flags := getTestDriverFlags()

store, sErr := getTestStore()
if sErr != nil {
t.Fatal(sErr)
}

mcn, err := libmachine.New(store)
if err != nil {
t.Fatal(err)
}

hostOptions := &libmachine.HostOptions{
EngineOptions: &engine.EngineOptions{},
SwarmOptions: &swarm.SwarmOptions{
Master: false,
Discovery: "",
Address: "",
Host: "",
},
AuthOptions: &auth.AuthOptions{},
}

host, err := mcn.Create("test-a", "none", hostOptions, flags)
if err != nil {
t.Fatal(err)
}

host, err = mcn.Get("test-a")
if err != nil {
t.Fatalf("error loading host: %v", err)
}

if err := mcn.SetActive(host); err != nil {
t.Fatalf("error setting active host: %v", err)
}

outStr := make(chan string)

go func() {
var testOutput bytes.Buffer
io.Copy(&testOutput, r)
outStr <- testOutput.String()
}()

set := flag.NewFlagSet("config", 0)
set.String("shell", "powershell", "")
c := cli.NewContext(nil, set, set)
c.App = &cli.App{
Name: "docker-machine-test",
}
cmdEnv(c)

w.Close()

out := <-outStr

// parse the output into a map of envvar:value for easier testing below
envvars := make(map[string]string)
for _, e := range strings.Split(strings.TrimSpace(out), "\n") {
if !strings.HasPrefix(e, "$Env") {
continue
}
kv := strings.SplitN(e, " = ", 2)
key, value := kv[0], kv[1]
envvars[strings.Replace(key, "$Env:", "", 1)] = value
}

testMachineDir := filepath.Join(store.GetPath(), "machines", host.Name)

expected := map[string]string{
"DOCKER_TLS_VERIFY": "\"1\"",
"DOCKER_CERT_PATH": fmt.Sprintf("\"%s\"", testMachineDir),
"DOCKER_HOST": "unix:///var/run/docker.sock",
"DOCKER_HOST": "\"unix:///var/run/docker.sock\"",
}

for k, v := range envvars {
Expand Down