X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/243f15e69ed3a5514be89115d3ec231551d31cb8..13f6d45704efc68ca8419e8917376aa44fdee1be:/lib/cmd/cmd.go diff --git a/lib/cmd/cmd.go b/lib/cmd/cmd.go index a3d5ae8767..a03cb90f68 100644 --- a/lib/cmd/cmd.go +++ b/lib/cmd/cmd.go @@ -2,77 +2,162 @@ // // SPDX-License-Identifier: Apache-2.0 -// package cmd defines a RunFunc type, representing a process that can -// be invoked from a command line. +// Package cmd helps define reusable functions that can be exposed as +// [subcommands of] command line programs. package cmd import ( "flag" "fmt" "io" + "io/ioutil" + "path/filepath" + "regexp" + "runtime" + "sort" + "strings" + + "github.com/sirupsen/logrus" ) -// A RunFunc runs a command with the given args, and returns an exit -// code. -type RunFunc func(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int +type Handler interface { + RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int +} + +type HandlerFunc func(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int + +func (f HandlerFunc) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int { + return f(prog, args, stdin, stdout, stderr) +} + +// Version is a Handler that prints the package version (set at build +// time using -ldflags) and Go runtime version to stdout, and returns +// 0. +var Version versionCommand + +var version = "dev" + +type versionCommand struct{} -// Multi returns a command that looks up its first argument in m, and -// runs the resulting RunFunc with the remaining args. +func (versionCommand) String() string { + return fmt.Sprintf("%s (%s)", version, runtime.Version()) +} + +func (versionCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int { + prog = regexp.MustCompile(` -*version$`).ReplaceAllLiteralString(prog, "") + fmt.Fprintf(stdout, "%s %s (%s)\n", prog, version, runtime.Version()) + return 0 +} + +// Multi is a Handler that looks up its first argument in a map (after +// stripping any "arvados-" or "crunch-" prefix), and invokes the +// resulting Handler with the remaining args. // // Example: // -// os.Exit(Multi(map[string]RunFunc{ -// "foobar": func(prog string, args []string) int { +// os.Exit(Multi(map[string]Handler{ +// "foobar": HandlerFunc(func(prog string, args []string) int { // fmt.Println(args[0]) // return 2 -// }, -// })("/usr/bin/multi", []string{"foobar", "baz"})) +// }), +// })("/usr/bin/multi", []string{"foobar", "baz"}, os.Stdin, os.Stdout, os.Stderr)) // // ...prints "baz" and exits 2. -func Multi(m map[string]RunFunc) RunFunc { - return func(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int { - if len(args) < 1 { - fmt.Fprintf(stderr, "usage: %s command [args]", prog) - return 2 +type Multi map[string]Handler + +func (m Multi) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int { + _, basename := filepath.Split(prog) + if i := strings.Index(basename, "~"); i >= 0 { + // drop "~anything" suffix (arvados-dispatch-cloud's + // DeployRunnerBinary feature relies on this) + basename = basename[:i] + } + cmd, ok := m[basename] + if !ok { + // "controller" command exists, and binary is named "arvados-controller" + cmd, ok = m[strings.TrimPrefix(basename, "arvados-")] + } + if !ok { + // "dispatch-slurm" command exists, and binary is named "crunch-dispatch-slurm" + cmd, ok = m[strings.TrimPrefix(basename, "crunch-")] + } + if ok { + return cmd.RunCommand(prog, args, stdin, stdout, stderr) + } else if len(args) < 1 { + fmt.Fprintf(stderr, "usage: %s command [args]\n", prog) + m.Usage(stderr) + return 2 + } else if cmd, ok = m[args[0]]; ok { + return cmd.RunCommand(prog+" "+args[0], args[1:], stdin, stdout, stderr) + } else { + fmt.Fprintf(stderr, "%s: unrecognized command %q\n", prog, args[0]) + m.Usage(stderr) + return 2 + } +} + +func (m Multi) Usage(stderr io.Writer) { + fmt.Fprintf(stderr, "\nAvailable commands:\n") + m.listSubcommands(stderr, "") +} + +func (m Multi) listSubcommands(out io.Writer, prefix string) { + var subcommands []string + for sc := range m { + if strings.HasPrefix(sc, "-") { + // Some subcommands have alternate versions + // like "--version" for compatibility. Don't + // clutter the subcommand summary with those. + continue } - if cmd, ok := m[args[0]]; !ok { - fmt.Fprintf(stderr, "unrecognized command %q", args[0]) - return 2 - } else { - return cmd(prog+" "+args[0], args[1:], stdin, stdout, stderr) + subcommands = append(subcommands, sc) + } + sort.Strings(subcommands) + for _, sc := range subcommands { + switch cmd := m[sc].(type) { + case Multi: + cmd.listSubcommands(out, prefix+sc+" ") + default: + fmt.Fprintf(out, " %s%s\n", prefix, sc) } } } -// WithLateSubcommand wraps a RunFunc by skipping over some known -// flags to find a subcommand, and moving that subcommand to the front -// of the args before calling the wrapped RunFunc. For example: +type FlagSet interface { + Init(string, flag.ErrorHandling) + Args() []string + NArg() int + Parse([]string) error + SetOutput(io.Writer) + PrintDefaults() +} + +// SubcommandToFront silently parses args using flagset, and returns a +// copy of args with the first non-flag argument moved to the +// front. If parsing fails or consumes all of args, args is returned +// unchanged. // -// // Translate [ --format foo subcommand bar] -// // to [subcommand --format foo bar] -// WithLateSubcommand(fn, []string{"format"}, nil) -func WithLateSubcommand(run RunFunc, argFlags, boolFlags []string) RunFunc { - return func(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int { - flags := flag.NewFlagSet("prog", flag.ContinueOnError) - for _, arg := range argFlags { - flags.String(arg, "", "") - } - for _, arg := range boolFlags { - flags.Bool(arg, false, "") - } - // Ignore errors. We can't report a useful error - // message anyway. - flags.Parse(args) - if flags.NArg() > 0 { - // Move the first arg after the recognized - // flags up to the front. - flagargs := len(args) - flags.NArg() - newargs := make([]string, len(args)) - newargs[0] = args[flagargs] - copy(newargs[1:flagargs+1], args[:flagargs]) - copy(newargs[flagargs+1:], args[flagargs+1:]) - args = newargs - } - return run(prog, args, stdin, stdout, stderr) +// SubcommandToFront invokes methods on flagset that have side +// effects, including Parse. In typical usage, flagset will not used +// for anything else after being passed to SubcommandToFront. +func SubcommandToFront(args []string, flagset FlagSet) []string { + flagset.Init("", flag.ContinueOnError) + flagset.SetOutput(ioutil.Discard) + if err := flagset.Parse(args); err != nil || flagset.NArg() == 0 { + // No subcommand found. + return args } + // Move subcommand to the front. + flagargs := len(args) - flagset.NArg() + newargs := make([]string, len(args)) + newargs[0] = args[flagargs] + copy(newargs[1:flagargs+1], args[:flagargs]) + copy(newargs[flagargs+1:], args[flagargs+1:]) + return newargs +} + +type NoPrefixFormatter struct{} + +func (NoPrefixFormatter) Format(entry *logrus.Entry) ([]byte, error) { + return []byte(entry.Message + "\n"), nil }