| // Copyright 2022 The Dawn Authors | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| // Package subcmd provides a multi-command interface for command line tools. | |
| package subcmd | |
| import ( | |
| "context" | |
| "errors" | |
| "flag" | |
| "fmt" | |
| "net/http" | |
| "net/http/pprof" | |
| "os" | |
| "path/filepath" | |
| "strings" | |
| "text/tabwriter" | |
| ) | |
| // ErrInvalidCLA is the error returned when an invalid command line argument was | |
| // provided, and the usage was already printed. | |
| var ErrInvalidCLA = errors.New("invalid command line args") | |
| // InvalidCLA shows the flag usage, and returns ErrInvalidCLA | |
| func InvalidCLA() error { | |
| flag.Usage() | |
| return ErrInvalidCLA | |
| } | |
| // Command is the interface for a command | |
| // Data is a generic data type passed down to the sub-command when run. | |
| type Command[Data any] interface { | |
| // Name returns the name of the command. | |
| Name() string | |
| // Desc returns a description of the command. | |
| Desc() string | |
| // RegisterFlags registers all the command-specific flags | |
| // Returns a list of mandatory arguments that must immediately follow the | |
| // command name | |
| RegisterFlags(context.Context, Data) ([]string, error) | |
| // Run invokes the command | |
| Run(context.Context, Data) error | |
| } | |
| // Run handles the parses the command line arguments, possibly invoking one of | |
| // the provided commands. | |
| // If the command line arguments are invalid, then an error message is printed | |
| // and Run returns ErrInvalidCLA. | |
| func Run[Data any](ctx context.Context, data Data, cmds ...Command[Data]) error { | |
| _, exe := filepath.Split(os.Args[0]) | |
| flag.Usage = func() { | |
| out := flag.CommandLine.Output() | |
| tw := tabwriter.NewWriter(out, 0, 1, 0, ' ', 0) | |
| fmt.Fprintln(tw, exe, "[command]") | |
| fmt.Fprintln(tw) | |
| fmt.Fprintln(tw, "Commands:") | |
| for _, cmd := range cmds { | |
| fmt.Fprintln(tw, " ", cmd.Name(), "\t-", cmd.Desc()) | |
| } | |
| fmt.Fprintln(tw) | |
| fmt.Fprintln(tw, "Common flags:") | |
| tw.Flush() | |
| flag.PrintDefaults() | |
| } | |
| profile := false | |
| flag.BoolVar(&profile, "profile", false, "enable a webserver at localhost:8080/profile that exposes a CPU profiler") | |
| mux := http.NewServeMux() | |
| mux.HandleFunc("/profile", pprof.Profile) | |
| if len(os.Args) < 2 { | |
| return InvalidCLA() | |
| } | |
| help := os.Args[1] == "help" | |
| if help { | |
| copy(os.Args[1:], os.Args[2:]) | |
| os.Args = os.Args[:len(os.Args)-1] | |
| } | |
| for _, cmd := range cmds { | |
| if cmd.Name() == os.Args[1] { | |
| out := flag.CommandLine.Output() | |
| mandatory, err := cmd.RegisterFlags(ctx, data) | |
| if err != nil { | |
| return err | |
| } | |
| flag.Usage = func() { | |
| flagsAndArgs := append([]string{"<flags>"}, mandatory...) | |
| fmt.Fprintln(out, exe, cmd.Name(), strings.Join(flagsAndArgs, " ")) | |
| fmt.Fprintln(out) | |
| fmt.Fprintln(out, cmd.Desc()) | |
| fmt.Fprintln(out) | |
| fmt.Fprintln(out, "flags:") | |
| flag.PrintDefaults() | |
| } | |
| if help { | |
| flag.Usage() | |
| return nil | |
| } | |
| args := os.Args[2:] // all arguments after the exe and command | |
| if err := flag.CommandLine.Parse(args); err != nil { | |
| return err | |
| } | |
| if nonFlagArgs := flag.Args(); len(nonFlagArgs) < len(mandatory) { | |
| fmt.Fprintln(out, "missing argument", mandatory[len(nonFlagArgs)]) | |
| fmt.Fprintln(out) | |
| return InvalidCLA() | |
| } | |
| if profile { | |
| fmt.Println("download profile at: localhost:8080/profile") | |
| fmt.Println("then run: 'go tool pprof <file>") | |
| go http.ListenAndServe(":8080", mux) | |
| } | |
| return cmd.Run(ctx, data) | |
| } | |
| } | |
| return InvalidCLA() | |
| } |