Merge branch '22132-scheduling-status-messages' refs #22132
[arvados.git] / lib / cmd / cmd.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: Apache-2.0
4
5 // Package cmd helps define reusable functions that can be exposed as
6 // [subcommands of] command line programs.
7 package cmd
8
9 import (
10         "flag"
11         "fmt"
12         "io"
13         "io/ioutil"
14         "path/filepath"
15         "regexp"
16         "runtime"
17         "runtime/debug"
18         "sort"
19         "strings"
20
21         "github.com/sirupsen/logrus"
22 )
23
24 const EXIT_INVALIDARGUMENT = 2
25
26 type Handler interface {
27         RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int
28 }
29
30 type HandlerFunc func(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int
31
32 func (f HandlerFunc) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
33         return f(prog, args, stdin, stdout, stderr)
34 }
35
36 // Version is a Handler that prints the package version (set at build
37 // time using -ldflags) and Go runtime version to stdout, and returns
38 // 0.
39 var Version versionCommand
40
41 var (
42         // These default version/commit strings should be set at build
43         // time: `go install -buildvcs=false -ldflags "-X
44         // git.arvados.org/arvados.git/lib/cmd.version=1.2.3"`
45         version = "dev"
46         commit  = "0000000000000000000000000000000000000000"
47 )
48
49 type versionCommand struct{}
50
51 func (versionCommand) String() string {
52         return fmt.Sprintf("%s (%s)", version, runtime.Version())
53 }
54
55 func (versionCommand) Commit() string {
56         if bi, ok := debug.ReadBuildInfo(); ok {
57                 for _, bs := range bi.Settings {
58                         if bs.Key == "vcs.revision" {
59                                 return bs.Value
60                         }
61                 }
62         }
63         return commit
64 }
65
66 func (versionCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
67         prog = regexp.MustCompile(` -*version$`).ReplaceAllLiteralString(prog, "")
68         fmt.Fprintf(stdout, "%s %s (%s)\n", prog, version, runtime.Version())
69         return 0
70 }
71
72 // Multi is a Handler that looks up its first argument in a map (after
73 // stripping any "arvados-" or "crunch-" prefix), and invokes the
74 // resulting Handler with the remaining args.
75 //
76 // Example:
77 //
78 //      os.Exit(Multi(map[string]Handler{
79 //              "foobar": HandlerFunc(func(prog string, args []string) int {
80 //                      fmt.Println(args[0])
81 //                      return 2
82 //              }),
83 //      })("/usr/bin/multi", []string{"foobar", "baz"}, os.Stdin, os.Stdout, os.Stderr))
84 //
85 // ...prints "baz" and exits 2.
86 type Multi map[string]Handler
87
88 func (m Multi) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
89         _, basename := filepath.Split(prog)
90         if i := strings.Index(basename, "~"); i >= 0 {
91                 // drop "~anything" suffix (arvados-dispatch-cloud's
92                 // DeployRunnerBinary feature relies on this)
93                 basename = basename[:i]
94         }
95         cmd, ok := m[basename]
96         if !ok {
97                 // "controller" command exists, and binary is named "arvados-controller"
98                 cmd, ok = m[strings.TrimPrefix(basename, "arvados-")]
99         }
100         if !ok {
101                 // "dispatch-slurm" command exists, and binary is named "crunch-dispatch-slurm"
102                 cmd, ok = m[strings.TrimPrefix(basename, "crunch-")]
103         }
104         if ok {
105                 return cmd.RunCommand(prog, args, stdin, stdout, stderr)
106         } else if len(args) < 1 {
107                 fmt.Fprintf(stderr, "usage: %s command [args]\n", prog)
108                 m.Usage(stderr)
109                 return EXIT_INVALIDARGUMENT
110         } else if cmd, ok = m[args[0]]; ok {
111                 return cmd.RunCommand(prog+" "+args[0], args[1:], stdin, stdout, stderr)
112         } else {
113                 fmt.Fprintf(stderr, "%s: unrecognized command %q\n", prog, args[0])
114                 m.Usage(stderr)
115                 return EXIT_INVALIDARGUMENT
116         }
117 }
118
119 func (m Multi) Usage(stderr io.Writer) {
120         fmt.Fprintf(stderr, "\nAvailable commands:\n")
121         m.listSubcommands(stderr, "")
122 }
123
124 func (m Multi) listSubcommands(out io.Writer, prefix string) {
125         var subcommands []string
126         for sc := range m {
127                 if strings.HasPrefix(sc, "-") {
128                         // Some subcommands have alternate versions
129                         // like "--version" for compatibility. Don't
130                         // clutter the subcommand summary with those.
131                         continue
132                 }
133                 subcommands = append(subcommands, sc)
134         }
135         sort.Strings(subcommands)
136         for _, sc := range subcommands {
137                 switch cmd := m[sc].(type) {
138                 case Multi:
139                         cmd.listSubcommands(out, prefix+sc+" ")
140                 default:
141                         fmt.Fprintf(out, "    %s%s\n", prefix, sc)
142                 }
143         }
144 }
145
146 type FlagSet interface {
147         Init(string, flag.ErrorHandling)
148         Args() []string
149         NArg() int
150         Parse([]string) error
151         SetOutput(io.Writer)
152         PrintDefaults()
153 }
154
155 // SubcommandToFront silently parses args using flagset, and returns a
156 // copy of args with the first non-flag argument moved to the
157 // front. If parsing fails or consumes all of args, args is returned
158 // unchanged.
159 //
160 // SubcommandToFront invokes methods on flagset that have side
161 // effects, including Parse. In typical usage, flagset will not used
162 // for anything else after being passed to SubcommandToFront.
163 func SubcommandToFront(args []string, flagset FlagSet) []string {
164         flagset.Init("", flag.ContinueOnError)
165         flagset.SetOutput(ioutil.Discard)
166         if err := flagset.Parse(args); err != nil || flagset.NArg() == 0 {
167                 // No subcommand found.
168                 return args
169         }
170         // Move subcommand to the front.
171         flagargs := len(args) - flagset.NArg()
172         newargs := make([]string, len(args))
173         newargs[0] = args[flagargs]
174         copy(newargs[1:flagargs+1], args[:flagargs])
175         copy(newargs[flagargs+1:], args[flagargs+1:])
176         return newargs
177 }
178
179 type NoPrefixFormatter struct{}
180
181 func (NoPrefixFormatter) Format(entry *logrus.Entry) ([]byte, error) {
182         return []byte(entry.Message + "\n"), nil
183 }