summaryrefslogtreecommitdiffstats
path: root/src/cmd/go/internal/envcmd
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 19:19:13 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 19:19:13 +0000
commitccd992355df7192993c666236047820244914598 (patch)
treef00fea65147227b7743083c6148396f74cd66935 /src/cmd/go/internal/envcmd
parentInitial commit. (diff)
downloadgolang-1.21-ccd992355df7192993c666236047820244914598.tar.xz
golang-1.21-ccd992355df7192993c666236047820244914598.zip
Adding upstream version 1.21.8.upstream/1.21.8
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/cmd/go/internal/envcmd')
-rw-r--r--src/cmd/go/internal/envcmd/env.go691
-rw-r--r--src/cmd/go/internal/envcmd/env_test.go93
2 files changed, 784 insertions, 0 deletions
diff --git a/src/cmd/go/internal/envcmd/env.go b/src/cmd/go/internal/envcmd/env.go
new file mode 100644
index 0000000..c7c2e83
--- /dev/null
+++ b/src/cmd/go/internal/envcmd/env.go
@@ -0,0 +1,691 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package envcmd implements the “go env” command.
+package envcmd
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "go/build"
+ "internal/buildcfg"
+ "io"
+ "os"
+ "path/filepath"
+ "runtime"
+ "sort"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+
+ "cmd/go/internal/base"
+ "cmd/go/internal/cache"
+ "cmd/go/internal/cfg"
+ "cmd/go/internal/fsys"
+ "cmd/go/internal/load"
+ "cmd/go/internal/modload"
+ "cmd/go/internal/work"
+ "cmd/internal/quoted"
+)
+
+var CmdEnv = &base.Command{
+ UsageLine: "go env [-json] [-u] [-w] [var ...]",
+ Short: "print Go environment information",
+ Long: `
+Env prints Go environment information.
+
+By default env prints information as a shell script
+(on Windows, a batch file). If one or more variable
+names is given as arguments, env prints the value of
+each named variable on its own line.
+
+The -json flag prints the environment in JSON format
+instead of as a shell script.
+
+The -u flag requires one or more arguments and unsets
+the default setting for the named environment variables,
+if one has been set with 'go env -w'.
+
+The -w flag requires one or more arguments of the
+form NAME=VALUE and changes the default settings
+of the named environment variables to the given values.
+
+For more about environment variables, see 'go help environment'.
+ `,
+}
+
+func init() {
+ CmdEnv.Run = runEnv // break init cycle
+ base.AddChdirFlag(&CmdEnv.Flag)
+ base.AddBuildFlagsNX(&CmdEnv.Flag)
+}
+
+var (
+ envJson = CmdEnv.Flag.Bool("json", false, "")
+ envU = CmdEnv.Flag.Bool("u", false, "")
+ envW = CmdEnv.Flag.Bool("w", false, "")
+)
+
+func MkEnv() []cfg.EnvVar {
+ envFile, _ := cfg.EnvFile()
+ env := []cfg.EnvVar{
+ {Name: "GO111MODULE", Value: cfg.Getenv("GO111MODULE")},
+ {Name: "GOARCH", Value: cfg.Goarch},
+ {Name: "GOBIN", Value: cfg.GOBIN},
+ {Name: "GOCACHE", Value: cache.DefaultDir()},
+ {Name: "GOENV", Value: envFile},
+ {Name: "GOEXE", Value: cfg.ExeSuffix},
+
+ // List the raw value of GOEXPERIMENT, not the cleaned one.
+ // The set of default experiments may change from one release
+ // to the next, so a GOEXPERIMENT setting that is redundant
+ // with the current toolchain might actually be relevant with
+ // a different version (for example, when bisecting a regression).
+ {Name: "GOEXPERIMENT", Value: cfg.RawGOEXPERIMENT},
+
+ {Name: "GOFLAGS", Value: cfg.Getenv("GOFLAGS")},
+ {Name: "GOHOSTARCH", Value: runtime.GOARCH},
+ {Name: "GOHOSTOS", Value: runtime.GOOS},
+ {Name: "GOINSECURE", Value: cfg.GOINSECURE},
+ {Name: "GOMODCACHE", Value: cfg.GOMODCACHE},
+ {Name: "GONOPROXY", Value: cfg.GONOPROXY},
+ {Name: "GONOSUMDB", Value: cfg.GONOSUMDB},
+ {Name: "GOOS", Value: cfg.Goos},
+ {Name: "GOPATH", Value: cfg.BuildContext.GOPATH},
+ {Name: "GOPRIVATE", Value: cfg.GOPRIVATE},
+ {Name: "GOPROXY", Value: cfg.GOPROXY},
+ {Name: "GOROOT", Value: cfg.GOROOT},
+ {Name: "GOSUMDB", Value: cfg.GOSUMDB},
+ {Name: "GOTMPDIR", Value: cfg.Getenv("GOTMPDIR")},
+ {Name: "GOTOOLCHAIN", Value: cfg.Getenv("GOTOOLCHAIN")},
+ {Name: "GOTOOLDIR", Value: build.ToolDir},
+ {Name: "GOVCS", Value: cfg.GOVCS},
+ {Name: "GOVERSION", Value: runtime.Version()},
+ }
+
+ if work.GccgoBin != "" {
+ env = append(env, cfg.EnvVar{Name: "GCCGO", Value: work.GccgoBin})
+ } else {
+ env = append(env, cfg.EnvVar{Name: "GCCGO", Value: work.GccgoName})
+ }
+
+ key, val := cfg.GetArchEnv()
+ if key != "" {
+ env = append(env, cfg.EnvVar{Name: key, Value: val})
+ }
+
+ cc := cfg.Getenv("CC")
+ if cc == "" {
+ cc = cfg.DefaultCC(cfg.Goos, cfg.Goarch)
+ }
+ cxx := cfg.Getenv("CXX")
+ if cxx == "" {
+ cxx = cfg.DefaultCXX(cfg.Goos, cfg.Goarch)
+ }
+ env = append(env, cfg.EnvVar{Name: "AR", Value: envOr("AR", "ar")})
+ env = append(env, cfg.EnvVar{Name: "CC", Value: cc})
+ env = append(env, cfg.EnvVar{Name: "CXX", Value: cxx})
+
+ if cfg.BuildContext.CgoEnabled {
+ env = append(env, cfg.EnvVar{Name: "CGO_ENABLED", Value: "1"})
+ } else {
+ env = append(env, cfg.EnvVar{Name: "CGO_ENABLED", Value: "0"})
+ }
+
+ return env
+}
+
+func envOr(name, def string) string {
+ val := cfg.Getenv(name)
+ if val != "" {
+ return val
+ }
+ return def
+}
+
+func findEnv(env []cfg.EnvVar, name string) string {
+ for _, e := range env {
+ if e.Name == name {
+ return e.Value
+ }
+ }
+ if cfg.CanGetenv(name) {
+ return cfg.Getenv(name)
+ }
+ return ""
+}
+
+// ExtraEnvVars returns environment variables that should not leak into child processes.
+func ExtraEnvVars() []cfg.EnvVar {
+ gomod := ""
+ modload.Init()
+ if modload.HasModRoot() {
+ gomod = modload.ModFilePath()
+ } else if modload.Enabled() {
+ gomod = os.DevNull
+ }
+ modload.InitWorkfile()
+ gowork := modload.WorkFilePath()
+ // As a special case, if a user set off explicitly, report that in GOWORK.
+ if cfg.Getenv("GOWORK") == "off" {
+ gowork = "off"
+ }
+ return []cfg.EnvVar{
+ {Name: "GOMOD", Value: gomod},
+ {Name: "GOWORK", Value: gowork},
+ }
+}
+
+// ExtraEnvVarsCostly returns environment variables that should not leak into child processes
+// but are costly to evaluate.
+func ExtraEnvVarsCostly() []cfg.EnvVar {
+ b := work.NewBuilder("")
+ defer func() {
+ if err := b.Close(); err != nil {
+ base.Fatal(err)
+ }
+ }()
+
+ cppflags, cflags, cxxflags, fflags, ldflags, err := b.CFlags(&load.Package{})
+ if err != nil {
+ // Should not happen - b.CFlags was given an empty package.
+ fmt.Fprintf(os.Stderr, "go: invalid cflags: %v\n", err)
+ return nil
+ }
+ cmd := b.GccCmd(".", "")
+
+ join := func(s []string) string {
+ q, err := quoted.Join(s)
+ if err != nil {
+ return strings.Join(s, " ")
+ }
+ return q
+ }
+
+ return []cfg.EnvVar{
+ // Note: Update the switch in runEnv below when adding to this list.
+ {Name: "CGO_CFLAGS", Value: join(cflags)},
+ {Name: "CGO_CPPFLAGS", Value: join(cppflags)},
+ {Name: "CGO_CXXFLAGS", Value: join(cxxflags)},
+ {Name: "CGO_FFLAGS", Value: join(fflags)},
+ {Name: "CGO_LDFLAGS", Value: join(ldflags)},
+ {Name: "PKG_CONFIG", Value: b.PkgconfigCmd()},
+ {Name: "GOGCCFLAGS", Value: join(cmd[3:])},
+ }
+}
+
+// argKey returns the KEY part of the arg KEY=VAL, or else arg itself.
+func argKey(arg string) string {
+ i := strings.Index(arg, "=")
+ if i < 0 {
+ return arg
+ }
+ return arg[:i]
+}
+
+func runEnv(ctx context.Context, cmd *base.Command, args []string) {
+ if *envJson && *envU {
+ base.Fatalf("go: cannot use -json with -u")
+ }
+ if *envJson && *envW {
+ base.Fatalf("go: cannot use -json with -w")
+ }
+ if *envU && *envW {
+ base.Fatalf("go: cannot use -u with -w")
+ }
+
+ // Handle 'go env -w' and 'go env -u' before calling buildcfg.Check,
+ // so they can be used to recover from an invalid configuration.
+ if *envW {
+ runEnvW(args)
+ return
+ }
+
+ if *envU {
+ runEnvU(args)
+ return
+ }
+
+ buildcfg.Check()
+ if cfg.ExperimentErr != nil {
+ base.Fatal(cfg.ExperimentErr)
+ }
+
+ for _, arg := range args {
+ if strings.Contains(arg, "=") {
+ base.Fatalf("go: invalid variable name %q (use -w to set variable)", arg)
+ }
+ }
+
+ env := cfg.CmdEnv
+ env = append(env, ExtraEnvVars()...)
+
+ if err := fsys.Init(base.Cwd()); err != nil {
+ base.Fatal(err)
+ }
+
+ // Do we need to call ExtraEnvVarsCostly, which is a bit expensive?
+ needCostly := false
+ if len(args) == 0 {
+ // We're listing all environment variables ("go env"),
+ // including the expensive ones.
+ needCostly = true
+ } else {
+ needCostly = false
+ checkCostly:
+ for _, arg := range args {
+ switch argKey(arg) {
+ case "CGO_CFLAGS",
+ "CGO_CPPFLAGS",
+ "CGO_CXXFLAGS",
+ "CGO_FFLAGS",
+ "CGO_LDFLAGS",
+ "PKG_CONFIG",
+ "GOGCCFLAGS":
+ needCostly = true
+ break checkCostly
+ }
+ }
+ }
+ if needCostly {
+ work.BuildInit()
+ env = append(env, ExtraEnvVarsCostly()...)
+ }
+
+ if len(args) > 0 {
+ if *envJson {
+ var es []cfg.EnvVar
+ for _, name := range args {
+ e := cfg.EnvVar{Name: name, Value: findEnv(env, name)}
+ es = append(es, e)
+ }
+ printEnvAsJSON(es)
+ } else {
+ for _, name := range args {
+ fmt.Printf("%s\n", findEnv(env, name))
+ }
+ }
+ return
+ }
+
+ if *envJson {
+ printEnvAsJSON(env)
+ return
+ }
+
+ PrintEnv(os.Stdout, env)
+}
+
+func runEnvW(args []string) {
+ // Process and sanity-check command line.
+ if len(args) == 0 {
+ base.Fatalf("go: no KEY=VALUE arguments given")
+ }
+ osEnv := make(map[string]string)
+ for _, e := range cfg.OrigEnv {
+ if i := strings.Index(e, "="); i >= 0 {
+ osEnv[e[:i]] = e[i+1:]
+ }
+ }
+ add := make(map[string]string)
+ for _, arg := range args {
+ key, val, found := strings.Cut(arg, "=")
+ if !found {
+ base.Fatalf("go: arguments must be KEY=VALUE: invalid argument: %s", arg)
+ }
+ if err := checkEnvWrite(key, val); err != nil {
+ base.Fatal(err)
+ }
+ if _, ok := add[key]; ok {
+ base.Fatalf("go: multiple values for key: %s", key)
+ }
+ add[key] = val
+ if osVal := osEnv[key]; osVal != "" && osVal != val {
+ fmt.Fprintf(os.Stderr, "warning: go env -w %s=... does not override conflicting OS environment variable\n", key)
+ }
+ }
+
+ if err := checkBuildConfig(add, nil); err != nil {
+ base.Fatal(err)
+ }
+
+ gotmp, okGOTMP := add["GOTMPDIR"]
+ if okGOTMP {
+ if !filepath.IsAbs(gotmp) && gotmp != "" {
+ base.Fatalf("go: GOTMPDIR must be an absolute path")
+ }
+ }
+
+ updateEnvFile(add, nil)
+}
+
+func runEnvU(args []string) {
+ // Process and sanity-check command line.
+ if len(args) == 0 {
+ base.Fatalf("go: 'go env -u' requires an argument")
+ }
+ del := make(map[string]bool)
+ for _, arg := range args {
+ if err := checkEnvWrite(arg, ""); err != nil {
+ base.Fatal(err)
+ }
+ del[arg] = true
+ }
+
+ if err := checkBuildConfig(nil, del); err != nil {
+ base.Fatal(err)
+ }
+
+ updateEnvFile(nil, del)
+}
+
+// checkBuildConfig checks whether the build configuration is valid
+// after the specified configuration environment changes are applied.
+func checkBuildConfig(add map[string]string, del map[string]bool) error {
+ // get returns the value for key after applying add and del and
+ // reports whether it changed. cur should be the current value
+ // (i.e., before applying changes) and def should be the default
+ // value (i.e., when no environment variables are provided at all).
+ get := func(key, cur, def string) (string, bool) {
+ if val, ok := add[key]; ok {
+ return val, true
+ }
+ if del[key] {
+ val := getOrigEnv(key)
+ if val == "" {
+ val = def
+ }
+ return val, true
+ }
+ return cur, false
+ }
+
+ goos, okGOOS := get("GOOS", cfg.Goos, build.Default.GOOS)
+ goarch, okGOARCH := get("GOARCH", cfg.Goarch, build.Default.GOARCH)
+ if okGOOS || okGOARCH {
+ if err := work.CheckGOOSARCHPair(goos, goarch); err != nil {
+ return err
+ }
+ }
+
+ goexperiment, okGOEXPERIMENT := get("GOEXPERIMENT", cfg.RawGOEXPERIMENT, buildcfg.DefaultGOEXPERIMENT)
+ if okGOEXPERIMENT {
+ if _, err := buildcfg.ParseGOEXPERIMENT(goos, goarch, goexperiment); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// PrintEnv prints the environment variables to w.
+func PrintEnv(w io.Writer, env []cfg.EnvVar) {
+ for _, e := range env {
+ if e.Name != "TERM" {
+ if runtime.GOOS != "plan9" && bytes.Contains([]byte(e.Value), []byte{0}) {
+ base.Fatalf("go: internal error: encountered null byte in environment variable %s on non-plan9 platform", e.Name)
+ }
+ switch runtime.GOOS {
+ default:
+ fmt.Fprintf(w, "%s=%s\n", e.Name, shellQuote(e.Value))
+ case "plan9":
+ if strings.IndexByte(e.Value, '\x00') < 0 {
+ fmt.Fprintf(w, "%s='%s'\n", e.Name, strings.ReplaceAll(e.Value, "'", "''"))
+ } else {
+ v := strings.Split(e.Value, "\x00")
+ fmt.Fprintf(w, "%s=(", e.Name)
+ for x, s := range v {
+ if x > 0 {
+ fmt.Fprintf(w, " ")
+ }
+ fmt.Fprintf(w, "'%s'", strings.ReplaceAll(s, "'", "''"))
+ }
+ fmt.Fprintf(w, ")\n")
+ }
+ case "windows":
+ if hasNonGraphic(e.Value) {
+ base.Errorf("go: stripping unprintable or unescapable characters from %%%q%%", e.Name)
+ }
+ fmt.Fprintf(w, "set %s=%s\n", e.Name, batchEscape(e.Value))
+ }
+ }
+ }
+}
+
+func hasNonGraphic(s string) bool {
+ for _, c := range []byte(s) {
+ if c == '\r' || c == '\n' || (!unicode.IsGraphic(rune(c)) && !unicode.IsSpace(rune(c))) {
+ return true
+ }
+ }
+ return false
+}
+
+func shellQuote(s string) string {
+ var b bytes.Buffer
+ b.WriteByte('\'')
+ for _, x := range []byte(s) {
+ if x == '\'' {
+ // Close the single quoted string, add an escaped single quote,
+ // and start another single quoted string.
+ b.WriteString(`'\''`)
+ } else {
+ b.WriteByte(x)
+ }
+ }
+ b.WriteByte('\'')
+ return b.String()
+}
+
+func batchEscape(s string) string {
+ var b bytes.Buffer
+ for _, x := range []byte(s) {
+ if x == '\r' || x == '\n' || (!unicode.IsGraphic(rune(x)) && !unicode.IsSpace(rune(x))) {
+ b.WriteRune(unicode.ReplacementChar)
+ continue
+ }
+ switch x {
+ case '%':
+ b.WriteString("%%")
+ case '<', '>', '|', '&', '^':
+ // These are special characters that need to be escaped with ^. See
+ // https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/set_1.
+ b.WriteByte('^')
+ b.WriteByte(x)
+ default:
+ b.WriteByte(x)
+ }
+ }
+ return b.String()
+}
+
+func printEnvAsJSON(env []cfg.EnvVar) {
+ m := make(map[string]string)
+ for _, e := range env {
+ if e.Name == "TERM" {
+ continue
+ }
+ m[e.Name] = e.Value
+ }
+ enc := json.NewEncoder(os.Stdout)
+ enc.SetIndent("", "\t")
+ if err := enc.Encode(m); err != nil {
+ base.Fatalf("go: %s", err)
+ }
+}
+
+func getOrigEnv(key string) string {
+ for _, v := range cfg.OrigEnv {
+ if v, found := strings.CutPrefix(v, key+"="); found {
+ return v
+ }
+ }
+ return ""
+}
+
+func checkEnvWrite(key, val string) error {
+ switch key {
+ case "GOEXE", "GOGCCFLAGS", "GOHOSTARCH", "GOHOSTOS", "GOMOD", "GOWORK", "GOTOOLDIR", "GOVERSION":
+ return fmt.Errorf("%s cannot be modified", key)
+ case "GOENV":
+ return fmt.Errorf("%s can only be set using the OS environment", key)
+ }
+
+ // To catch typos and the like, check that we know the variable.
+ // If it's already in the env file, we assume it's known.
+ if !cfg.CanGetenv(key) {
+ return fmt.Errorf("unknown go command variable %s", key)
+ }
+
+ // Some variables can only have one of a few valid values. If set to an
+ // invalid value, the next cmd/go invocation might fail immediately,
+ // even 'go env -w' itself.
+ switch key {
+ case "GO111MODULE":
+ switch val {
+ case "", "auto", "on", "off":
+ default:
+ return fmt.Errorf("invalid %s value %q", key, val)
+ }
+ case "GOPATH":
+ if strings.HasPrefix(val, "~") {
+ return fmt.Errorf("GOPATH entry cannot start with shell metacharacter '~': %q", val)
+ }
+ if !filepath.IsAbs(val) && val != "" {
+ return fmt.Errorf("GOPATH entry is relative; must be absolute path: %q", val)
+ }
+ case "GOMODCACHE":
+ if !filepath.IsAbs(val) && val != "" {
+ return fmt.Errorf("GOMODCACHE entry is relative; must be absolute path: %q", val)
+ }
+ case "CC", "CXX":
+ if val == "" {
+ break
+ }
+ args, err := quoted.Split(val)
+ if err != nil {
+ return fmt.Errorf("invalid %s: %v", key, err)
+ }
+ if len(args) == 0 {
+ return fmt.Errorf("%s entry cannot contain only space", key)
+ }
+ if !filepath.IsAbs(args[0]) && args[0] != filepath.Base(args[0]) {
+ return fmt.Errorf("%s entry is relative; must be absolute path: %q", key, args[0])
+ }
+ }
+
+ if !utf8.ValidString(val) {
+ return fmt.Errorf("invalid UTF-8 in %s=... value", key)
+ }
+ if strings.Contains(val, "\x00") {
+ return fmt.Errorf("invalid NUL in %s=... value", key)
+ }
+ if strings.ContainsAny(val, "\v\r\n") {
+ return fmt.Errorf("invalid newline in %s=... value", key)
+ }
+ return nil
+}
+
+func readEnvFileLines(mustExist bool) []string {
+ file, err := cfg.EnvFile()
+ if file == "" {
+ if mustExist {
+ base.Fatalf("go: cannot find go env config: %v", err)
+ }
+ return nil
+ }
+ data, err := os.ReadFile(file)
+ if err != nil && (!os.IsNotExist(err) || mustExist) {
+ base.Fatalf("go: reading go env config: %v", err)
+ }
+ lines := strings.SplitAfter(string(data), "\n")
+ if lines[len(lines)-1] == "" {
+ lines = lines[:len(lines)-1]
+ } else {
+ lines[len(lines)-1] += "\n"
+ }
+ return lines
+}
+
+func updateEnvFile(add map[string]string, del map[string]bool) {
+ lines := readEnvFileLines(len(add) == 0)
+
+ // Delete all but last copy of any duplicated variables,
+ // since the last copy is the one that takes effect.
+ prev := make(map[string]int)
+ for l, line := range lines {
+ if key := lineToKey(line); key != "" {
+ if p, ok := prev[key]; ok {
+ lines[p] = ""
+ }
+ prev[key] = l
+ }
+ }
+
+ // Add variables (go env -w). Update existing lines in file if present, add to end otherwise.
+ for key, val := range add {
+ if p, ok := prev[key]; ok {
+ lines[p] = key + "=" + val + "\n"
+ delete(add, key)
+ }
+ }
+ for key, val := range add {
+ lines = append(lines, key+"="+val+"\n")
+ }
+
+ // Delete requested variables (go env -u).
+ for key := range del {
+ if p, ok := prev[key]; ok {
+ lines[p] = ""
+ }
+ }
+
+ // Sort runs of KEY=VALUE lines
+ // (that is, blocks of lines where blocks are separated
+ // by comments, blank lines, or invalid lines).
+ start := 0
+ for i := 0; i <= len(lines); i++ {
+ if i == len(lines) || lineToKey(lines[i]) == "" {
+ sortKeyValues(lines[start:i])
+ start = i + 1
+ }
+ }
+
+ file, err := cfg.EnvFile()
+ if file == "" {
+ base.Fatalf("go: cannot find go env config: %v", err)
+ }
+ data := []byte(strings.Join(lines, ""))
+ err = os.WriteFile(file, data, 0666)
+ if err != nil {
+ // Try creating directory.
+ os.MkdirAll(filepath.Dir(file), 0777)
+ err = os.WriteFile(file, data, 0666)
+ if err != nil {
+ base.Fatalf("go: writing go env config: %v", err)
+ }
+ }
+}
+
+// lineToKey returns the KEY part of the line KEY=VALUE or else an empty string.
+func lineToKey(line string) string {
+ i := strings.Index(line, "=")
+ if i < 0 || strings.Contains(line[:i], "#") {
+ return ""
+ }
+ return line[:i]
+}
+
+// sortKeyValues sorts a sequence of lines by key.
+// It differs from sort.Strings in that keys which are GOx where x is an ASCII
+// character smaller than = sort after GO=.
+// (There are no such keys currently. It used to matter for GO386 which was
+// removed in Go 1.16.)
+func sortKeyValues(lines []string) {
+ sort.Slice(lines, func(i, j int) bool {
+ return lineToKey(lines[i]) < lineToKey(lines[j])
+ })
+}
diff --git a/src/cmd/go/internal/envcmd/env_test.go b/src/cmd/go/internal/envcmd/env_test.go
new file mode 100644
index 0000000..7419cf3
--- /dev/null
+++ b/src/cmd/go/internal/envcmd/env_test.go
@@ -0,0 +1,93 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build unix || windows
+
+package envcmd
+
+import (
+ "bytes"
+ "cmd/go/internal/cfg"
+ "fmt"
+ "internal/testenv"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "testing"
+ "unicode"
+)
+
+func FuzzPrintEnvEscape(f *testing.F) {
+ f.Add(`$(echo 'cc"'; echo 'OOPS="oops')`)
+ f.Add("$(echo shell expansion 1>&2)")
+ f.Add("''")
+ f.Add(`C:\"Program Files"\`)
+ f.Add(`\\"Quoted Host"\\share`)
+ f.Add("\xfb")
+ f.Add("0")
+ f.Add("")
+ f.Add("''''''''")
+ f.Add("\r")
+ f.Add("\n")
+ f.Add("E,%")
+ f.Fuzz(func(t *testing.T, s string) {
+ t.Parallel()
+
+ for _, c := range []byte(s) {
+ if c == 0 {
+ t.Skipf("skipping %q: contains a null byte. Null bytes can't occur in the environment"+
+ " outside of Plan 9, which has different code path than Windows and Unix that this test"+
+ " isn't testing.", s)
+ }
+ if c > unicode.MaxASCII {
+ t.Skipf("skipping %#q: contains a non-ASCII character %q", s, c)
+ }
+ if !unicode.IsGraphic(rune(c)) && !unicode.IsSpace(rune(c)) {
+ t.Skipf("skipping %#q: contains non-graphic character %q", s, c)
+ }
+ if runtime.GOOS == "windows" && c == '\r' || c == '\n' {
+ t.Skipf("skipping %#q on Windows: contains unescapable character %q", s, c)
+ }
+ }
+
+ var b bytes.Buffer
+ if runtime.GOOS == "windows" {
+ b.WriteString("@echo off\n")
+ }
+ PrintEnv(&b, []cfg.EnvVar{{Name: "var", Value: s}})
+ var want string
+ if runtime.GOOS == "windows" {
+ fmt.Fprintf(&b, "echo \"%%var%%\"\n")
+ want += "\"" + s + "\"\r\n"
+ } else {
+ fmt.Fprintf(&b, "printf '%%s\\n' \"$var\"\n")
+ want += s + "\n"
+ }
+ scriptfilename := "script.sh"
+ if runtime.GOOS == "windows" {
+ scriptfilename = "script.bat"
+ }
+ var cmd *exec.Cmd
+ if runtime.GOOS == "windows" {
+ scriptfile := filepath.Join(t.TempDir(), scriptfilename)
+ if err := os.WriteFile(scriptfile, b.Bytes(), 0777); err != nil {
+ t.Fatal(err)
+ }
+ cmd = testenv.Command(t, "cmd.exe", "/C", scriptfile)
+ } else {
+ cmd = testenv.Command(t, "sh", "-c", b.String())
+ }
+ out, err := cmd.Output()
+ t.Log(string(out))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if string(out) != want {
+ t.Fatalf("output of running PrintEnv script and echoing variable: got: %q, want: %q",
+ string(out), want)
+ }
+ })
+}