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