1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: Apache-2.0
5 // Package cmd helps define reusable functions that can be exposed as
6 // [subcommands of] command line programs.
21 "github.com/sirupsen/logrus"
24 type Handler interface {
25 RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int
28 type HandlerFunc func(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int
30 func (f HandlerFunc) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
31 return f(prog, args, stdin, stdout, stderr)
34 // Version is a Handler that prints the package version (set at build
35 // time using -ldflags) and Go runtime version to stdout, and returns
37 var Version versionCommand
40 // These default version/commit strings should be set at build
41 // time: `go install -buildvcs=false -ldflags "-X
42 // git.arvados.org/arvados.git/lib/cmd.version=1.2.3"`
44 commit = "0000000000000000000000000000000000000000"
47 type versionCommand struct{}
49 func (versionCommand) String() string {
50 return fmt.Sprintf("%s (%s)", version, runtime.Version())
53 func (versionCommand) Commit() string {
54 if bi, ok := debug.ReadBuildInfo(); ok {
55 for _, bs := range bi.Settings {
56 if bs.Key == "vcs.revision" {
64 func (versionCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
65 prog = regexp.MustCompile(` -*version$`).ReplaceAllLiteralString(prog, "")
66 fmt.Fprintf(stdout, "%s %s (%s)\n", prog, version, runtime.Version())
70 // Multi is a Handler that looks up its first argument in a map (after
71 // stripping any "arvados-" or "crunch-" prefix), and invokes the
72 // resulting Handler with the remaining args.
76 // os.Exit(Multi(map[string]Handler{
77 // "foobar": HandlerFunc(func(prog string, args []string) int {
78 // fmt.Println(args[0])
81 // })("/usr/bin/multi", []string{"foobar", "baz"}, os.Stdin, os.Stdout, os.Stderr))
83 // ...prints "baz" and exits 2.
84 type Multi map[string]Handler
86 func (m Multi) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
87 _, basename := filepath.Split(prog)
88 if i := strings.Index(basename, "~"); i >= 0 {
89 // drop "~anything" suffix (arvados-dispatch-cloud's
90 // DeployRunnerBinary feature relies on this)
91 basename = basename[:i]
93 cmd, ok := m[basename]
95 // "controller" command exists, and binary is named "arvados-controller"
96 cmd, ok = m[strings.TrimPrefix(basename, "arvados-")]
99 // "dispatch-slurm" command exists, and binary is named "crunch-dispatch-slurm"
100 cmd, ok = m[strings.TrimPrefix(basename, "crunch-")]
103 return cmd.RunCommand(prog, args, stdin, stdout, stderr)
104 } else if len(args) < 1 {
105 fmt.Fprintf(stderr, "usage: %s command [args]\n", prog)
108 } else if cmd, ok = m[args[0]]; ok {
109 return cmd.RunCommand(prog+" "+args[0], args[1:], stdin, stdout, stderr)
111 fmt.Fprintf(stderr, "%s: unrecognized command %q\n", prog, args[0])
117 func (m Multi) Usage(stderr io.Writer) {
118 fmt.Fprintf(stderr, "\nAvailable commands:\n")
119 m.listSubcommands(stderr, "")
122 func (m Multi) listSubcommands(out io.Writer, prefix string) {
123 var subcommands []string
125 if strings.HasPrefix(sc, "-") {
126 // Some subcommands have alternate versions
127 // like "--version" for compatibility. Don't
128 // clutter the subcommand summary with those.
131 subcommands = append(subcommands, sc)
133 sort.Strings(subcommands)
134 for _, sc := range subcommands {
135 switch cmd := m[sc].(type) {
137 cmd.listSubcommands(out, prefix+sc+" ")
139 fmt.Fprintf(out, " %s%s\n", prefix, sc)
144 type FlagSet interface {
145 Init(string, flag.ErrorHandling)
148 Parse([]string) error
153 // SubcommandToFront silently parses args using flagset, and returns a
154 // copy of args with the first non-flag argument moved to the
155 // front. If parsing fails or consumes all of args, args is returned
158 // SubcommandToFront invokes methods on flagset that have side
159 // effects, including Parse. In typical usage, flagset will not used
160 // for anything else after being passed to SubcommandToFront.
161 func SubcommandToFront(args []string, flagset FlagSet) []string {
162 flagset.Init("", flag.ContinueOnError)
163 flagset.SetOutput(ioutil.Discard)
164 if err := flagset.Parse(args); err != nil || flagset.NArg() == 0 {
165 // No subcommand found.
168 // Move subcommand to the front.
169 flagargs := len(args) - flagset.NArg()
170 newargs := make([]string, len(args))
171 newargs[0] = args[flagargs]
172 copy(newargs[1:flagargs+1], args[:flagargs])
173 copy(newargs[flagargs+1:], args[flagargs+1:])
177 type NoPrefixFormatter struct{}
179 func (NoPrefixFormatter) Format(entry *logrus.Entry) ([]byte, error) {
180 return []byte(entry.Message + "\n"), nil