Merge branch '19792-pysdk-cookbook'
[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         "sort"
18         "strings"
19
20         "github.com/sirupsen/logrus"
21 )
22
23 type Handler interface {
24         RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int
25 }
26
27 type HandlerFunc func(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int
28
29 func (f HandlerFunc) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
30         return f(prog, args, stdin, stdout, stderr)
31 }
32
33 // Version is a Handler that prints the package version (set at build
34 // time using -ldflags) and Go runtime version to stdout, and returns
35 // 0.
36 var Version versionCommand
37
38 var version = "dev"
39
40 type versionCommand struct{}
41
42 func (versionCommand) String() string {
43         return fmt.Sprintf("%s (%s)", version, runtime.Version())
44 }
45
46 func (versionCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
47         prog = regexp.MustCompile(` -*version$`).ReplaceAllLiteralString(prog, "")
48         fmt.Fprintf(stdout, "%s %s (%s)\n", prog, version, runtime.Version())
49         return 0
50 }
51
52 // Multi is a Handler that looks up its first argument in a map (after
53 // stripping any "arvados-" or "crunch-" prefix), and invokes the
54 // resulting Handler with the remaining args.
55 //
56 // Example:
57 //
58 //     os.Exit(Multi(map[string]Handler{
59 //             "foobar": HandlerFunc(func(prog string, args []string) int {
60 //                     fmt.Println(args[0])
61 //                     return 2
62 //             }),
63 //     })("/usr/bin/multi", []string{"foobar", "baz"}, os.Stdin, os.Stdout, os.Stderr))
64 //
65 // ...prints "baz" and exits 2.
66 type Multi map[string]Handler
67
68 func (m Multi) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
69         _, basename := filepath.Split(prog)
70         if i := strings.Index(basename, "~"); i >= 0 {
71                 // drop "~anything" suffix (arvados-dispatch-cloud's
72                 // DeployRunnerBinary feature relies on this)
73                 basename = basename[:i]
74         }
75         cmd, ok := m[basename]
76         if !ok {
77                 // "controller" command exists, and binary is named "arvados-controller"
78                 cmd, ok = m[strings.TrimPrefix(basename, "arvados-")]
79         }
80         if !ok {
81                 // "dispatch-slurm" command exists, and binary is named "crunch-dispatch-slurm"
82                 cmd, ok = m[strings.TrimPrefix(basename, "crunch-")]
83         }
84         if ok {
85                 return cmd.RunCommand(prog, args, stdin, stdout, stderr)
86         } else if len(args) < 1 {
87                 fmt.Fprintf(stderr, "usage: %s command [args]\n", prog)
88                 m.Usage(stderr)
89                 return 2
90         } else if cmd, ok = m[args[0]]; ok {
91                 return cmd.RunCommand(prog+" "+args[0], args[1:], stdin, stdout, stderr)
92         } else {
93                 fmt.Fprintf(stderr, "%s: unrecognized command %q\n", prog, args[0])
94                 m.Usage(stderr)
95                 return 2
96         }
97 }
98
99 func (m Multi) Usage(stderr io.Writer) {
100         fmt.Fprintf(stderr, "\nAvailable commands:\n")
101         m.listSubcommands(stderr, "")
102 }
103
104 func (m Multi) listSubcommands(out io.Writer, prefix string) {
105         var subcommands []string
106         for sc := range m {
107                 if strings.HasPrefix(sc, "-") {
108                         // Some subcommands have alternate versions
109                         // like "--version" for compatibility. Don't
110                         // clutter the subcommand summary with those.
111                         continue
112                 }
113                 subcommands = append(subcommands, sc)
114         }
115         sort.Strings(subcommands)
116         for _, sc := range subcommands {
117                 switch cmd := m[sc].(type) {
118                 case Multi:
119                         cmd.listSubcommands(out, prefix+sc+" ")
120                 default:
121                         fmt.Fprintf(out, "    %s%s\n", prefix, sc)
122                 }
123         }
124 }
125
126 type FlagSet interface {
127         Init(string, flag.ErrorHandling)
128         Args() []string
129         NArg() int
130         Parse([]string) error
131         SetOutput(io.Writer)
132         PrintDefaults()
133 }
134
135 // SubcommandToFront silently parses args using flagset, and returns a
136 // copy of args with the first non-flag argument moved to the
137 // front. If parsing fails or consumes all of args, args is returned
138 // unchanged.
139 //
140 // SubcommandToFront invokes methods on flagset that have side
141 // effects, including Parse. In typical usage, flagset will not used
142 // for anything else after being passed to SubcommandToFront.
143 func SubcommandToFront(args []string, flagset FlagSet) []string {
144         flagset.Init("", flag.ContinueOnError)
145         flagset.SetOutput(ioutil.Discard)
146         if err := flagset.Parse(args); err != nil || flagset.NArg() == 0 {
147                 // No subcommand found.
148                 return args
149         }
150         // Move subcommand to the front.
151         flagargs := len(args) - flagset.NArg()
152         newargs := make([]string, len(args))
153         newargs[0] = args[flagargs]
154         copy(newargs[1:flagargs+1], args[:flagargs])
155         copy(newargs[flagargs+1:], args[flagargs+1:])
156         return newargs
157 }
158
159 type NoPrefixFormatter struct{}
160
161 func (NoPrefixFormatter) Format(entry *logrus.Entry) ([]byte, error) {
162         return []byte(entry.Message + "\n"), nil
163 }