lib: Add support for cmdline.Env.Vars

This adds envrionment variable support to cmdline.Env, and also
updates some programs to use this support.  The main benefit of
using cmdline.Env.Vars is that it makes testing easier, since you
can just pass in your own map.

We represent environment variables as map[string]string, which is
a nice simple form for lookups and mutations.  Originally I was
planning on using *envvar.Vars, but the delta-tracking support is
probably unnecessary, and the native Go map syntax is nicer.

A subsequent change will update the v23.Init and test.InitForTest
mechanisms to support explicit passing of args and envvars, and
will take advantage of this support.

MultiPart: 1/2
Change-Id: I6044cf72a54254d9abd585908a1b712847b97913
diff --git a/cmdline/.api b/cmdline/.api
index 2e4b2ae..9067aa0 100644
--- a/cmdline/.api
+++ b/cmdline/.api
@@ -23,6 +23,7 @@
 pkg cmdline, type Env struct, Stdin io.Reader
 pkg cmdline, type Env struct, Stdout io.Writer
 pkg cmdline, type Env struct, Usage func(io.Writer)
+pkg cmdline, type Env struct, Vars map[string]string
 pkg cmdline, type ErrExitCode int
 pkg cmdline, type Runner interface { Run }
 pkg cmdline, type Runner interface, Run(*Env, []string) error
diff --git a/cmdline/cmdline_test.go b/cmdline/cmdline_test.go
index d367a96..ecdf043 100644
--- a/cmdline/cmdline_test.go
+++ b/cmdline/cmdline_test.go
@@ -9,15 +9,12 @@
 	"errors"
 	"flag"
 	"fmt"
-	"os"
 	"regexp"
 	"strings"
 	"testing"
-)
 
