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