aboutsummaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorJoe Richey joerichey@google.com <joerichey@google.com>2017-10-12 17:59:45 -0700
committerJoseph Richey <joerichey94@gmail.com>2017-10-19 02:22:25 -0700
commitb4299090c3e503ba0c49a6086b1a46c218ca45f4 (patch)
tree889adbf3da9616a5c6eaa783291e5f94c01955a2 /cmd
parent921f1c977c4e0704f61e3a7c092d3a4317ab278c (diff)
Command, Context, command line splitting setup
Diffstat (limited to 'cmd')
-rw-r--r--cmd/cmd.go146
-rw-r--r--cmd/flag.go104
-rw-r--r--cmd/info.go88
-rw-r--r--cmd/output.go4
-rw-r--r--cmd/strings.go14
-rw-r--r--cmd/version.go57
6 files changed, 254 insertions, 159 deletions
diff --git a/cmd/cmd.go b/cmd/cmd.go
index 725aaea..945e945 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -27,18 +27,69 @@
// to use for other commands.
package cmd
-import "os"
+import (
+ "fmt"
+ "io"
+ "os"
+ "text/template"
+ "time"
-// Command represents a command with many potential top-level commands. This is
-// transformed into a cli.Command in Run().
-type Command struct {
- Name string
- UsageLines []string
- SubCmds []*Command
- Arguments []*Argument
- Flags []Flag
- ManPage *ManEntry
- Action CommandFunc
+ "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
@@ -48,19 +99,35 @@ type Argument struct {
Usage string
}
-// ManEntry represents an entry in a man page with a name, section, and title.
-type ManEntry struct {
- Name string
- Section int
+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. The provided args have
-// the flags and leading command names removed. If a normal error is returned,
-// it is printed out (with an optional explanation) and exits with FailureCode.
-// If a usage error is returned, it is printed out with the command's usage and
-// exits with UsageFailureCode. Returning nil causes an exit with success.
-type CommandFunc func(args []string) error
+// 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() {
@@ -71,5 +138,40 @@ func (c *Command) Run() {
// empty, args[0]'s basename is used instead. If the command fails, this method
// will not return.
func (c *Command) RunArgs(args []string) {
- // TODO(joerichey): Implement conversion to cli.Command
+ 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
}
diff --git a/cmd/flag.go b/cmd/flag.go
index 18b2a4c..5f864b9 100644
--- a/cmd/flag.go
+++ b/cmd/flag.go
@@ -26,48 +26,31 @@ import (
"time"
)
-// Flag represents a command line flag that can be passed to a command. Note
-// that Flag also conforms to the cli.Flag interface. The Name, ArgName, and
-// Usage of the Flag can be used to format it in a short form with ShortFormat,
-// or in it's full format with the String method.
+// Flag represents a flag that can be passed to a command. The Name, ArgName,
+// and Usage are used to format and display the flag.
type Flag interface {
+ // String formats the flag as either "--name" or "--name=<argName>".
fmt.Stringer
+ // FullUsage is the usage for this flag with an optional default note.
+ FullUsage() string
+ // Apply sets up this flag on a flag set.
Apply(*flag.FlagSet)
- GetName() string
- GetArgName() string
- GetUsage() string
}
-// How the first usage line for a Flag should appear. We have two formats:
-// --name
-// --name=<argName>
-// The <argName> appears if the prettyFlag's GetArgName() method returns a
-// non-empty string. The returned string from shortFormat() does not include
-// any leading or trailing whitespace.
-func ShortFormat(f Flag) string {
- if argName := f.GetArgName(); argName != "" {
- return fmt.Sprintf("--%s=%s", f.GetName(), argName)
+// Formats as "--name" or as "--name=<argName>" if argName is present.
+func formatHelper(name, argName string) string {
+ if argName != "" {
+ return fmt.Sprintf("--%s=<%s>", name, argName)
}
- return fmt.Sprintf("--%s", f.GetName())
+ return fmt.Sprintf("--%s", name)
}
-// How our flags should appear when displaying their usage. An example would be:
-// --help
-// Prints help screen for commands and subcommands.
-//
-// If defaultString is specified, this if appended to the usage. Example:
-//
-// --legacy
-// Allow for support of older kernels with ext4 (before v4.8) and
-// F2FS (before v4.6) filesystems. (default: true)
-func longFormat(f Flag, defaultString ...string) string {
- usage := f.GetUsage()
- if len(defaultString) > 0 {
- usage += fmt.Sprintf(" (default: %v)", defaultString[0])
+// Appends (default: <default>) to the usage if defaultString is present.
+func usageHelper(usage, defaultString string) string {
+ if defaultString != "" {
+ usage += fmt.Sprintf(" (default: %s)", defaultString)
}
-
- usage = wrapText(usage, 2)
- return fmt.Sprintf("\t%s\n%s", ShortFormat(f), usage)
+ return usage
}
// BoolFlag is a Flag of type bool.
@@ -78,25 +61,20 @@ type BoolFlag struct {
Value bool
}
-func (f *BoolFlag) String() string {
+// String always uses the smaller format, as it has no ArgName.
+func (f *BoolFlag) String() string { return formatHelper(f.Name, "") }
+
+// FullUsage shows the default if it's true (flag is implicitly passed).
+func (f *BoolFlag) FullUsage() string {
if !f.Default {
- return longFormat(f)
+ return usageHelper(f.Usage, "")
}
- return longFormat(f, strconv.FormatBool(f.Default))
+ return usageHelper(f.Usage, "true")
}
// Apply uses BoolFlag's value to set a flag.BoolVar on the FlagSet.
func (f *BoolFlag) Apply(s *flag.FlagSet) { s.BoolVar(&f.Value, f.Name, f.Default, f.Usage) }
-// GetName just returns BoolFlag's name.
-func (f *BoolFlag) GetName() string { return f.Name }
-
-// GetArgName returns nothing as BoolFlags don't have an argument name.
-func (f *BoolFlag) GetArgName() string { return "" }
-
-// GetUsage returns BoolFlag's usage.
-func (f *BoolFlag) GetUsage() string { return f.Usage }
-
// StringFlag is a Flag of type string.
type StringFlag struct {
Name string
@@ -106,25 +84,19 @@ type StringFlag struct {
Value string
}
-func (f *StringFlag) String() string {
+func (f *StringFlag) String() string { return formatHelper(f.Name, f.ArgName) }
+
+// FullUsage shows the deafult if the string is non-empty.
+func (f *StringFlag) FullUsage() string {
if f.Default == "" {
- return longFormat(f)
+ return usageHelper(f.Usage, "")
}
- return longFormat(f, strconv.Quote(f.Default))
+ return usageHelper(f.Usage, strconv.Quote(f.Default))
}
// Apply uses StringFlag's value to set a flag.StringVar on the FlagSet.
func (f *StringFlag) Apply(s *flag.FlagSet) { s.StringVar(&f.Value, f.Name, f.Default, f.Usage) }
-// GetName just returns StringFlag's name.
-func (f *StringFlag) GetName() string { return f.Name }
-
-// GetArgName returns StringFlag's argument name.
-func (f *StringFlag) GetArgName() string { return f.ArgName }
-
-// GetUsage returns StringFlag's usage.
-func (f *StringFlag) GetUsage() string { return f.Usage }
-
// DurationFlag is a Flag of type time.Duration.
type DurationFlag struct {
Name string
@@ -134,21 +106,15 @@ type DurationFlag struct {
Value time.Duration
}
-func (f *DurationFlag) String() string {
+func (f *DurationFlag) String() string { return formatHelper(f.Name, f.ArgName) }
+
+// FullUsage shows the default if the duration is non-zero.
+func (f *DurationFlag) FullUsage() string {
if f.Default == 0 {
- return longFormat(f)
+ return usageHelper(f.Usage, "")
}
- return longFormat(f, f.Default.String())
+ return usageHelper(f.Usage, f.Default.String())
}
// Apply uses DurationFlag's value to set a flag.DurationVar on the FlagSet.
func (f *DurationFlag) Apply(s *flag.FlagSet) { s.DurationVar(&f.Value, f.Name, f.Default, f.Usage) }
-
-// GetName just returns DurationFlag's name.
-func (f *DurationFlag) GetName() string { return f.Name }
-
-// GetArgName returns DurationFlag's argument name.
-func (f *DurationFlag) GetArgName() string { return f.ArgName }
-
-// GetUsage returns DurationFlag's usage.
-func (f *DurationFlag) GetUsage() string { return f.Usage }
diff --git a/cmd/info.go b/cmd/info.go
index 6257ec1..96079ea 100644
--- a/cmd/info.go
+++ b/cmd/info.go
@@ -22,51 +22,75 @@ package cmd
import (
"time"
- "github.com/urfave/cli"
+ "github.com/blang/semver"
+ "github.com/pkg/errors"
)
-// Info contains the global info for the functions.
-var Info struct {
- // Program is the name of the top-level program being executed. If not
- // set it is set in cmd.RunArgs().
- Program string
+var (
// VersionTag (if set) will be displayed in both the short and long
- // version output. VersionTag is not parsed, so any string will work.
+ // version output and can be accessed though Context.Info.Version.
+ // VersionTag must be formatted using Semver (http://semver.org/).
+ //
+ // Often set in Makefile with "-X cmd.VersionTag=$(VERSION)"
VersionTag string
- // BuildTime (if set) will be displayed in the long version output.
- BuildTime time.Time
- // Authors (if non-empty) are displayed in the long version output.
- Authors []cli.Author
- // Copyright (if set) is displayed in the long version output.
+ // BuildTimeTag (if set) will be displayed in the long version
+ // output and can be accessed thought Context.Info.BuildTime. This
+ // string must be formatted as the output of UNIX `date`.
+ //
+ // Often set in Makefile with "-X cmd.BuildTimeTag=$(shell date)"
+ BuildTimeTag string
+ // Authors (if non-empty) are displayed in the long version output and
+ // can be accessed though Context.Info.Authors.
+ Authors []Author
+ // Copyright (if set) is displayed in the long version output and can
+ // be accessed through Context.Info.Copyright.
Copyright string
-}
-
-// Linker flags of the form "-X cmd.Info.VersionTag=1.0" do not work, so we use
-// these separate files so variables can be set from the Makefile.
-var (
- versionTag string
- buildTime string
)
// fscrypt specific initialization
func init() {
- Info.VersionTag = versionTag
- Info.BuildTime = buildTime
- Info.Authors = []cli.Author{{
+ Authors = []Author{{
Name: "Joe Richey",
Email: "joerichey@google.com",
}}
- Info.Copyright = `Copyright 2017 Google, Inc.
+ Copyright = `Copyright 2017 Google, Inc.
+
+ 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.`
+}
+
+// Creates the Info structure by parsing the above global variables. Panics if
+// the variables to parse are in the incorrect format.
+func parseInfo() *Info {
+ var err error
- 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
+ var t time.Time
+ if BuildTimeTag != "" {
+ if t, err = time.Parse(time.UnixDate, BuildTimeTag); err != nil {
+ panic(err)
+ }
+ }
- http://www.apache.org/licenses/LICENSE-2.0
+ var v semver.Version
+ if VersionTag != "" {
+ if v, err = semver.ParseTolerant(VersionTag); err != nil {
+ panic(errors.Wrapf(err, "semver: parsing %q", VersionTag))
+ }
+ }
- 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.`
+ return &Info{
+ Version: v,
+ BuildTime: t,
+ Authors: Authors,
+ Copyright: Copyright,
+ }
}
diff --git a/cmd/output.go b/cmd/output.go
index 024705d..c3a79a4 100644
--- a/cmd/output.go
+++ b/cmd/output.go
@@ -51,7 +51,7 @@ var (
// HelpFlag writes help to Stdout
HelpFlag = &BoolFlag{
Name: "help",
- Usage: "Prints this 🧗help text for commands and subcommands",
+ Usage: "Prints this help text for commands and subcommands",
}
// VerboseFlag indicates that all logging output should be printed.
VerboseFlag = &BoolFlag{
@@ -101,7 +101,7 @@ func wrapText(text string, numTabs int) string {
spaceLeft := 0
maxTextLen := LineLength - numTabs*TabWidth
delimiter := strings.Repeat("\t", numTabs)
- for i, word := range strings.Fields(text) {
+ for _, word := range strings.Fields(text) {
wordLen := utf8.RuneCountInString(word)
if wordLen >= spaceLeft {
// If no room left, write the word on the next line.
diff --git a/cmd/strings.go b/cmd/strings.go
index 559c60c..16c80f2 100644
--- a/cmd/strings.go
+++ b/cmd/strings.go
@@ -18,17 +18,3 @@
*/
package cmd
-
-import (
- "io"
- "text/template"
-)
-
-// ExecuteTemplate creates an anonymous template the text, and runs it with the
-// provided writer and data. Panics if text has bad format or execution fails.
-func ExecuteTemplate(w io.Writer, text string, data interface{}) {
- tmpl := template.Must(template.New("").Parse(text))
- if err := tmpl.Execute(w, data); err != nil {
- panic(err)
- }
-}
diff --git a/cmd/version.go b/cmd/version.go
index 787e2cd..99097b5 100644
--- a/cmd/version.go
+++ b/cmd/version.go
@@ -19,44 +19,61 @@
package cmd
+import (
+ "fmt"
+
+ "github.com/blang/semver"
+)
+
// Templates for use with the version command, which both parse the Info var.
var (
- VersionShortTemplate = "{{.Command}} version {{.VersionTag}}\n"
- VersionLongTemplate = VersionShortTemplate + `{{if .Compiled}}
+ VersionTemplate = "{{.FullName}} {{.Info.Version}}\n"
+ VersionLongTemplate = `{{if .Info.BuildTime}}
Compiled:
- {{.Compiled}}
-{{end}}{{if len .Authors}}
-Author{{with $length := len .Authors}}{{if ne 1 $length}}s{{end}}{{end}}:{{range .Authors}}
- {{.}}{{end}}
-{{end}}{{if .Copyright}}
+ {{.Info.BuildTime}}
+{{end}}
+
+{{with $length := len .Info.Authors}}
+{{if $length}}
+Author{{if ne 1 $length}}s{{end}}:
+{{range .Info.Authors}}
+ {{.Name}}{{if .Email}} <{{.Email}}>{{end}}
+{{end}}
+{{end}}
+{{end}}
+
+{{if .Info.Copyright}}
Copyright:
- {{.Copyright}}
+{{.Info.Copyright}}
{{end}}`
)
-// Version is a command which will display either the VersionTag (by default) or
-// the full version information (version, copyright, authors).
-var Version = &Command{
+// VersionCommand is a command which will display either the VersionTag (by
+// default) or the full version information: version, copyright, authors, etc...
+var VersionCommand = &Command{
Name: "version",
- UsageLines: []string{""},
- Flags: []Flag{longFlag},
+ Title: "display this program's version information",
+ UsageLines: []string{fmt.Sprintf("[%v]", longFlag)},
+ Flags: []Flag{longFlag, HelpFlag},
Action: versionAction,
}
-// Using longFlag with the version command displays the longer version info.
+// VersionUsage is a UsageLine to add to a Command with a version Subcommand.
+var VersionUsage = VersionCommand.Name + " " + VersionCommand.UsageLines[0]
+
+// longFlag tells the version command to display the longer version info.
var longFlag = &BoolFlag{
Name: "long",
- Usage: "Print the detailed version and copyright information.",
+ Usage: "Print the detailed version, build, and copyright information.",
}
-func versionAction(_ []string) error {
- if Info.VersionTag == "" {
+func versionAction(ctx *Context) error {
+ if ctx.Info.Version.Equals(semver.Version{}) {
return ErrUnknownVersion
}
+ ctx.executeTemplate(Output, VersionTemplate)
if longFlag.Value {
-
- } else {
-
+ ctx.executeTemplate(Output, VersionLongTemplate)
}
return nil
}