-func init() {
-	os.Setenv("CMDLINE_WIDTH", "80") // make sure the formatting stays the same.
-}
+	"v.io/x/lib/envvar"
+)
 
 var (
 	errEchoStr        = "echo error"
@@ -62,7 +59,7 @@
 
 type testCase struct {
 	Args        []string
-	Envs        map[string]string
+	Vars        map[string]string
 	Err         string
 	Stdout      string
 	Stderr      string
@@ -84,22 +81,18 @@
 	return fmt.Sprint(err)
 }
 
+var baseVars = map[string]string{
+	"CMDLINE_WIDTH": "80", // make sure formatting stays the same.
+}
+
 func runTestCases(t *testing.T, cmd *Command, tests []testCase) {
 	for _, test := range tests {
 		// Reset global variables before running each test case.
 		var stdout, stderr bytes.Buffer
 		flagExtra, flagTopLevelExtra, optNoNewline = false, false, false
-		origEnvs := make(map[string]string)
 
-		// Set a fresh flag.CommandLine and fresh envvars for each run.
-		flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
-		for k, v := range test.Envs {
-			origEnvs[k] = os.Getenv(k)
-			if err := os.Setenv(k, v); err != nil {
-				t.Fatalf("os.Setenv(%v, %v) failed: %v", k, v, err)
-			}
-		}
-
+		// Start with a fresh flag.CommandLine for each run.
+		flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
 		var globalFlag1 string
 		flag.StringVar(&globalFlag1, "global1", "", "global test flag 1")
 		globalFlag2 := flag.Int64("global2", 0, "global test flag 2")
@@ -109,20 +102,24 @@
 
 		// Parse and run the command and check against expected results.
 		parseOK := false
-		env := &Env{Stdout: &stdout, Stderr: &stderr}
+		env := &Env{
+			Stdout: &stdout,
+			Stderr: &stderr,
+			Vars:   envvar.MergeMaps(baseVars, test.Vars),
+		}
 		runner, args, err := Parse(cmd, env, test.Args)
 		if err == nil {
 			err = runner.Run(env, args)
 			parseOK = true
 		}
 		if got, want := errString(err), test.Err; got != want {
-			t.Errorf("Ran with args %q envs %q\n GOT error:\n%q\nWANT error:\n%q", test.Args, test.Envs, got, want)
+			t.Errorf("Ran with args %q vars %q\n GOT error:\n%q\nWANT error:\n%q", test.Args, test.Vars, got, want)
 		}
 		if got, want := stripTestFlags(stdout.String()), test.Stdout; got != want {
-			t.Errorf("Ran with args %q envs %q\n GOT stdout:\n%q\nWANT stdout:\n%q", test.Args, test.Envs, got, want)
+			t.Errorf("Ran with args %q vars %q\n GOT stdout:\n%q\nWANT stdout:\n%q", test.Args, test.Vars, got, want)
 		}
 		if got, want := stripTestFlags(stderr.String()), test.Stderr; got != want {
-			t.Errorf("Ran with args %q envs %q\n GOT stderr:\n%q\nWANT stderr:\n%q", test.Args, test.Envs, got, want)
+			t.Errorf("Ran with args %q vars %q\n GOT stderr:\n%q\nWANT stderr:\n%q", test.Args, test.Vars, got, want)
 		}
 		if got, want := globalFlag1, test.GlobalFlag1; got != want {
 			t.Errorf("global1 flag got %q, want %q", got, want)
@@ -134,11 +131,6 @@
 		if parseOK && !flag.CommandLine.Parsed() {
 			t.Errorf("flag.CommandLine should be parsed by now")
 		}
-		for k, v := range origEnvs {
-			if err := os.Setenv(k, v); err != nil {
-				t.Fatalf("os.Setenv(%v, %v) failed: %v", k, v, err)
-			}
-		}
 	}
 }
 
@@ -2372,7 +2364,7 @@
 		},
 		{
 			Args: []string{"-help"},
-			Envs: map[string]string{"CMDLINE_STYLE": "full"},
+			Vars: map[string]string{"CMDLINE_STYLE": "full"},
 			Stdout: `Test hiding global flags, root no children.
 
 Usage:
diff --git a/cmdline/env.go b/cmdline/env.go
index cbe8e5d..df06fa5 100644
--- a/cmdline/env.go
+++ b/cmdline/env.go
@@ -10,12 +10,18 @@
 	"os"
 	"strconv"
 
+	"v.io/x/lib/envvar"
 	"v.io/x/lib/textutil"
 )
 
 // NewEnv returns a new environment with defaults based on the operating system.
 func NewEnv() *Env {
-	return &Env{Stdin: os.Stdin, Stdout: os.Stdout, Stderr: os.Stderr}
+	return &Env{
+		Stdin:  os.Stdin,
+		Stdout: os.Stdout,
+		Stderr: os.Stderr,
+		Vars:   envvar.SliceToMap(os.Environ()),
+	}
 }
 
 // Env represents the environment for command parsing and running.  Typically
@@ -25,8 +31,7 @@
 	Stdin  io.Reader
 	Stdout io.Writer
 	Stderr io.Writer
-	// TODO(toddw): Add env vars, using a new "v.io/x/lib/envvar" package.
-	// Vars envvar.Vars
+	Vars   map[string]string // Environment variables
 
 	// Usage is a function that prints usage information to w.  Typically set by
 	// calls to Main or Parse to print usage of the leaf command.
@@ -56,8 +61,7 @@
 const defaultWidth = 80
 
 func (e *Env) width() int {
-	// TODO(toddw): Replace os.Getenv with lookup in env.Vars.
-	if width, err := strconv.Atoi(os.Getenv("CMDLINE_WIDTH")); err == nil && width != 0 {
+	if width, err := strconv.Atoi(e.Vars["CMDLINE_WIDTH"]); err == nil && width != 0 {
 		return width
 	}
 	if _, width, err := textutil.TerminalSize(); err == nil && width != 0 {
@@ -67,9 +71,8 @@
 }
 
 func (e *Env) style() style {
-	// TODO(toddw): Replace os.Getenv with lookup in env.Vars.
 	style := styleCompact
-	style.Set(os.Getenv("CMDLINE_STYLE"))
+	style.Set(e.Vars["CMDLINE_STYLE"])
 	return style
 }
 
diff --git a/cmdline/env_test.go b/cmdline/env_test.go
index 963343a..86bc819 100644
--- a/cmdline/env_test.go
+++ b/cmdline/env_test.go
@@ -42,8 +42,8 @@
 
 func TestEnvWidth(t *testing.T) {
 	tests := []struct {
-		env  string
-		want int
+		value string
+		want  int
 	}{
 		{"123", 123},
 		{"-1", -1},
@@ -52,20 +52,25 @@
 		{"foobar", defaultWidth},
 	}
 	for _, test := range tests {
-		if err := os.Setenv("CMDLINE_WIDTH", test.env); err != nil {
-			t.Errorf("Setenv(%q) failed: %v", test.env, err)
-			continue
+		// Test using a fake environment.
+		env := &Env{Vars: map[string]string{"CMDLINE_WIDTH": test.value}}
+		if got, want := env.width(), test.want; got != want {
+			t.Errorf("%q got %v, want %v", test.value, got, want)
 		}
-		if got, want := NewEnv().width(), test.want; got != want {
-			t.Errorf("%q got %v, want %v", test.env, got, want)
+		// Test using the OS environment.
+		if err := os.Setenv("CMDLINE_WIDTH", test.value); err != nil {
+			t.Errorf("Setenv(%q) failed: %v", test.value, err)
+		} else if got, want := NewEnv().width(), test.want; got != want {
+			t.Errorf("%q got %v, want %v", test.value, got, want)
 		}
 	}
+	os.Unsetenv("CMDLINE_WIDTH")
 }
 
 func TestEnvStyle(t *testing.T) {
 	tests := []struct {
-		env  string
-		want style
+		value string
+		want  style
 	}{
 		{"compact", styleCompact},
 		{"full", styleFull},
@@ -75,12 +80,17 @@
 		{"foobar", styleCompact},
 	}
 	for _, test := range tests {
-		if err := os.Setenv("CMDLINE_STYLE", test.env); err != nil {
-			t.Errorf("Setenv(%q) failed: %v", test.env, err)
-			continue
+		// Test using a fake environment.
+		env := &Env{Vars: map[string]string{"CMDLINE_STYLE": test.value}}
+		if got, want := env.style(), test.want; got != want {
+			t.Errorf("%q got %v, want %v", test.value, got, want)
 		}
-		if got, want := NewEnv().style(), test.want; got != want {
-			t.Errorf("%q got %v, want %v", test.env, got, want)
+		// Test using the OS environment.
+		if err := os.Setenv("CMDLINE_STYLE", test.value); err != nil {
+			t.Errorf("Setenv(%q) failed: %v", test.value, err)
+		} else if got, want := NewEnv().style(), test.want; got != want {
+			t.Errorf("%q got %v, want %v", test.value, got, want)
 		}
 	}
+	os.Unsetenv("CMDLINE_STYLE")
 }
diff --git a/envvar/envvar_test.go b/envvar/envvar_test.go
index 74d8a02..d991184 100644
--- a/envvar/envvar_test.go
+++ b/envvar/envvar_test.go
@@ -424,7 +424,7 @@
 	if err := os.Setenv(testKey, testValue); err != nil {
 		t.Fatalf("Setenv(%q, %q) failed: %v", testKey, testValue, err)
 	}
-	defer os.Setenv(testKey, "")
+	defer os.Unsetenv(testKey)
 	vars := VarsFromOS()
 	if got, want := vars.Contains(testKey), true; got != want {
 		t.Errorf(`Contains(%q) got %v, want %v`, testKey, got, want)