blob: 35ec87887777be94b5bd06bea781f9b2b137706f [file] [log] [blame]
// 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
}
// 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()
}