github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/lintcmd/cmd.go (about) 1 // Package lintcmd implements the frontend of an analysis runner. 2 // It serves as the entry-point for the staticcheck command, and can also be used to implement custom linters that behave like staticcheck. 3 package lintcmd 4 5 import ( 6 "bufio" 7 "encoding/gob" 8 "flag" 9 "fmt" 10 "go/token" 11 "io" 12 "log" 13 "os" 14 "path/filepath" 15 "reflect" 16 "runtime" 17 "runtime/pprof" 18 "runtime/trace" 19 "sort" 20 "strings" 21 "sync" 22 "time" 23 24 "github.com/amarpal/go-tools/analysis/lint" 25 "github.com/amarpal/go-tools/config" 26 "github.com/amarpal/go-tools/go/loader" 27 "github.com/amarpal/go-tools/lintcmd/version" 28 29 "golang.org/x/tools/go/analysis" 30 "golang.org/x/tools/go/buildutil" 31 ) 32 33 type buildConfig struct { 34 Name string 35 Envs []string 36 Flags []string 37 } 38 39 // Command represents a linter command line tool. 40 type Command struct { 41 name string 42 analyzers map[string]*lint.Analyzer 43 version string 44 machineVersion string 45 46 flags struct { 47 fs *flag.FlagSet 48 49 tags string 50 tests bool 51 showIgnored bool 52 formatter string 53 54 // mutually exclusive mode flags 55 explain string 56 printVersion bool 57 listChecks bool 58 merge bool 59 60 matrix bool 61 62 debugCpuprofile string 63 debugMemprofile string 64 debugVersion bool 65 debugNoCompileErrors bool 66 debugMeasureAnalyzers string 67 debugTrace string 68 69 checks list 70 fail list 71 goVersion versionFlag 72 } 73 } 74 75 // NewCommand returns a new Command. 76 func NewCommand(name string) *Command { 77 cmd := &Command{ 78 name: name, 79 analyzers: map[string]*lint.Analyzer{}, 80 version: "devel", 81 machineVersion: "devel", 82 } 83 cmd.initFlagSet(name) 84 return cmd 85 } 86 87 // SetVersion sets the command's version. 88 // It is divided into a human part and a machine part. 89 // For example, Staticcheck 2020.2.1 had the human version "2020.2.1" and the machine version "v0.1.1". 90 // If you only use Semver, you can set both parts to the same value. 91 // 92 // Calling this method is optional. Both versions default to "devel", and we'll attempt to deduce more version information from the Go module. 93 func (cmd *Command) SetVersion(human, machine string) { 94 cmd.version = human 95 cmd.machineVersion = machine 96 } 97 98 // FlagSet returns the command's flag set. 99 // This can be used to add additional command line arguments. 100 func (cmd *Command) FlagSet() *flag.FlagSet { 101 return cmd.flags.fs 102 } 103 104 // AddAnalyzers adds analyzers to the command. 105 // These are lint.Analyzer analyzers, which wrap analysis.Analyzer analyzers, bundling them with structured documentation. 106 // 107 // To add analysis.Analyzer analyzers without providing structured documentation, use AddBareAnalyzers. 108 func (cmd *Command) AddAnalyzers(as ...*lint.Analyzer) { 109 for _, a := range as { 110 cmd.analyzers[a.Analyzer.Name] = a 111 } 112 } 113 114 // AddBareAnalyzers adds bare analyzers to the command. 115 func (cmd *Command) AddBareAnalyzers(as ...*analysis.Analyzer) { 116 for _, a := range as { 117 var title, text string 118 if idx := strings.Index(a.Doc, "\n\n"); idx > -1 { 119 title = a.Doc[:idx] 120 text = a.Doc[idx+2:] 121 } 122 123 doc := &lint.Documentation{ 124 Title: title, 125 Text: text, 126 Severity: lint.SeverityWarning, 127 } 128 129 cmd.analyzers[a.Name] = &lint.Analyzer{ 130 Doc: doc, 131 Analyzer: a, 132 } 133 } 134 } 135 136 func (cmd *Command) initFlagSet(name string) { 137 flags := flag.NewFlagSet("", flag.ExitOnError) 138 cmd.flags.fs = flags 139 flags.Usage = usage(name, flags) 140 141 flags.StringVar(&cmd.flags.tags, "tags", "", "List of `build tags`") 142 flags.BoolVar(&cmd.flags.tests, "tests", true, "Include tests") 143 flags.BoolVar(&cmd.flags.printVersion, "version", false, "Print version and exit") 144 flags.BoolVar(&cmd.flags.showIgnored, "show-ignored", false, "Don't filter ignored diagnostics") 145 flags.StringVar(&cmd.flags.formatter, "f", "text", "Output `format` (valid choices are 'stylish', 'text' and 'json')") 146 flags.StringVar(&cmd.flags.explain, "explain", "", "Print description of `check`") 147 flags.BoolVar(&cmd.flags.listChecks, "list-checks", false, "List all available checks") 148 flags.BoolVar(&cmd.flags.merge, "merge", false, "Merge results of multiple Staticcheck runs") 149 flags.BoolVar(&cmd.flags.matrix, "matrix", false, "Read a build config matrix from stdin") 150 151 flags.StringVar(&cmd.flags.debugCpuprofile, "debug.cpuprofile", "", "Write CPU profile to `file`") 152 flags.StringVar(&cmd.flags.debugMemprofile, "debug.memprofile", "", "Write memory profile to `file`") 153 flags.BoolVar(&cmd.flags.debugVersion, "debug.version", false, "Print detailed version information about this program") 154 flags.BoolVar(&cmd.flags.debugNoCompileErrors, "debug.no-compile-errors", false, "Don't print compile errors") 155 flags.StringVar(&cmd.flags.debugMeasureAnalyzers, "debug.measure-analyzers", "", "Write analysis measurements to `file`. `file` will be opened for appending if it already exists.") 156 flags.StringVar(&cmd.flags.debugTrace, "debug.trace", "", "Write trace to `file`") 157 158 cmd.flags.checks = list{"inherit"} 159 cmd.flags.fail = list{"all"} 160 cmd.flags.goVersion = versionFlag("module") 161 flags.Var(&cmd.flags.checks, "checks", "Comma-separated list of `checks` to enable.") 162 flags.Var(&cmd.flags.fail, "fail", "Comma-separated list of `checks` that can cause a non-zero exit status.") 163 flags.Var(&cmd.flags.goVersion, "go", "Target Go `version` in the format '1.x', or the literal 'module' to use the module's Go version") 164 } 165 166 type list []string 167 168 func (list *list) String() string { 169 return `"` + strings.Join(*list, ",") + `"` 170 } 171 172 func (list *list) Set(s string) error { 173 if s == "" { 174 *list = nil 175 return nil 176 } 177 178 elems := strings.Split(s, ",") 179 for i, elem := range elems { 180 elems[i] = strings.TrimSpace(elem) 181 } 182 *list = elems 183 return nil 184 } 185 186 type versionFlag string 187 188 func (v *versionFlag) String() string { 189 return fmt.Sprintf("%q", string(*v)) 190 } 191 192 func (v *versionFlag) Set(s string) error { 193 if s == "module" { 194 *v = "module" 195 } else { 196 var vf lint.VersionFlag 197 if err := vf.Set(s); err != nil { 198 return err 199 } 200 *v = versionFlag(s) 201 } 202 return nil 203 } 204 205 // ParseFlags parses command line flags. 206 // It must be called before calling Run. 207 // After calling ParseFlags, the values of flags can be accessed. 208 // 209 // Example: 210 // 211 // cmd.ParseFlags(os.Args[1:]) 212 func (cmd *Command) ParseFlags(args []string) { 213 cmd.flags.fs.Parse(args) 214 } 215 216 // diagnosticDescriptor represents the uniquiely identifying information of diagnostics. 217 type diagnosticDescriptor struct { 218 Position token.Position 219 End token.Position 220 Category string 221 Message string 222 } 223 224 func (diag diagnostic) descriptor() diagnosticDescriptor { 225 return diagnosticDescriptor{ 226 Position: diag.Position, 227 End: diag.End, 228 Category: diag.Category, 229 Message: diag.Message, 230 } 231 } 232 233 type run struct { 234 checkedFiles map[string]struct{} 235 diagnostics map[diagnosticDescriptor]diagnostic 236 } 237 238 func runFromLintResult(res lintResult) run { 239 out := run{ 240 checkedFiles: map[string]struct{}{}, 241 diagnostics: map[diagnosticDescriptor]diagnostic{}, 242 } 243 244 for _, cf := range res.CheckedFiles { 245 out.checkedFiles[cf] = struct{}{} 246 } 247 for _, diag := range res.Diagnostics { 248 out.diagnostics[diag.descriptor()] = diag 249 } 250 return out 251 } 252 253 func decodeGob(br io.ByteReader) ([]run, error) { 254 var runs []run 255 for { 256 var res lintResult 257 if err := gob.NewDecoder(br.(io.Reader)).Decode(&res); err != nil { 258 if err == io.EOF { 259 break 260 } else { 261 return nil, err 262 } 263 } 264 runs = append(runs, runFromLintResult(res)) 265 } 266 return runs, nil 267 } 268 269 // Run runs all registered analyzers and reports their findings. 270 // It always calls os.Exit and does not return. 271 func (cmd *Command) Run() { 272 // Set up profiling and tracing 273 if path := cmd.flags.debugCpuprofile; path != "" { 274 f, err := os.Create(path) 275 if err != nil { 276 log.Fatal(err) 277 } 278 pprof.StartCPUProfile(f) 279 } 280 if path := cmd.flags.debugTrace; path != "" { 281 f, err := os.Create(path) 282 if err != nil { 283 log.Fatal(err) 284 } 285 trace.Start(f) 286 } 287 288 // Update the default config's list of enabled checks 289 defaultChecks := []string{"all"} 290 for _, a := range cmd.analyzers { 291 if a.Doc.NonDefault { 292 defaultChecks = append(defaultChecks, "-"+a.Analyzer.Name) 293 } 294 } 295 config.DefaultConfig.Checks = defaultChecks 296 297 // Run the appropriate mode 298 var exit int 299 switch { 300 case cmd.flags.debugVersion: 301 exit = cmd.printDebugVersion() 302 case cmd.flags.listChecks: 303 exit = cmd.listChecks() 304 case cmd.flags.printVersion: 305 exit = cmd.printVersion() 306 case cmd.flags.explain != "": 307 exit = cmd.explain() 308 case cmd.flags.merge: 309 exit = cmd.merge() 310 default: 311 exit = cmd.lint() 312 } 313 314 // Stop profiling 315 if cmd.flags.debugCpuprofile != "" { 316 pprof.StopCPUProfile() 317 } 318 if path := cmd.flags.debugMemprofile; path != "" { 319 f, err := os.Create(path) 320 if err != nil { 321 panic(err) 322 } 323 runtime.GC() 324 pprof.WriteHeapProfile(f) 325 } 326 if cmd.flags.debugTrace != "" { 327 trace.Stop() 328 } 329 330 // Exit with appropriate status 331 os.Exit(exit) 332 } 333 334 func (cmd *Command) analyzersAsSlice() []*lint.Analyzer { 335 cs := make([]*lint.Analyzer, 0, len(cmd.analyzers)) 336 for _, a := range cmd.analyzers { 337 cs = append(cs, a) 338 } 339 return cs 340 } 341 342 func (cmd *Command) printDebugVersion() int { 343 version.Verbose(cmd.version, cmd.machineVersion) 344 return 0 345 } 346 347 func (cmd *Command) listChecks() int { 348 cs := cmd.analyzersAsSlice() 349 sort.Slice(cs, func(i, j int) bool { 350 return cs[i].Analyzer.Name < cs[j].Analyzer.Name 351 }) 352 for _, c := range cs { 353 var title string 354 if c.Doc != nil { 355 title = c.Doc.Title 356 } 357 fmt.Printf("%s %s\n", c.Analyzer.Name, title) 358 } 359 return 0 360 } 361 362 func (cmd *Command) printVersion() int { 363 version.Print(cmd.version, cmd.machineVersion) 364 return 0 365 } 366 367 func (cmd *Command) explain() int { 368 explain := cmd.flags.explain 369 check, ok := cmd.analyzers[explain] 370 if !ok { 371 fmt.Fprintln(os.Stderr, "Couldn't find check", explain) 372 return 1 373 } 374 if check.Analyzer.Doc == "" { 375 fmt.Fprintln(os.Stderr, explain, "has no documentation") 376 return 1 377 } 378 fmt.Println(check.Doc) 379 fmt.Println("Online documentation\n https://staticcheck.dev/docs/checks#" + check.Analyzer.Name) 380 return 0 381 } 382 383 func (cmd *Command) merge() int { 384 var runs []run 385 if len(cmd.flags.fs.Args()) == 0 { 386 var err error 387 runs, err = decodeGob(bufio.NewReader(os.Stdin)) 388 if err != nil { 389 fmt.Fprintln(os.Stderr, fmt.Errorf("couldn't parse stdin: %s", err)) 390 return 1 391 } 392 } else { 393 for _, path := range cmd.flags.fs.Args() { 394 someRuns, err := func(path string) ([]run, error) { 395 f, err := os.Open(path) 396 if err != nil { 397 return nil, err 398 } 399 defer f.Close() 400 br := bufio.NewReader(f) 401 return decodeGob(br) 402 }(path) 403 if err != nil { 404 fmt.Fprintln(os.Stderr, fmt.Errorf("couldn't parse file %s: %s", path, err)) 405 return 1 406 } 407 runs = append(runs, someRuns...) 408 } 409 } 410 411 relevantDiagnostics := mergeRuns(runs) 412 cs := cmd.analyzersAsSlice() 413 return cmd.printDiagnostics(cs, relevantDiagnostics) 414 } 415 416 func (cmd *Command) lint() int { 417 switch cmd.flags.formatter { 418 case "text", "stylish", "json", "sarif", "binary", "null": 419 default: 420 fmt.Fprintf(os.Stderr, "unsupported output format %q\n", cmd.flags.formatter) 421 return 2 422 } 423 424 var bconfs []buildConfig 425 if cmd.flags.matrix { 426 if cmd.flags.tags != "" { 427 fmt.Fprintln(os.Stderr, "cannot use -matrix and -tags together") 428 return 2 429 } 430 431 var err error 432 bconfs, err = parseBuildConfigs(os.Stdin) 433 if err != nil { 434 if perr, ok := err.(parseBuildConfigError); ok { 435 fmt.Fprintf(os.Stderr, "<stdin>:%d couldn't parse build matrix: %s\n", perr.line, perr.err) 436 } else { 437 fmt.Fprintln(os.Stderr, err) 438 } 439 return 2 440 } 441 } else { 442 bc := buildConfig{} 443 if cmd.flags.tags != "" { 444 // Validate that the tags argument is well-formed. go/packages 445 // doesn't detect malformed build flags and returns unhelpful 446 // errors. 447 tf := buildutil.TagsFlag{} 448 if err := tf.Set(cmd.flags.tags); err != nil { 449 fmt.Fprintln(os.Stderr, fmt.Errorf("invalid value %q for flag -tags: %s", cmd.flags.tags, err)) 450 return 1 451 } 452 453 bc.Flags = []string{"-tags", cmd.flags.tags} 454 } 455 bconfs = append(bconfs, bc) 456 } 457 458 var measureAnalyzers func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration) 459 if path := cmd.flags.debugMeasureAnalyzers; path != "" { 460 f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) 461 if err != nil { 462 log.Fatal(err) 463 } 464 465 mu := &sync.Mutex{} 466 measureAnalyzers = func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration) { 467 mu.Lock() 468 defer mu.Unlock() 469 // FIXME(dh): print pkg.ID 470 if _, err := fmt.Fprintf(f, "%s\t%s\t%d\n", analysis.Name, pkg, d.Nanoseconds()); err != nil { 471 log.Println("error writing analysis measurements:", err) 472 } 473 } 474 } 475 476 var runs []run 477 cs := cmd.analyzersAsSlice() 478 opts := options{ 479 analyzers: cs, 480 patterns: cmd.flags.fs.Args(), 481 lintTests: cmd.flags.tests, 482 goVersion: string(cmd.flags.goVersion), 483 config: config.Config{ 484 Checks: cmd.flags.checks, 485 }, 486 printAnalyzerMeasurement: measureAnalyzers, 487 } 488 l, err := newLinter(opts) 489 if err != nil { 490 fmt.Fprintln(os.Stderr, err) 491 return 1 492 } 493 for _, bconf := range bconfs { 494 res, err := l.run(bconf) 495 if err != nil { 496 fmt.Fprintln(os.Stderr, err) 497 return 1 498 } 499 500 for _, w := range res.Warnings { 501 fmt.Fprintln(os.Stderr, "warning:", w) 502 } 503 504 cwd, err := os.Getwd() 505 if err != nil { 506 cwd = "" 507 } 508 relPath := func(s string) string { 509 if cwd == "" { 510 return filepath.ToSlash(s) 511 } 512 out, err := filepath.Rel(cwd, s) 513 if err != nil { 514 return filepath.ToSlash(s) 515 } 516 return filepath.ToSlash(out) 517 } 518 519 if cmd.flags.formatter == "binary" { 520 for i, s := range res.CheckedFiles { 521 res.CheckedFiles[i] = relPath(s) 522 } 523 for i := range res.Diagnostics { 524 // We turn all paths into relative, /-separated paths. This is to make -merge work correctly when 525 // merging runs from different OSs, with different absolute paths. 526 // 527 // We zero out Offset, because checkouts of code on different OSs may have different kinds of 528 // newlines and thus different offsets. We don't ever make use of the Offset, anyway. Line and 529 // column numbers are precomputed. 530 531 d := &res.Diagnostics[i] 532 d.Position.Filename = relPath(d.Position.Filename) 533 d.Position.Offset = 0 534 d.End.Filename = relPath(d.End.Filename) 535 d.End.Offset = 0 536 for j := range d.Related { 537 r := &d.Related[j] 538 r.Position.Filename = relPath(r.Position.Filename) 539 r.Position.Offset = 0 540 r.End.Filename = relPath(r.End.Filename) 541 r.End.Offset = 0 542 } 543 } 544 err := gob.NewEncoder(os.Stdout).Encode(res) 545 if err != nil { 546 fmt.Fprintf(os.Stderr, "failed writing output: %s\n", err) 547 return 2 548 } 549 } else { 550 runs = append(runs, runFromLintResult(res)) 551 } 552 } 553 554 l.cache.Trim() 555 556 if cmd.flags.formatter != "binary" { 557 diags := mergeRuns(runs) 558 return cmd.printDiagnostics(cs, diags) 559 } 560 return 0 561 } 562 563 func mergeRuns(runs []run) []diagnostic { 564 var relevantDiagnostics []diagnostic 565 for _, r := range runs { 566 for _, diag := range r.diagnostics { 567 switch diag.MergeIf { 568 case lint.MergeIfAny: 569 relevantDiagnostics = append(relevantDiagnostics, diag) 570 case lint.MergeIfAll: 571 doPrint := true 572 for _, r := range runs { 573 if _, ok := r.checkedFiles[diag.Position.Filename]; ok { 574 if _, ok := r.diagnostics[diag.descriptor()]; !ok { 575 doPrint = false 576 } 577 } 578 } 579 if doPrint { 580 relevantDiagnostics = append(relevantDiagnostics, diag) 581 } 582 } 583 } 584 } 585 return relevantDiagnostics 586 } 587 588 // printDiagnostics prints the diagnostics and exits the process. 589 func (cmd *Command) printDiagnostics(cs []*lint.Analyzer, diagnostics []diagnostic) int { 590 if len(diagnostics) > 1 { 591 sort.Slice(diagnostics, func(i, j int) bool { 592 di := diagnostics[i] 593 dj := diagnostics[j] 594 pi := di.Position 595 pj := dj.Position 596 597 if pi.Filename != pj.Filename { 598 return pi.Filename < pj.Filename 599 } 600 if pi.Line != pj.Line { 601 return pi.Line < pj.Line 602 } 603 if pi.Column != pj.Column { 604 return pi.Column < pj.Column 605 } 606 if di.Message != dj.Message { 607 return di.Message < dj.Message 608 } 609 if di.BuildName != dj.BuildName { 610 return di.BuildName < dj.BuildName 611 } 612 return di.Category < dj.Category 613 }) 614 615 filtered := []diagnostic{ 616 diagnostics[0], 617 } 618 builds := []map[string]struct{}{ 619 {diagnostics[0].BuildName: {}}, 620 } 621 for _, diag := range diagnostics[1:] { 622 // We may encounter duplicate diagnostics because one file 623 // can be part of many packages, and because multiple 624 // build configurations may check the same files. 625 if !filtered[len(filtered)-1].equal(diag) { 626 if filtered[len(filtered)-1].descriptor() == diag.descriptor() { 627 // Diagnostics only differ in build name, track new name 628 builds[len(filtered)-1][diag.BuildName] = struct{}{} 629 } else { 630 filtered = append(filtered, diag) 631 builds = append(builds, map[string]struct{}{}) 632 builds[len(filtered)-1][diag.BuildName] = struct{}{} 633 } 634 } 635 } 636 637 var names []string 638 for i := range filtered { 639 names = names[:0] 640 for k := range builds[i] { 641 names = append(names, k) 642 } 643 sort.Strings(names) 644 filtered[i].BuildName = strings.Join(names, ",") 645 } 646 diagnostics = filtered 647 } 648 649 var f formatter 650 switch cmd.flags.formatter { 651 case "text": 652 f = textFormatter{W: os.Stdout} 653 case "stylish": 654 f = &stylishFormatter{W: os.Stdout} 655 case "json": 656 f = jsonFormatter{W: os.Stdout} 657 case "sarif": 658 f = &sarifFormatter{ 659 driverName: cmd.name, 660 driverVersion: cmd.version, 661 } 662 if cmd.name == "staticcheck" { 663 f.(*sarifFormatter).driverName = "Staticcheck" 664 f.(*sarifFormatter).driverWebsite = "https://staticcheck.dev" 665 } 666 case "binary": 667 fmt.Fprintln(os.Stderr, "'-f binary' not supported in this context") 668 return 2 669 case "null": 670 f = nullFormatter{} 671 default: 672 fmt.Fprintf(os.Stderr, "unsupported output format %q\n", cmd.flags.formatter) 673 return 2 674 } 675 676 fail := cmd.flags.fail 677 analyzerNames := make([]string, len(cs)) 678 for i, a := range cs { 679 analyzerNames[i] = a.Analyzer.Name 680 } 681 shouldExit := filterAnalyzerNames(analyzerNames, fail) 682 shouldExit["staticcheck"] = true 683 shouldExit["compile"] = true 684 685 var ( 686 numErrors int 687 numWarnings int 688 numIgnored int 689 ) 690 notIgnored := make([]diagnostic, 0, len(diagnostics)) 691 for _, diag := range diagnostics { 692 if diag.Category == "compile" && cmd.flags.debugNoCompileErrors { 693 continue 694 } 695 if diag.Severity == severityIgnored && !cmd.flags.showIgnored { 696 numIgnored++ 697 continue 698 } 699 if shouldExit[diag.Category] { 700 numErrors++ 701 } else { 702 diag.Severity = severityWarning 703 numWarnings++ 704 } 705 notIgnored = append(notIgnored, diag) 706 } 707 708 f.Format(cs, notIgnored) 709 if f, ok := f.(statter); ok { 710 f.Stats(len(diagnostics), numErrors, numWarnings, numIgnored) 711 } 712 713 if numErrors > 0 { 714 if _, ok := f.(*sarifFormatter); ok { 715 // When emitting SARIF, finding errors is considered success. 716 return 0 717 } else { 718 return 1 719 } 720 } 721 return 0 722 } 723 724 func usage(name string, fs *flag.FlagSet) func() { 725 return func() { 726 fmt.Fprintf(os.Stderr, "Usage: %s [flags] [packages]\n", name) 727 728 fmt.Fprintln(os.Stderr) 729 fmt.Fprintln(os.Stderr, "Flags:") 730 printDefaults(fs) 731 732 fmt.Fprintln(os.Stderr) 733 fmt.Fprintln(os.Stderr, "For help about specifying packages, see 'go help packages'") 734 } 735 } 736 737 // isZeroValue determines whether the string represents the zero 738 // value for a flag. 739 // 740 // this function has been copied from the Go standard library's 'flag' package. 741 func isZeroValue(f *flag.Flag, value string) bool { 742 // Build a zero value of the flag's Value type, and see if the 743 // result of calling its String method equals the value passed in. 744 // This works unless the Value type is itself an interface type. 745 typ := reflect.TypeOf(f.Value) 746 var z reflect.Value 747 if typ.Kind() == reflect.Ptr { 748 z = reflect.New(typ.Elem()) 749 } else { 750 z = reflect.Zero(typ) 751 } 752 return value == z.Interface().(flag.Value).String() 753 } 754 755 // this function has been copied from the Go standard library's 'flag' package and modified to skip debug flags. 756 func printDefaults(fs *flag.FlagSet) { 757 fs.VisitAll(func(f *flag.Flag) { 758 // Don't print debug flags 759 if strings.HasPrefix(f.Name, "debug.") { 760 return 761 } 762 763 var b strings.Builder 764 fmt.Fprintf(&b, " -%s", f.Name) // Two spaces before -; see next two comments. 765 name, usage := flag.UnquoteUsage(f) 766 if len(name) > 0 { 767 b.WriteString(" ") 768 b.WriteString(name) 769 } 770 // Boolean flags of one ASCII letter are so common we 771 // treat them specially, putting their usage on the same line. 772 if b.Len() <= 4 { // space, space, '-', 'x'. 773 b.WriteString("\t") 774 } else { 775 // Four spaces before the tab triggers good alignment 776 // for both 4- and 8-space tab stops. 777 b.WriteString("\n \t") 778 } 779 b.WriteString(strings.ReplaceAll(usage, "\n", "\n \t")) 780 781 if !isZeroValue(f, f.DefValue) { 782 if T := reflect.TypeOf(f.Value); T.Name() == "*stringValue" && T.PkgPath() == "flag" { 783 // put quotes on the value 784 fmt.Fprintf(&b, " (default %q)", f.DefValue) 785 } else { 786 fmt.Fprintf(&b, " (default %v)", f.DefValue) 787 } 788 } 789 fmt.Fprint(fs.Output(), b.String(), "\n") 790 }) 791 }