-
Notifications
You must be signed in to change notification settings - Fork 4.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Centralize command-line parsing #2717
Merged
Merged
Changes from 7 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
de1718a
Centralize command-line parsing
sethvargo 3d09fb8
Merge branch 'master' into f-cli-rework
kyhavlov e86ec5b
vendor: Add golang-text dependency
kyhavlov 8009432
Convert configtest and force-leave commands to use Meta
kyhavlov edc3539
Fix the check for displaying the command options
kyhavlov 49d2ce1
Move command Meta to base.Command and split http options
kyhavlov 197dc10
Add utility types to enable checking for unset flags
kyhavlov 0b85bbf
Small tweaks to base command
kyhavlov File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,239 @@ | ||
package base | ||
|
||
import ( | ||
"bufio" | ||
"bytes" | ||
"flag" | ||
"fmt" | ||
"io" | ||
"strings" | ||
|
||
"github.com/hashicorp/consul/api" | ||
"github.com/mitchellh/cli" | ||
text "github.com/tonnerre/golang-text" | ||
) | ||
|
||
// maxLineLength is the maximum width of any line. | ||
const maxLineLength int = 72 | ||
|
||
// FlagSetFlags is an enum to define what flags are present in the | ||
// default FlagSet returned. | ||
type FlagSetFlags uint | ||
|
||
const ( | ||
FlagSetNone FlagSetFlags = iota << 1 | ||
FlagSetClientHTTP FlagSetFlags = iota << 1 | ||
FlagSetServerHTTP FlagSetFlags = iota << 1 | ||
|
||
FlagSetHTTP = FlagSetClientHTTP | FlagSetServerHTTP | ||
) | ||
|
||
type Command struct { | ||
Ui cli.Ui | ||
Flags FlagSetFlags | ||
|
||
flagSet *flag.FlagSet | ||
|
||
// These are the options which correspond to the HTTP API options | ||
httpAddr stringValue | ||
token stringValue | ||
datacenter stringValue | ||
stale boolValue | ||
} | ||
|
||
// HTTPClient returns a client with the parsed flags. It panics if the command | ||
// does not accept HTTP flags or if the flags have not been parsed. | ||
func (c *Command) HTTPClient() (*api.Client, error) { | ||
if !c.hasClientHTTP() && !c.hasServerHTTP() { | ||
panic("no http flags defined") | ||
} | ||
if !c.flagSet.Parsed() { | ||
panic("flags have not been parsed") | ||
} | ||
|
||
config := api.DefaultConfig() | ||
c.httpAddr.Merge(&config.Address) | ||
c.token.Merge(&config.Token) | ||
c.datacenter.Merge(&config.Datacenter) | ||
c.Ui.Info(fmt.Sprintf("client http addr: %s", config.Address)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This'll add clutter to the command output - let's drop this print. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oops, this was a debug print that got left in |
||
return api.NewClient(config) | ||
} | ||
|
||
// httpFlagsClient is the list of flags that apply to HTTP connections. | ||
func (c *Command) httpFlagsClient(f *flag.FlagSet) *flag.FlagSet { | ||
if f == nil { | ||
f = flag.NewFlagSet("", flag.ContinueOnError) | ||
} | ||
|
||
f.Var(&c.httpAddr, "http-addr", | ||
"Address and port to the Consul HTTP agent. The value can be an IP "+ | ||
"address or DNS address, but it must also include the port. This can "+ | ||
"also be specified via the CONSUL_HTTP_ADDR environment variable. The "+ | ||
"default value is 127.0.0.1:8500.") | ||
f.Var(&c.token, "token", | ||
"ACL token to use in the request. This can also be specified via the "+ | ||
"CONSUL_HTTP_TOKEN environment variable. If unspecified, the query will "+ | ||
"default to the token of the Consul agent at the HTTP address.") | ||
|
||
return f | ||
} | ||
|
||
// httpFlagsServer is the list of flags that apply to HTTP connections. | ||
func (c *Command) httpFlagsServer(f *flag.FlagSet) *flag.FlagSet { | ||
if f == nil { | ||
f = flag.NewFlagSet("", flag.ContinueOnError) | ||
} | ||
|
||
f.Var(&c.datacenter, "datacenter", | ||
"Name of the datacenter to query. If unspecified, this will default to "+ | ||
"the datacenter of the queried agent.") | ||
f.Var(&c.stale, "stale", | ||
"Permit any Consul server (non-leader) to respond to this request. This "+ | ||
"allows for lower latency and higher throughput, but can result in "+ | ||
"stale data. This option has no effect on non-read operations. The "+ | ||
"default value is false.") | ||
|
||
return f | ||
} | ||
|
||
// NewFlagSet creates a new flag set for the given command. It automatically | ||
// generates help output and adds the appropriate API flags. | ||
func (c *Command) NewFlagSet(command cli.Command) *flag.FlagSet { | ||
f := flag.NewFlagSet("", flag.ContinueOnError) | ||
f.Usage = func() { c.Ui.Error(command.Help()) } | ||
|
||
if c.hasClientHTTP() { | ||
c.httpFlagsClient(f) | ||
} | ||
|
||
if c.hasServerHTTP() { | ||
c.httpFlagsServer(f) | ||
} | ||
|
||
errR, errW := io.Pipe() | ||
errScanner := bufio.NewScanner(errR) | ||
go func() { | ||
for errScanner.Scan() { | ||
c.Ui.Error(errScanner.Text()) | ||
} | ||
}() | ||
f.SetOutput(errW) | ||
|
||
c.flagSet = f | ||
|
||
return f | ||
} | ||
|
||
// Parse is used to parse the underlying flag set. | ||
func (c *Command) Parse(args []string) error { | ||
return c.flagSet.Parse(args) | ||
} | ||
|
||
// Help returns the help for this flagSet. | ||
func (c *Command) Help() string { | ||
return c.helpFlagsFor(c.flagSet) | ||
} | ||
|
||
// hasClientHTTP returns true if this meta command contains client HTTP flags. | ||
func (c *Command) hasClientHTTP() bool { | ||
return c.Flags&FlagSetClientHTTP != 0 | ||
} | ||
|
||
// hasServerHTTP returns true if this meta command contains server HTTP flags. | ||
func (c *Command) hasServerHTTP() bool { | ||
return c.Flags&FlagSetServerHTTP != 0 | ||
} | ||
|
||
// helpFlagsFor visits all flags in the given flag set and prints formatted | ||
// help output. This function is sad because there's no "merging" of command | ||
// line flags. We explicitly pull out our "common" options into another section | ||
// by doing string comparisons :(. | ||
func (c *Command) helpFlagsFor(f *flag.FlagSet) string { | ||
httpFlagsClient := c.httpFlagsClient(nil) | ||
httpFlagsServer := c.httpFlagsServer(nil) | ||
|
||
var out bytes.Buffer | ||
|
||
firstHTTP := true | ||
if c.hasClientHTTP() { | ||
if firstHTTP { | ||
printTitle(&out, "HTTP API Options") | ||
firstHTTP = false | ||
} | ||
httpFlagsClient.VisitAll(func(f *flag.Flag) { | ||
printFlag(&out, f) | ||
}) | ||
} | ||
if c.hasServerHTTP() { | ||
if firstHTTP { | ||
printTitle(&out, "HTTP API Options") | ||
firstHTTP = false | ||
} | ||
httpFlagsServer.VisitAll(func(f *flag.Flag) { | ||
printFlag(&out, f) | ||
}) | ||
} | ||
|
||
firstCommand := true | ||
f.VisitAll(func(f *flag.Flag) { | ||
// Skip HTTP flags as they will be grouped separately | ||
if flagContains(httpFlagsClient, f) || flagContains(httpFlagsServer, f) { | ||
return | ||
} | ||
if firstCommand { | ||
printTitle(&out, "Command Options") | ||
firstCommand = false | ||
} | ||
printFlag(&out, f) | ||
}) | ||
|
||
return strings.TrimRight(out.String(), "\n") | ||
} | ||
|
||
// printTitle prints a consistently-formatted title to the given writer. | ||
func printTitle(w io.Writer, s string) { | ||
fmt.Fprintf(w, "%s\n\n", s) | ||
} | ||
|
||
// printFlag prints a single flag to the given writer. | ||
func printFlag(w io.Writer, f *flag.Flag) { | ||
example, _ := flag.UnquoteUsage(f) | ||
if example != "" { | ||
fmt.Fprintf(w, " -%s=<%s>\n", f.Name, example) | ||
} else { | ||
fmt.Fprintf(w, " -%s\n", f.Name) | ||
} | ||
|
||
indented := wrapAtLength(f.Usage, 5) | ||
fmt.Fprintf(w, "%s\n\n", indented) | ||
} | ||
|
||
// flagContains returns true if the given flag is contained in the given flag | ||
// set or false otherwise. | ||
func flagContains(fs *flag.FlagSet, f *flag.Flag) bool { | ||
var skip bool | ||
|
||
fs.VisitAll(func(hf *flag.Flag) { | ||
if skip { | ||
return | ||
} | ||
|
||
if f.Name == hf.Name && f.Usage == hf.Usage { | ||
skip = true | ||
return | ||
} | ||
}) | ||
|
||
return skip | ||
} | ||
|
||
// wrapAtLength wraps the given text at the maxLineLength, taking into account | ||
// any provided left padding. | ||
func wrapAtLength(s string, pad int) string { | ||
wrapped := text.Wrap(s, maxLineLength-pad) | ||
lines := strings.Split(wrapped, "\n") | ||
for i, line := range lines { | ||
lines[i] = strings.Repeat(" ", pad) + line | ||
} | ||
return strings.Join(lines, "\n") | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should be
1 << iota
.