diff options
| author | Joe Richey joerichey@google.com <joerichey@google.com> | 2017-10-12 17:59:45 -0700 |
|---|---|---|
| committer | Joseph Richey <joerichey94@gmail.com> | 2017-10-19 02:22:25 -0700 |
| commit | b4299090c3e503ba0c49a6086b1a46c218ca45f4 (patch) | |
| tree | 889adbf3da9616a5c6eaa783291e5f94c01955a2 /cmd | |
| parent | 921f1c977c4e0704f61e3a7c092d3a4317ab278c (diff) | |
Command, Context, command line splitting setup
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/cmd.go | 146 | ||||
| -rw-r--r-- | cmd/flag.go | 104 | ||||
| -rw-r--r-- | cmd/info.go | 88 | ||||
| -rw-r--r-- | cmd/output.go | 4 | ||||
| -rw-r--r-- | cmd/strings.go | 14 | ||||
| -rw-r--r-- | cmd/version.go | 57 |
6 files changed, 254 insertions, 159 deletions
@@ -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 } |