| // Copyright 2022 The Dawn & Tint Authors | 
 | // | 
 | // Redistribution and use in source and binary forms, with or without | 
 | // modification, are permitted provided that the following conditions are met: | 
 | // | 
 | // 1. Redistributions of source code must retain the above copyright notice, this | 
 | //    list of conditions and the following disclaimer. | 
 | // | 
 | // 2. Redistributions in binary form must reproduce the above copyright notice, | 
 | //    this list of conditions and the following disclaimer in the documentation | 
 | //    and/or other materials provided with the distribution. | 
 | // | 
 | // 3. Neither the name of the copyright holder nor the names of its | 
 | //    contributors may be used to endorse or promote products derived from | 
 | //    this software without specific prior written permission. | 
 | // | 
 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | 
 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | 
 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | 
 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | 
 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | 
 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | 
 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | 
 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | 
 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 
 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 
 |  | 
 | // 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 | 
 | } | 
 |  | 
 | // DefaultCommand is the interface for a command that should be used as the | 
 | // default if no command is specified on the command line. Only one command can | 
 | // be the default command. | 
 | type DefaultCommand interface { | 
 | 	IsDefaultCommand() | 
 | } | 
 |  | 
 | // 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 { | 
 | 			name := cmd.Name() | 
 | 			_, isDefault := cmd.(DefaultCommand) | 
 | 			if isDefault { | 
 | 				name += " (default)" | 
 | 			} | 
 | 			fmt.Fprintln(tw, "  ", name, "\t-", strings.Split(cmd.Desc(), "\n")[0]) | 
 | 		} | 
 | 		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) | 
 |  | 
 | 	// Look for a default command | 
 | 	var defaultCmd Command[Data] | 
 | 	for _, cmd := range cmds { | 
 | 		_, isDefault := cmd.(DefaultCommand) | 
 | 		if isDefault { | 
 | 			if defaultCmd != nil { | 
 | 				panic(fmt.Sprintf("both commands %v and %v implement DefaultCommand", defaultCmd.Name(), cmd.Name())) | 
 | 			} | 
 | 			defaultCmd = cmd | 
 | 		} | 
 | 	} | 
 |  | 
 | 	var cmdName string | 
 | 	if len(os.Args) > 1 { | 
 | 		cmdName = os.Args[1] | 
 | 	} | 
 |  | 
 | 	args := os.Args[1:] // Skip executable name | 
 |  | 
 | 	// Find the requested command | 
 | 	cmd := findCmd(cmdName, cmds) | 
 | 	if cmd != nil { | 
 | 		args = args[1:] // Skip command name | 
 | 	} | 
 |  | 
 | 	help := strings.TrimLeft(cmdName, "-") == "help" | 
 | 	if help { | 
 | 		if len(args) > 1 { // help cmd ... | 
 | 			cmdName = args[1] | 
 | 			cmd = findCmd(cmdName, cmds) | 
 | 		} | 
 | 		if cmd == nil { | 
 | 			flag.Usage() | 
 | 			return nil | 
 | 		} | 
 | 	} | 
 |  | 
 | 	if cmd == nil { // Command not found | 
 | 		if defaultCmd == nil { | 
 | 			return InvalidCLA() // No default | 
 | 		} | 
 | 		cmd = defaultCmd // Use default command | 
 | 	} | 
 |  | 
 | 	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 | 
 | 	} | 
 | 	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) | 
 | } | 
 |  | 
 | func findCmd[Data any](name string, cmds []Command[Data]) Command[Data] { | 
 | 	for _, c := range cmds { | 
 | 		if c.Name() == name { | 
 | 			return c | 
 | 		} | 
 | 	} | 
 | 	return nil | 
 | } |