Skip to content

Commit 71d3d86

Browse files
committed
internal/tool: add a small package to unify the flag handling across all our tools
This adds an opinionated package that has the common code that should be in all our tools (profiling, logging, context handling etc) It also adds code for using struct fields as flags to allow us to remove all the flag globals. Change-Id: I27bb493ebcce3a86ddcdab87892a2295c237cb16 Reviewed-on: https://go-review.googlesource.com/c/154557 Reviewed-by: Rebecca Stambler <[email protected]>
1 parent ae5b881 commit 71d3d86

File tree

1 file changed

+196
-0
lines changed

1 file changed

+196
-0
lines changed

internal/tool/tool.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// Copyright 2018 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 tool is an opinionated harness for writing Go tools.
6+
package tool
7+
8+
import (
9+
"context"
10+
"flag"
11+
"fmt"
12+
"log"
13+
"os"
14+
"reflect"
15+
"runtime"
16+
"runtime/pprof"
17+
"runtime/trace"
18+
"time"
19+
)
20+
21+
// This file is a very opinionated harness for writing your main function.
22+
// The original version of the file is in golang.org/x/tools/internal/tool.
23+
//
24+
// It adds a method to the Application type
25+
// Main(name, usage string, args []string)
26+
// which should normally be invoked from a true main as follows:
27+
// func main() {
28+
// (&Application{}).Main("myapp", "non-flag-command-line-arg-help", os.Args[1:])
29+
// }
30+
// It recursively scans the application object for fields with a tag containing
31+
// `flag:"flagname" help:"short help text"``
32+
// uses all those fields to build command line flags.
33+
// It expects the Application type to have a method
34+
// Run(context.Context, args...string) error
35+
// which it invokes only after all command line flag processing has been finished.
36+
// If Run returns an error, the error will be printed to stderr and the
37+
// application will quit with a non zero exit status.
38+
39+
// Profile can be embedded in your application struct to automatically
40+
// add command line arguments and handling for the common profiling methods.
41+
type Profile struct {
42+
CPU string `flag:"profile.cpu" help:"write CPU profile to this file"`
43+
Memory string `flag:"profile.mem" help:"write memory profile to this file"`
44+
Trace string `flag:"profile.trace" help:"write trace log to this file"`
45+
}
46+
47+
// Application is the interface that must be satisfied by an object passed to Main.
48+
type Application interface {
49+
// Name returns the application's name. It is used in help and error messages.
50+
Name() string
51+
// Most of the help usage is automatically generated, this string should only
52+
// describe the contents of non flag arguments.
53+
Usage() string
54+
// ShortHelp returns the one line overview of the command.
55+
ShortHelp() string
56+
// DetailedHelp should print a detailed help message. It will only ever be shown
57+
// when the ShortHelp is also printed, so there is no need to duplicate
58+
// anything from there.
59+
// It is passed the flag set so it can print the default values of the flags.
60+
// It should use the flag sets configured Output to write the help to.
61+
DetailedHelp(*flag.FlagSet)
62+
// Run is invoked after all flag processing, and inside the profiling and
63+
// error handling harness.
64+
Run(ctx context.Context, args ...string) error
65+
}
66+
67+
// This is the type returned by CommandLineErrorf, which causes the outer main
68+
// to trigger printing of the command line help.
69+
type commandLineError string
70+
71+
func (e commandLineError) Error() string { return string(e) }
72+
73+
// CommandLineErrorf is like fmt.Errorf except that it returns a value that
74+
// triggers printing of the command line help.
75+
// In general you should use this when generating command line validation errors.
76+
func CommandLineErrorf(message string, args ...interface{}) error {
77+
return commandLineError(fmt.Sprintf(message, args...))
78+
}
79+
80+
// Main should be invoked directly by main function.
81+
// It will only return if there was no error.
82+
func Main(ctx context.Context, app Application, args []string) {
83+
s := flag.NewFlagSet(app.Name(), flag.ExitOnError)
84+
s.Usage = func() {
85+
fmt.Fprint(s.Output(), app.ShortHelp())
86+
fmt.Fprintf(s.Output(), "\n\nUsage: %v [flags] %v\n", app.Name(), app.Usage())
87+
app.DetailedHelp(s)
88+
}
89+
p := addFlags(s, reflect.StructField{}, reflect.ValueOf(app))
90+
s.Parse(args)
91+
err := func() error {
92+
if p != nil && p.CPU != "" {
93+
f, err := os.Create(p.CPU)
94+
if err != nil {
95+
return err
96+
}
97+
if err := pprof.StartCPUProfile(f); err != nil {
98+
return err
99+
}
100+
defer pprof.StopCPUProfile()
101+
}
102+
103+
if p != nil && p.Trace != "" {
104+
f, err := os.Create(p.Trace)
105+
if err != nil {
106+
return err
107+
}
108+
if err := trace.Start(f); err != nil {
109+
return err
110+
}
111+
defer func() {
112+
trace.Stop()
113+
log.Printf("To view the trace, run:\n$ go tool trace view %s", p.Trace)
114+
}()
115+
}
116+
117+
if p != nil && p.Memory != "" {
118+
f, err := os.Create(p.Memory)
119+
if err != nil {
120+
return err
121+
}
122+
defer func() {
123+
runtime.GC() // get up-to-date statistics
124+
if err := pprof.WriteHeapProfile(f); err != nil {
125+
log.Printf("Writing memory profile: %v", err)
126+
}
127+
f.Close()
128+
}()
129+
}
130+
return app.Run(ctx, s.Args()...)
131+
}()
132+
if err != nil {
133+
fmt.Fprintf(s.Output(), "%s: %v\n", app.Name(), err)
134+
if _, printHelp := err.(commandLineError); printHelp {
135+
s.Usage()
136+
}
137+
os.Exit(2)
138+
}
139+
}
140+
141+
// addFlags scans fields of structs recursively to find things with flag tags
142+
// and add them to the flag set.
143+
func addFlags(f *flag.FlagSet, field reflect.StructField, value reflect.Value) *Profile {
144+
// is it a field we are allowed to reflect on?
145+
if field.PkgPath != "" {
146+
return nil
147+
}
148+
// now see if is actually a flag
149+
flagName, isFlag := field.Tag.Lookup("flag")
150+
help := field.Tag.Get("help")
151+
if !isFlag {
152+
// not a flag, but it might be a struct with flags in it
153+
if value.Elem().Kind() != reflect.Struct {
154+
return nil
155+
}
156+
p, _ := value.Interface().(*Profile)
157+
// go through all the fields of the struct
158+
sv := value.Elem()
159+
for i := 0; i < sv.Type().NumField(); i++ {
160+
child := sv.Type().Field(i)
161+
v := sv.Field(i)
162+
// make sure we have a pointer
163+
if v.Kind() != reflect.Ptr {
164+
v = v.Addr()
165+
}
166+
// check if that field is a flag or contains flags
167+
if fp := addFlags(f, child, v); fp != nil {
168+
p = fp
169+
}
170+
}
171+
return p
172+
}
173+
switch v := value.Interface().(type) {
174+
case flag.Value:
175+
f.Var(v, flagName, help)
176+
case *bool:
177+
f.BoolVar(v, flagName, *v, help)
178+
case *time.Duration:
179+
f.DurationVar(v, flagName, *v, help)
180+
case *float64:
181+
f.Float64Var(v, flagName, *v, help)
182+
case *int64:
183+
f.Int64Var(v, flagName, *v, help)
184+
case *int:
185+
f.IntVar(v, flagName, *v, help)
186+
case *string:
187+
f.StringVar(v, flagName, *v, help)
188+
case *uint:
189+
f.UintVar(v, flagName, *v, help)
190+
case *uint64:
191+
f.Uint64Var(v, flagName, *v, help)
192+
default:
193+
log.Fatalf("Cannot understand flag of type %T", v)
194+
}
195+
return nil
196+
}

0 commit comments

Comments
 (0)