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