1// Copyright 2012 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 envcmd implements the “go env” command.
6package envcmd
7
8import (
9	"bytes"
10	"context"
11	"encoding/json"
12	"fmt"
13	"go/build"
14	"internal/buildcfg"
15	"io"
16	"os"
17	"path/filepath"
18	"runtime"
19	"sort"
20	"strings"
21	"unicode"
22	"unicode/utf8"
23
24	"cmd/go/internal/base"
25	"cmd/go/internal/cache"
26	"cmd/go/internal/cfg"
27	"cmd/go/internal/fsys"
28	"cmd/go/internal/load"
29	"cmd/go/internal/modload"
30	"cmd/go/internal/work"
31	"cmd/internal/quoted"
32	"cmd/internal/telemetry"
33)
34
35var CmdEnv = &base.Command{
36	UsageLine: "go env [-json] [-changed] [-u] [-w] [var ...]",
37	Short:     "print Go environment information",
38	Long: `
39Env prints Go environment information.
40
41By default env prints information as a shell script
42(on Windows, a batch file). If one or more variable
43names is given as arguments, env prints the value of
44each named variable on its own line.
45
46The -json flag prints the environment in JSON format
47instead of as a shell script.
48
49The -u flag requires one or more arguments and unsets
50the default setting for the named environment variables,
51if one has been set with 'go env -w'.
52
53The -w flag requires one or more arguments of the
54form NAME=VALUE and changes the default settings
55of the named environment variables to the given values.
56
57The -changed flag prints only those settings whose effective
58value differs from the default value that would be obtained in
59an empty environment with no prior uses of the -w flag.
60
61For more about environment variables, see 'go help environment'.
62	`,
63}
64
65func init() {
66	CmdEnv.Run = runEnv // break init cycle
67	base.AddChdirFlag(&CmdEnv.Flag)
68	base.AddBuildFlagsNX(&CmdEnv.Flag)
69}
70
71var (
72	envJson    = CmdEnv.Flag.Bool("json", false, "")
73	envU       = CmdEnv.Flag.Bool("u", false, "")
74	envW       = CmdEnv.Flag.Bool("w", false, "")
75	envChanged = CmdEnv.Flag.Bool("changed", false, "")
76)
77
78func MkEnv() []cfg.EnvVar {
79	envFile, envFileChanged, _ := cfg.EnvFile()
80	env := []cfg.EnvVar{
81		{Name: "GO111MODULE", Value: cfg.Getenv("GO111MODULE")},
82		{Name: "GOARCH", Value: cfg.Goarch, Changed: cfg.Goarch != runtime.GOARCH},
83		{Name: "GOBIN", Value: cfg.GOBIN},
84		{Name: "GOCACHE"},
85		{Name: "GOENV", Value: envFile, Changed: envFileChanged},
86		{Name: "GOEXE", Value: cfg.ExeSuffix},
87
88		// List the raw value of GOEXPERIMENT, not the cleaned one.
89		// The set of default experiments may change from one release
90		// to the next, so a GOEXPERIMENT setting that is redundant
91		// with the current toolchain might actually be relevant with
92		// a different version (for example, when bisecting a regression).
93		{Name: "GOEXPERIMENT", Value: cfg.RawGOEXPERIMENT},
94
95		{Name: "GOFLAGS", Value: cfg.Getenv("GOFLAGS")},
96		{Name: "GOHOSTARCH", Value: runtime.GOARCH},
97		{Name: "GOHOSTOS", Value: runtime.GOOS},
98		{Name: "GOINSECURE", Value: cfg.GOINSECURE},
99		{Name: "GOMODCACHE", Value: cfg.GOMODCACHE, Changed: cfg.GOMODCACHEChanged},
100		{Name: "GONOPROXY", Value: cfg.GONOPROXY, Changed: cfg.GONOPROXYChanged},
101		{Name: "GONOSUMDB", Value: cfg.GONOSUMDB, Changed: cfg.GONOSUMDBChanged},
102		{Name: "GOOS", Value: cfg.Goos, Changed: cfg.Goos != runtime.GOOS},
103		{Name: "GOPATH", Value: cfg.BuildContext.GOPATH, Changed: cfg.GOPATHChanged},
104		{Name: "GOPRIVATE", Value: cfg.GOPRIVATE},
105		{Name: "GOPROXY", Value: cfg.GOPROXY, Changed: cfg.GOPROXYChanged},
106		{Name: "GOROOT", Value: cfg.GOROOT},
107		{Name: "GOSUMDB", Value: cfg.GOSUMDB, Changed: cfg.GOSUMDBChanged},
108		{Name: "GOTMPDIR", Value: cfg.Getenv("GOTMPDIR")},
109		{Name: "GOTOOLCHAIN"},
110		{Name: "GOTOOLDIR", Value: build.ToolDir},
111		{Name: "GOVCS", Value: cfg.GOVCS},
112		{Name: "GOVERSION", Value: runtime.Version()},
113		{Name: "GODEBUG", Value: os.Getenv("GODEBUG")},
114		{Name: "GOTELEMETRY", Value: telemetry.Mode()},
115		{Name: "GOTELEMETRYDIR", Value: telemetry.Dir()},
116	}
117
118	for i := range env {
119		switch env[i].Name {
120		case "GO111MODULE":
121			if env[i].Value != "on" && env[i].Value != "" {
122				env[i].Changed = true
123			}
124		case "GOBIN", "GOEXPERIMENT", "GOFLAGS", "GOINSECURE", "GOPRIVATE", "GOTMPDIR", "GOVCS":
125			if env[i].Value != "" {
126				env[i].Changed = true
127			}
128		case "GOCACHE":
129			env[i].Value, env[i].Changed = cache.DefaultDir()
130		case "GOTOOLCHAIN":
131			env[i].Value, env[i].Changed = cfg.EnvOrAndChanged("GOTOOLCHAIN", "")
132		case "GODEBUG":
133			env[i].Changed = env[i].Value != ""
134		}
135	}
136
137	if work.GccgoBin != "" {
138		env = append(env, cfg.EnvVar{Name: "GCCGO", Value: work.GccgoBin, Changed: true})
139	} else {
140		env = append(env, cfg.EnvVar{Name: "GCCGO", Value: work.GccgoName})
141	}
142
143	goarch, val, changed := cfg.GetArchEnv()
144	if goarch != "" {
145		env = append(env, cfg.EnvVar{Name: goarch, Value: val, Changed: changed})
146	}
147
148	cc := cfg.Getenv("CC")
149	ccChanged := true
150	if cc == "" {
151		ccChanged = false
152		cc = cfg.DefaultCC(cfg.Goos, cfg.Goarch)
153	}
154	cxx := cfg.Getenv("CXX")
155	cxxChanged := true
156	if cxx == "" {
157		cxxChanged = false
158		cxx = cfg.DefaultCXX(cfg.Goos, cfg.Goarch)
159	}
160	ar, arChanged := cfg.EnvOrAndChanged("AR", "ar")
161	env = append(env, cfg.EnvVar{Name: "AR", Value: ar, Changed: arChanged})
162	env = append(env, cfg.EnvVar{Name: "CC", Value: cc, Changed: ccChanged})
163	env = append(env, cfg.EnvVar{Name: "CXX", Value: cxx, Changed: cxxChanged})
164
165	if cfg.BuildContext.CgoEnabled {
166		env = append(env, cfg.EnvVar{Name: "CGO_ENABLED", Value: "1", Changed: cfg.CGOChanged})
167	} else {
168		env = append(env, cfg.EnvVar{Name: "CGO_ENABLED", Value: "0", Changed: cfg.CGOChanged})
169	}
170
171	return env
172}
173
174func findEnv(env []cfg.EnvVar, name string) string {
175	for _, e := range env {
176		if e.Name == name {
177			return e.Value
178		}
179	}
180	if cfg.CanGetenv(name) {
181		return cfg.Getenv(name)
182	}
183	return ""
184}
185
186// ExtraEnvVars returns environment variables that should not leak into child processes.
187func ExtraEnvVars() []cfg.EnvVar {
188	gomod := ""
189	modload.Init()
190	if modload.HasModRoot() {
191		gomod = modload.ModFilePath()
192	} else if modload.Enabled() {
193		gomod = os.DevNull
194	}
195	modload.InitWorkfile()
196	gowork := modload.WorkFilePath()
197	// As a special case, if a user set off explicitly, report that in GOWORK.
198	if cfg.Getenv("GOWORK") == "off" {
199		gowork = "off"
200	}
201	return []cfg.EnvVar{
202		{Name: "GOMOD", Value: gomod},
203		{Name: "GOWORK", Value: gowork},
204	}
205}
206
207// ExtraEnvVarsCostly returns environment variables that should not leak into child processes
208// but are costly to evaluate.
209func ExtraEnvVarsCostly() []cfg.EnvVar {
210	b := work.NewBuilder("")
211	defer func() {
212		if err := b.Close(); err != nil {
213			base.Fatal(err)
214		}
215	}()
216
217	cppflags, cflags, cxxflags, fflags, ldflags, err := b.CFlags(&load.Package{})
218	if err != nil {
219		// Should not happen - b.CFlags was given an empty package.
220		fmt.Fprintf(os.Stderr, "go: invalid cflags: %v\n", err)
221		return nil
222	}
223	cmd := b.GccCmd(".", "")
224
225	join := func(s []string) string {
226		q, err := quoted.Join(s)
227		if err != nil {
228			return strings.Join(s, " ")
229		}
230		return q
231	}
232
233	ret := []cfg.EnvVar{
234		// Note: Update the switch in runEnv below when adding to this list.
235		{Name: "CGO_CFLAGS", Value: join(cflags)},
236		{Name: "CGO_CPPFLAGS", Value: join(cppflags)},
237		{Name: "CGO_CXXFLAGS", Value: join(cxxflags)},
238		{Name: "CGO_FFLAGS", Value: join(fflags)},
239		{Name: "CGO_LDFLAGS", Value: join(ldflags)},
240		{Name: "PKG_CONFIG", Value: b.PkgconfigCmd()},
241		{Name: "GOGCCFLAGS", Value: join(cmd[3:])},
242	}
243
244	for i := range ret {
245		ev := &ret[i]
246		switch ev.Name {
247		case "GOGCCFLAGS": // GOGCCFLAGS cannot be modified
248		case "CGO_CPPFLAGS":
249			ev.Changed = ev.Value != ""
250		case "PKG_CONFIG":
251			ev.Changed = ev.Value != cfg.DefaultPkgConfig
252		case "CGO_CXXFLAGS", "CGO_CFLAGS", "CGO_FFLAGS", "GGO_LDFLAGS":
253			ev.Changed = ev.Value != work.DefaultCFlags
254		}
255	}
256
257	return ret
258}
259
260// argKey returns the KEY part of the arg KEY=VAL, or else arg itself.
261func argKey(arg string) string {
262	i := strings.Index(arg, "=")
263	if i < 0 {
264		return arg
265	}
266	return arg[:i]
267}
268
269func runEnv(ctx context.Context, cmd *base.Command, args []string) {
270	if *envJson && *envU {
271		base.Fatalf("go: cannot use -json with -u")
272	}
273	if *envJson && *envW {
274		base.Fatalf("go: cannot use -json with -w")
275	}
276	if *envU && *envW {
277		base.Fatalf("go: cannot use -u with -w")
278	}
279
280	// Handle 'go env -w' and 'go env -u' before calling buildcfg.Check,
281	// so they can be used to recover from an invalid configuration.
282	if *envW {
283		runEnvW(args)
284		return
285	}
286
287	if *envU {
288		runEnvU(args)
289		return
290	}
291
292	buildcfg.Check()
293	if cfg.ExperimentErr != nil {
294		base.Fatal(cfg.ExperimentErr)
295	}
296
297	for _, arg := range args {
298		if strings.Contains(arg, "=") {
299			base.Fatalf("go: invalid variable name %q (use -w to set variable)", arg)
300		}
301	}
302
303	env := cfg.CmdEnv
304	env = append(env, ExtraEnvVars()...)
305
306	if err := fsys.Init(base.Cwd()); err != nil {
307		base.Fatal(err)
308	}
309
310	// Do we need to call ExtraEnvVarsCostly, which is a bit expensive?
311	needCostly := false
312	if len(args) == 0 {
313		// We're listing all environment variables ("go env"),
314		// including the expensive ones.
315		needCostly = true
316	} else {
317		needCostly = false
318	checkCostly:
319		for _, arg := range args {
320			switch argKey(arg) {
321			case "CGO_CFLAGS",
322				"CGO_CPPFLAGS",
323				"CGO_CXXFLAGS",
324				"CGO_FFLAGS",
325				"CGO_LDFLAGS",
326				"PKG_CONFIG",
327				"GOGCCFLAGS":
328				needCostly = true
329				break checkCostly
330			}
331		}
332	}
333	if needCostly {
334		work.BuildInit()
335		env = append(env, ExtraEnvVarsCostly()...)
336	}
337
338	if len(args) > 0 {
339		// Show only the named vars.
340		if !*envChanged {
341			if *envJson {
342				var es []cfg.EnvVar
343				for _, name := range args {
344					e := cfg.EnvVar{Name: name, Value: findEnv(env, name)}
345					es = append(es, e)
346				}
347				env = es
348			} else {
349				// Print just the values, without names.
350				for _, name := range args {
351					fmt.Printf("%s\n", findEnv(env, name))
352				}
353				return
354			}
355		} else {
356			// Show only the changed, named vars.
357			var es []cfg.EnvVar
358			for _, name := range args {
359				for _, e := range env {
360					if e.Name == name {
361						es = append(es, e)
362						break
363					}
364				}
365			}
366			env = es
367		}
368	}
369
370	// print
371	if *envJson {
372		printEnvAsJSON(env, *envChanged)
373	} else {
374		PrintEnv(os.Stdout, env, *envChanged)
375	}
376}
377
378func runEnvW(args []string) {
379	// Process and sanity-check command line.
380	if len(args) == 0 {
381		base.Fatalf("go: no KEY=VALUE arguments given")
382	}
383	osEnv := make(map[string]string)
384	for _, e := range cfg.OrigEnv {
385		if i := strings.Index(e, "="); i >= 0 {
386			osEnv[e[:i]] = e[i+1:]
387		}
388	}
389	add := make(map[string]string)
390	for _, arg := range args {
391		key, val, found := strings.Cut(arg, "=")
392		if !found {
393			base.Fatalf("go: arguments must be KEY=VALUE: invalid argument: %s", arg)
394		}
395		if err := checkEnvWrite(key, val); err != nil {
396			base.Fatal(err)
397		}
398		if _, ok := add[key]; ok {
399			base.Fatalf("go: multiple values for key: %s", key)
400		}
401		add[key] = val
402		if osVal := osEnv[key]; osVal != "" && osVal != val {
403			fmt.Fprintf(os.Stderr, "warning: go env -w %s=... does not override conflicting OS environment variable\n", key)
404		}
405	}
406
407	if err := checkBuildConfig(add, nil); err != nil {
408		base.Fatal(err)
409	}
410
411	gotmp, okGOTMP := add["GOTMPDIR"]
412	if okGOTMP {
413		if !filepath.IsAbs(gotmp) && gotmp != "" {
414			base.Fatalf("go: GOTMPDIR must be an absolute path")
415		}
416	}
417
418	updateEnvFile(add, nil)
419}
420
421func runEnvU(args []string) {
422	// Process and sanity-check command line.
423	if len(args) == 0 {
424		base.Fatalf("go: 'go env -u' requires an argument")
425	}
426	del := make(map[string]bool)
427	for _, arg := range args {
428		if err := checkEnvWrite(arg, ""); err != nil {
429			base.Fatal(err)
430		}
431		del[arg] = true
432	}
433
434	if err := checkBuildConfig(nil, del); err != nil {
435		base.Fatal(err)
436	}
437
438	updateEnvFile(nil, del)
439}
440
441// checkBuildConfig checks whether the build configuration is valid
442// after the specified configuration environment changes are applied.
443func checkBuildConfig(add map[string]string, del map[string]bool) error {
444	// get returns the value for key after applying add and del and
445	// reports whether it changed. cur should be the current value
446	// (i.e., before applying changes) and def should be the default
447	// value (i.e., when no environment variables are provided at all).
448	get := func(key, cur, def string) (string, bool) {
449		if val, ok := add[key]; ok {
450			return val, true
451		}
452		if del[key] {
453			val := getOrigEnv(key)
454			if val == "" {
455				val = def
456			}
457			return val, true
458		}
459		return cur, false
460	}
461
462	goos, okGOOS := get("GOOS", cfg.Goos, build.Default.GOOS)
463	goarch, okGOARCH := get("GOARCH", cfg.Goarch, build.Default.GOARCH)
464	if okGOOS || okGOARCH {
465		if err := work.CheckGOOSARCHPair(goos, goarch); err != nil {
466			return err
467		}
468	}
469
470	goexperiment, okGOEXPERIMENT := get("GOEXPERIMENT", cfg.RawGOEXPERIMENT, buildcfg.DefaultGOEXPERIMENT)
471	if okGOEXPERIMENT {
472		if _, err := buildcfg.ParseGOEXPERIMENT(goos, goarch, goexperiment); err != nil {
473			return err
474		}
475	}
476
477	return nil
478}
479
480// PrintEnv prints the environment variables to w.
481func PrintEnv(w io.Writer, env []cfg.EnvVar, onlyChanged bool) {
482	for _, e := range env {
483		if e.Name != "TERM" {
484			if runtime.GOOS != "plan9" && bytes.Contains([]byte(e.Value), []byte{0}) {
485				base.Fatalf("go: internal error: encountered null byte in environment variable %s on non-plan9 platform", e.Name)
486			}
487			if onlyChanged && !e.Changed {
488				continue
489			}
490			switch runtime.GOOS {
491			default:
492				fmt.Fprintf(w, "%s=%s\n", e.Name, shellQuote(e.Value))
493			case "plan9":
494				if strings.IndexByte(e.Value, '\x00') < 0 {
495					fmt.Fprintf(w, "%s='%s'\n", e.Name, strings.ReplaceAll(e.Value, "'", "''"))
496				} else {
497					v := strings.Split(e.Value, "\x00")
498					fmt.Fprintf(w, "%s=(", e.Name)
499					for x, s := range v {
500						if x > 0 {
501							fmt.Fprintf(w, " ")
502						}
503						fmt.Fprintf(w, "'%s'", strings.ReplaceAll(s, "'", "''"))
504					}
505					fmt.Fprintf(w, ")\n")
506				}
507			case "windows":
508				if hasNonGraphic(e.Value) {
509					base.Errorf("go: stripping unprintable or unescapable characters from %%%q%%", e.Name)
510				}
511				fmt.Fprintf(w, "set %s=%s\n", e.Name, batchEscape(e.Value))
512			}
513		}
514	}
515}
516
517func hasNonGraphic(s string) bool {
518	for _, c := range []byte(s) {
519		if c == '\r' || c == '\n' || (!unicode.IsGraphic(rune(c)) && !unicode.IsSpace(rune(c))) {
520			return true
521		}
522	}
523	return false
524}
525
526func shellQuote(s string) string {
527	var b bytes.Buffer
528	b.WriteByte('\'')
529	for _, x := range []byte(s) {
530		if x == '\'' {
531			// Close the single quoted string, add an escaped single quote,
532			// and start another single quoted string.
533			b.WriteString(`'\''`)
534		} else {
535			b.WriteByte(x)
536		}
537	}
538	b.WriteByte('\'')
539	return b.String()
540}
541
542func batchEscape(s string) string {
543	var b bytes.Buffer
544	for _, x := range []byte(s) {
545		if x == '\r' || x == '\n' || (!unicode.IsGraphic(rune(x)) && !unicode.IsSpace(rune(x))) {
546			b.WriteRune(unicode.ReplacementChar)
547			continue
548		}
549		switch x {
550		case '%':
551			b.WriteString("%%")
552		case '<', '>', '|', '&', '^':
553			// These are special characters that need to be escaped with ^. See
554			// https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/set_1.
555			b.WriteByte('^')
556			b.WriteByte(x)
557		default:
558			b.WriteByte(x)
559		}
560	}
561	return b.String()
562}
563
564func printEnvAsJSON(env []cfg.EnvVar, onlyChanged bool) {
565	m := make(map[string]string)
566	for _, e := range env {
567		if e.Name == "TERM" {
568			continue
569		}
570		if onlyChanged && !e.Changed {
571			continue
572		}
573		m[e.Name] = e.Value
574	}
575	enc := json.NewEncoder(os.Stdout)
576	enc.SetIndent("", "\t")
577	if err := enc.Encode(m); err != nil {
578		base.Fatalf("go: %s", err)
579	}
580}
581
582func getOrigEnv(key string) string {
583	for _, v := range cfg.OrigEnv {
584		if v, found := strings.CutPrefix(v, key+"="); found {
585			return v
586		}
587	}
588	return ""
589}
590
591func checkEnvWrite(key, val string) error {
592	switch key {
593	case "GOEXE", "GOGCCFLAGS", "GOHOSTARCH", "GOHOSTOS", "GOMOD", "GOWORK", "GOTOOLDIR", "GOVERSION":
594		return fmt.Errorf("%s cannot be modified", key)
595	case "GOENV", "GODEBUG":
596		return fmt.Errorf("%s can only be set using the OS environment", key)
597	}
598
599	// To catch typos and the like, check that we know the variable.
600	// If it's already in the env file, we assume it's known.
601	if !cfg.CanGetenv(key) {
602		return fmt.Errorf("unknown go command variable %s", key)
603	}
604
605	// Some variables can only have one of a few valid values. If set to an
606	// invalid value, the next cmd/go invocation might fail immediately,
607	// even 'go env -w' itself.
608	switch key {
609	case "GO111MODULE":
610		switch val {
611		case "", "auto", "on", "off":
612		default:
613			return fmt.Errorf("invalid %s value %q", key, val)
614		}
615	case "GOPATH":
616		if strings.HasPrefix(val, "~") {
617			return fmt.Errorf("GOPATH entry cannot start with shell metacharacter '~': %q", val)
618		}
619		if !filepath.IsAbs(val) && val != "" {
620			return fmt.Errorf("GOPATH entry is relative; must be absolute path: %q", val)
621		}
622	case "GOMODCACHE":
623		if !filepath.IsAbs(val) && val != "" {
624			return fmt.Errorf("GOMODCACHE entry is relative; must be absolute path: %q", val)
625		}
626	case "CC", "CXX":
627		if val == "" {
628			break
629		}
630		args, err := quoted.Split(val)
631		if err != nil {
632			return fmt.Errorf("invalid %s: %v", key, err)
633		}
634		if len(args) == 0 {
635			return fmt.Errorf("%s entry cannot contain only space", key)
636		}
637		if !filepath.IsAbs(args[0]) && args[0] != filepath.Base(args[0]) {
638			return fmt.Errorf("%s entry is relative; must be absolute path: %q", key, args[0])
639		}
640	}
641
642	if !utf8.ValidString(val) {
643		return fmt.Errorf("invalid UTF-8 in %s=... value", key)
644	}
645	if strings.Contains(val, "\x00") {
646		return fmt.Errorf("invalid NUL in %s=... value", key)
647	}
648	if strings.ContainsAny(val, "\v\r\n") {
649		return fmt.Errorf("invalid newline in %s=... value", key)
650	}
651	return nil
652}
653
654func readEnvFileLines(mustExist bool) []string {
655	file, _, err := cfg.EnvFile()
656	if file == "" {
657		if mustExist {
658			base.Fatalf("go: cannot find go env config: %v", err)
659		}
660		return nil
661	}
662	data, err := os.ReadFile(file)
663	if err != nil && (!os.IsNotExist(err) || mustExist) {
664		base.Fatalf("go: reading go env config: %v", err)
665	}
666	lines := strings.SplitAfter(string(data), "\n")
667	if lines[len(lines)-1] == "" {
668		lines = lines[:len(lines)-1]
669	} else {
670		lines[len(lines)-1] += "\n"
671	}
672	return lines
673}
674
675func updateEnvFile(add map[string]string, del map[string]bool) {
676	lines := readEnvFileLines(len(add) == 0)
677
678	// Delete all but last copy of any duplicated variables,
679	// since the last copy is the one that takes effect.
680	prev := make(map[string]int)
681	for l, line := range lines {
682		if key := lineToKey(line); key != "" {
683			if p, ok := prev[key]; ok {
684				lines[p] = ""
685			}
686			prev[key] = l
687		}
688	}
689
690	// Add variables (go env -w). Update existing lines in file if present, add to end otherwise.
691	for key, val := range add {
692		if p, ok := prev[key]; ok {
693			lines[p] = key + "=" + val + "\n"
694			delete(add, key)
695		}
696	}
697	for key, val := range add {
698		lines = append(lines, key+"="+val+"\n")
699	}
700
701	// Delete requested variables (go env -u).
702	for key := range del {
703		if p, ok := prev[key]; ok {
704			lines[p] = ""
705		}
706	}
707
708	// Sort runs of KEY=VALUE lines
709	// (that is, blocks of lines where blocks are separated
710	// by comments, blank lines, or invalid lines).
711	start := 0
712	for i := 0; i <= len(lines); i++ {
713		if i == len(lines) || lineToKey(lines[i]) == "" {
714			sortKeyValues(lines[start:i])
715			start = i + 1
716		}
717	}
718
719	file, _, err := cfg.EnvFile()
720	if file == "" {
721		base.Fatalf("go: cannot find go env config: %v", err)
722	}
723	data := []byte(strings.Join(lines, ""))
724	err = os.WriteFile(file, data, 0666)
725	if err != nil {
726		// Try creating directory.
727		os.MkdirAll(filepath.Dir(file), 0777)
728		err = os.WriteFile(file, data, 0666)
729		if err != nil {
730			base.Fatalf("go: writing go env config: %v", err)
731		}
732	}
733}
734
735// lineToKey returns the KEY part of the line KEY=VALUE or else an empty string.
736func lineToKey(line string) string {
737	i := strings.Index(line, "=")
738	if i < 0 || strings.Contains(line[:i], "#") {
739		return ""
740	}
741	return line[:i]
742}
743
744// sortKeyValues sorts a sequence of lines by key.
745// It differs from sort.Strings in that GO386= sorts after GO=.
746func sortKeyValues(lines []string) {
747	sort.Slice(lines, func(i, j int) bool {
748		return lineToKey(lines[i]) < lineToKey(lines[j])
749	})
750}
751