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