Skip to content

Commit e73f489

Browse files
committed
os/exec: remove duplicate environment variables in Cmd.Start
Nobody intends to have duplicates anyway because it's so undefined and everything handles it so poorly. Removing duplicates automatically simplifies code and makes existing code do what people already expect. Fixes #12868 Change-Id: I95eeba8c59ff94d0f018012a6f4e031aaabfd5d9 Reviewed-on: https://go-review.googlesource.com/37586 Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]>
1 parent 3c023f7 commit e73f489

File tree

4 files changed

+109
-2
lines changed

4 files changed

+109
-2
lines changed

src/os/exec/env_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2017 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package exec
6+
7+
import (
8+
"reflect"
9+
"testing"
10+
)
11+
12+
func TestDedupEnv(t *testing.T) {
13+
tests := []struct {
14+
noCase bool
15+
in []string
16+
want []string
17+
}{
18+
{
19+
noCase: true,
20+
in: []string{"k1=v1", "k2=v2", "K1=v3"},
21+
want: []string{"K1=v3", "k2=v2"},
22+
},
23+
{
24+
noCase: false,
25+
in: []string{"k1=v1", "K1=V2", "k1=v3"},
26+
want: []string{"k1=v3", "K1=V2"},
27+
},
28+
{
29+
in: []string{"=a", "=b", "foo", "bar"},
30+
want: []string{"=b", "foo", "bar"},
31+
},
32+
}
33+
for _, tt := range tests {
34+
got := dedupEnvCase(tt.noCase, tt.in)
35+
if !reflect.DeepEqual(got, tt.want) {
36+
t.Errorf("Dedup(%v, %q) = %q; want %q", tt.noCase, tt.in, got, tt.want)
37+
}
38+
}
39+
}

src/os/exec/example_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"io"
1313
"io/ioutil"
1414
"log"
15+
"os"
1516
"os/exec"
1617
"strings"
1718
"time"
@@ -37,6 +38,17 @@ func ExampleCommand() {
3738
fmt.Printf("in all caps: %q\n", out.String())
3839
}
3940

41+
func ExampleCommand_environment() {
42+
cmd := exec.Command("prog")
43+
cmd.Env = append(os.Environ(),
44+
"FOO=duplicate_value", // ignored
45+
"FOO=actual_value", // this value is used
46+
)
47+
if err := cmd.Run(); err != nil {
48+
log.Fatal(err)
49+
}
50+
}
51+
4052
func ExampleCmd_Output() {
4153
out, err := exec.Command("date").Output()
4254
if err != nil {

src/os/exec/exec.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ type Cmd struct {
5555
Args []string
5656

5757
// Env specifies the environment of the process.
58-
// If Env is nil, Run uses the current process's environment.
58+
// Each entry is of the form "key=value".
59+
// If Env is nil, the new process uses the current process's
60+
// environment.
61+
// If Env contains duplicate environment keys, only the last
62+
// value in the slice for each duplicate key is used.
5963
Env []string
6064

6165
// Dir specifies the working directory of the command.
@@ -354,7 +358,7 @@ func (c *Cmd) Start() error {
354358
c.Process, err = os.StartProcess(c.Path, c.argv(), &os.ProcAttr{
355359
Dir: c.Dir,
356360
Files: c.childFiles,
357-
Env: c.envv(),
361+
Env: dedupEnv(c.envv()),
358362
Sys: c.SysProcAttr,
359363
})
360364
if err != nil {
@@ -712,3 +716,35 @@ func minInt(a, b int) int {
712716
}
713717
return b
714718
}
719+
720+
// dedupEnv returns a copy of env with any duplicates removed, in favor of
721+
// later values.
722+
// Items not of the normal environment "key=value" form are preserved unchanged.
723+
func dedupEnv(env []string) []string {
724+
return dedupEnvCase(runtime.GOOS == "windows", env)
725+
}
726+
727+
// dedupEnvCase is dedupEnv with a case option for testing.
728+
// If caseInsensitive is true, the case of keys is ignored.
729+
func dedupEnvCase(caseInsensitive bool, env []string) []string {
730+
out := make([]string, 0, len(env))
731+
saw := map[string]int{} // key => index into out
732+
for _, kv := range env {
733+
eq := strings.Index(kv, "=")
734+
if eq < 0 {
735+
out = append(out, kv)
736+
continue
737+
}
738+
k := kv[:eq]
739+
if caseInsensitive {
740+
k = strings.ToLower(k)
741+
}
742+
if dupIdx, isDup := saw[k]; isDup {
743+
out[dupIdx] = kv
744+
continue
745+
}
746+
saw[k] = len(out)
747+
out = append(out, kv)
748+
}
749+
return out
750+
}

src/os/exec/exec_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,11 @@ func TestHelperProcess(*testing.T) {
693693
iargs = append(iargs, s)
694694
}
695695
fmt.Println(iargs...)
696+
case "echoenv":
697+
for _, s := range args {
698+
fmt.Println(os.Getenv(s))
699+
}
700+
os.Exit(0)
696701
case "cat":
697702
if len(args) == 0 {
698703
io.Copy(os.Stdout, os.Stdin)
@@ -1043,3 +1048,18 @@ func TestContextCancel(t *testing.T) {
10431048
t.Logf("exit status: %v", err)
10441049
}
10451050
}
1051+
1052+
// test that environment variables are de-duped.
1053+
func TestDedupEnvEcho(t *testing.T) {
1054+
testenv.MustHaveExec(t)
1055+
1056+
cmd := helperCommand(t, "echoenv", "FOO")
1057+
cmd.Env = append(cmd.Env, "FOO=bad", "FOO=good")
1058+
out, err := cmd.CombinedOutput()
1059+
if err != nil {
1060+
t.Fatal(err)
1061+
}
1062+
if got, want := strings.TrimSpace(string(out)), "good"; got != want {
1063+
t.Errorf("output = %q; want %q", got, want)
1064+
}
1065+
}

0 commit comments

Comments
 (0)