tools: Add 'subcmd' package

Helper for writing command line tools that have sub-commands.
Will be used by the CTS tool (roll, results, export, etc).

Bug: dawn:1342
Change-Id: Id22beecbc02b73af23064adc67c590f825b37789
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/88315
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
This commit is contained in:
Ben Clayton 2022-04-29 16:30:24 +00:00 committed by Dawn LUCI CQ
parent b909d5554d
commit 15a85dec7e
1 changed files with 131 additions and 0 deletions

131
tools/src/subcmd/subcmd.go Normal file
View File

@ -0,0 +1,131 @@
// 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()
}