//
// 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 (
"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 RunFunc that looks up its first argument in m, and
-// invokes 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"}))
//
// ...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]\n", prog)
- multiUsage(stderr, m)
- return 2
- }
- if cmd, ok := m[args[0]]; !ok {
- fmt.Fprintf(stderr, "unrecognized command %q\n", args[0])
- multiUsage(stderr, m)
- return 2
- } else {
- return cmd(prog+" "+args[0], args[1:], stdin, stdout, stderr)
- }
+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 multiUsage(stderr io.Writer, m map[string]RunFunc) {
+func (m Multi) listSubcommands(out io.Writer, prefix string) {
var subcommands []string
for sc := range m {
if strings.HasPrefix(sc, "-") {
subcommands = append(subcommands, sc)
}
sort.Strings(subcommands)
- fmt.Fprintf(stderr, "\nAvailable commands:\n")
for _, sc := range subcommands {
- fmt.Fprintf(stderr, " %s\n", sc)
+ 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.SetOutput(ioutil.Discard)
- flags.Usage = func() {}
- 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
}