isle/go/cmd/entrypoint/sub_cmd.go

240 lines
5.2 KiB
Go
Raw Normal View History

package main
import (
2022-10-26 22:37:03 +00:00
"context"
"errors"
"fmt"
"isle/daemon"
"isle/jsonutil"
"os"
"strings"
2024-06-22 15:49:56 +00:00
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
"github.com/spf13/pflag"
"gopkg.in/yaml.v3"
)
type flagSet struct {
*pflag.FlagSet
2022-10-26 22:37:03 +00:00
network string
logLevel logLevelFlag
}
type subCmd struct {
2024-07-12 14:13:44 +00:00
name string
descr string
do func(subCmdCtx) error
2024-07-22 13:52:51 +00:00
// If set then the name will be allowed to be suffixed with this string.
plural string
// noNetwork, if true, means the call doesn't require a network to be
// specified on the command-line if there are more than one networks
// configured.
noNetwork bool
// Extra arguments on the command-line will be passed through to some
// underlying command.
passthroughArgs bool
}
// subCmdCtx contains all information available to a subCmd's do method.
type subCmdCtx struct {
context.Context
subCmd subCmd // the subCmd itself
args []string // command-line arguments, excluding the subCmd itself.
subCmdNames []string // names of subCmds so far, including this one
flags *flagSet
}
func newSubCmdCtx(
ctx context.Context,
subCmd subCmd,
args []string,
subCmdNames []string,
) subCmdCtx {
flags := pflag.NewFlagSet(subCmd.name, pflag.ExitOnError)
flags.Usage = func() {
var passthroughStr string
if subCmd.passthroughArgs {
passthroughStr = " [--] [args...]"
}
fmt.Fprintf(
os.Stderr, "%s[-h|--help] [%s flags...]%s\n\n",
usagePrefix(subCmdNames), subCmd.name, passthroughStr,
)
fmt.Fprintf(os.Stderr, "%s FLAGS:\n\n", strings.ToUpper(subCmd.name))
fmt.Fprintln(os.Stderr, flags.FlagUsages())
os.Stderr.Sync()
os.Exit(2)
}
fs := &flagSet{
FlagSet: flags,
logLevel: logLevelFlag{mlog.LevelInfo},
}
if !subCmd.noNetwork {
fs.FlagSet.StringVar(
&fs.network, "network", "", "Which network to perform the command against, if more than one is joined. Can be an ID, name, or domain.",
)
}
fs.FlagSet.VarP(
&fs.logLevel,
"log-level", "l",
"Maximum log level to output. Can be DEBUG, CHILD, INFO, WARN, ERROR, or FATAL.",
)
return subCmdCtx{
Context: ctx,
subCmd: subCmd,
args: args,
subCmdNames: subCmdNames,
flags: fs,
}
}
func usagePrefix(subCmdNames []string) string {
subCmdNamesStr := strings.Join(subCmdNames, " ")
if subCmdNamesStr != "" {
subCmdNamesStr += " "
}
return fmt.Sprintf("\nUSAGE: %s %s", os.Args[0], subCmdNamesStr)
}
func (ctx subCmdCtx) logger() *mlog.Logger {
return mlog.NewLogger(&mlog.LoggerOpts{
MaxLevel: ctx.flags.logLevel.Int(),
})
}
func (ctx subCmdCtx) withParsedFlags() (subCmdCtx, error) {
ctx.flags.VisitAll(func(f *pflag.Flag) {
if f.Shorthand == "h" {
panic(fmt.Sprintf("flag %+v has reserved shorthand `-h`", f))
}
if f.Name == "help" {
panic(fmt.Sprintf("flag %+v has reserved name `--help`", f))
}
})
if err := ctx.flags.Parse(ctx.args); err != nil {
return ctx, err
}
ctx.Context = daemon.WithNetwork(ctx.Context, ctx.flags.network)
return ctx, nil
}
func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
printUsageExit := func(subCmdName string) {
fmt.Fprintf(os.Stderr, "unknown sub-command %q\n", subCmdName)
fmt.Fprintf(
os.Stderr,
"%s<subCmd> [-h|--help] [sub-command flags...]\n",
usagePrefix(ctx.subCmdNames),
)
fmt.Fprintf(os.Stderr, "\nSUB-COMMANDS:\n\n")
for _, subCmd := range subCmds {
2024-07-22 13:52:51 +00:00
name := subCmd.name
if subCmd.plural != "" {
name += "(" + subCmd.plural + ")"
}
fmt.Fprintf(os.Stderr, " %s\t%s\n", name, subCmd.descr)
}
fmt.Fprintf(os.Stderr, "\n")
os.Stderr.Sync()
os.Exit(2)
}
args := ctx.args
if len(args) == 0 {
printUsageExit("")
}
subCmdsMap := map[string]subCmd{}
for _, subCmd := range subCmds {
subCmdsMap[subCmd.name] = subCmd
2024-07-22 13:52:51 +00:00
if subCmd.plural != "" {
subCmdsMap[subCmd.name+subCmd.plural] = subCmd
}
}
subCmdName, args := args[0], args[1:]
subCmd, ok := subCmdsMap[subCmdName]
if !ok {
printUsageExit(subCmdName)
}
nextSubCmdCtx := newSubCmdCtx(
ctx.Context,
subCmd,
args,
append(ctx.subCmdNames, subCmdName),
)
if err := subCmd.do(nextSubCmdCtx); err != nil {
return err
}
return nil
}
type outputFormat string
func (f outputFormat) MarshalText() ([]byte, error) { return []byte(f), nil }
func (f *outputFormat) UnmarshalText(b []byte) error {
*f = outputFormat(strings.ToLower(string(b)))
switch *f {
case "json", "yaml":
return nil
default:
return errors.New("invalid output format")
}
}
// doWithOutput wraps a subCmd's do function so that it will output some value
// to stdout. The value will be formatted according to a command-line argument.
func doWithOutput(fn func(subCmdCtx) (any, error)) func(subCmdCtx) error {
return func(ctx subCmdCtx) error {
type outputFormatFlag = textUnmarshalerFlag[outputFormat, *outputFormat]
outputFormat := outputFormatFlag{"yaml"}
ctx.flags.Var(
&outputFormat,
"format",
"How to format the output value. Can be 'json' or 'yaml'.",
)
res, err := fn(ctx)
if err != nil {
return err
}
switch outputFormat.V {
case "json":
return jsonutil.WriteIndented(os.Stdout, res)
case "yaml":
return yaml.NewEncoder(os.Stdout).Encode(res)
default:
panic(fmt.Sprintf("unexpected outputFormat %q", outputFormat))
}
}
}