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