aboutsummaryrefslogtreecommitdiff
path: root/cmd/cmd.go
blob: 945e9457ee924ed779e4f5af769798c6f42fd851 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
/*
 * cmd.go - Main interface to cmd package (running, Cmd and Flag structs, etc)
 *
 * Copyright 2017 Google Inc.
 * Author: Joe Richey (joerichey@google.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

// Package cmd is the common library for writing command line binaries.
// This package is mainly a wrapper around github.com/urfave/cli, but provides
// additional support to make the usage look similar to the man page.
//
// The main componets are the `Cmd`, `Argument`, and `Flag` types which can be
// used to define a top-level command with many potential subcommands. This
// package also presents a smaller interface than urfave/cli, making it easier
// to use for other commands.
package cmd

import (
	"fmt"
	"io"
	"os"
	"text/template"
	"time"

	"github.com/blang/semver"
)

// Context represents the state of a running application, and is the only thing
// passed to a CommandFunc.
type Context struct {
	Command  *Command
	Parent   *Context
	Info     *Info
	Args     []string
	flagArgs []string
}

// FullName returns the space-separated name of the command and all parents.
func (ctx *Context) FullName() string {
	if ctx.Parent == nil {
		return ctx.Command.Name
	}
	return fmt.Sprintf("%s %s", ctx.Parent.FullName(), ctx.Command.Name)
}

// ManPage returns the man page entry for this context. It is either the ManPage
// for the the current command or the closet Parent.
func (ctx *Context) ManPage() *ManPage {
	if ctx.Command.ManPage.Section != 0 || ctx.Parent == nil {
		return ctx.Command.ManPage
	}
	return ctx.Parent.ManPage()
}

// Creates an anonymous template from the text, and runs it with the provided
// Context and writer. Panics if text has a bad format or execution fails.
func (ctx *Context) executeTemplate(w io.Writer, text string) {
	tmpl := template.Must(template.New("").Parse(text))
	if err := tmpl.Execute(w, ctx); err != nil {
		panic(err)
	}
}

func (ctx *Context) execute() {
	fmt.Printf("%+v\n", ctx)
	return
}

// Info is a parsed view of the corresponding global variables.
type Info struct {
	Version   semver.Version
	BuildTime time.Time
	Authors   []Author
	Copyright string
}

// Author contains the contact information for a contributor.
type Author struct {
	Name  string
	Email string
}

// Argument represents a parameter passed to a function. It has an optional
// usage explains how it should be used.
type Argument struct {
	ArgName string
	Usage   string
}

func (a *Argument) String() string { return fmt.Sprintf("<%s>", a.ArgName) }

// ManPage a man page with a title and section.
type ManPage struct {
	Title   string
	Section int
}

// CommandFunc contains the implementation of a command. If a normal error is
// returned, the error will be printed out (with an optional explanation) and
// Run will exit with FailureCode. If a usage error is returned, the error and
// the commnd's usage are printed out and Run will exit with UsageFailureCode.
// Returning nil causes Run to return.
type CommandFunc func(ctx *Context) error

// Command represents a command with many potential top-level commands. This is
// transformed into a cli.Command in Run().
type Command struct {
	Name             string
	Title            string
	UsageLines       []string
	SubCommands      []*Command
	InheritArguments bool
	Arguments        []*Argument
	InheritFlags     bool
	Flags            []Flag
	ManPage          *ManPage
	Action           CommandFunc
}

// Run executes the command with os.Args, equivalent to c.RunArgs(os.Args).
func (c *Command) Run() {
	c.RunArgs(os.Args)
}

// RunArgs executes the command with the provided args. If the Name argument is
// empty, args[0]'s basename is used instead. If the command fails, this method
// will not return.
func (c *Command) RunArgs(args []string) {
	binaryName, args := args[0], args[1:]
	if c.Name == "" {
		c.Name = binaryName
	}

	// Create our initial context by sorting the args and parsing the tags.
	ctx := &Context{
		Command: c,
		Info:    parseInfo(),
	}
	ctx.Args, ctx.flagArgs = sortArgs(args)

	ctx.execute()
}

// Divide the arguments into flag arguments (those starting with "-") and normal
// arguments. If "--" appears in the list, it will classified as a normal
// argument as well as all arguments following it. Also removes empty args.
func sortArgs(args []string) (normalArgs, flagArgs []string) {
	var arg string
	for len(args) > 0 {
		arg, args = args[0], args[1:]
		if arg == "" {
			continue
		}
		if arg == "--" {
			normalArgs = append(normalArgs, arg)
			normalArgs = append(normalArgs, args...)
			return
		} else if arg[0] == '-' {
			flagArgs = append(flagArgs, arg)
		} else {
			normalArgs = append(normalArgs, arg)
		}
	}
	return
}