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