72cbb3db6a
The default help template relies on the String() method of Flag to render the flag. For most flag types, String() indirects through FlagStringer, so that is the best place to customize flag rendering. FlagStringer was not called for slice flags because their help output differs from other flags in two ways: there can be multiple default values, and the flag name is shown two times to indicate that the flag can be specified multiple times. To make multiple values work in the FlagStringer, I simply changed GetValue() to return all values. Showing the flag more than once is achieved through a new interface, DocGenerationSliceFlag, which the FlagStringer uses to decide whether the flag is a slice flag type.
378 lines
8.9 KiB
Go
378 lines
8.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
const defaultPlaceholder = "value"
|
|
|
|
var (
|
|
slPfx = fmt.Sprintf("sl:::%d:::", time.Now().UTC().UnixNano())
|
|
|
|
commaWhitespace = regexp.MustCompile("[, ]+.*")
|
|
)
|
|
|
|
// BashCompletionFlag enables bash-completion for all commands and subcommands
|
|
var BashCompletionFlag Flag = &BoolFlag{
|
|
Name: "generate-bash-completion",
|
|
Hidden: true,
|
|
}
|
|
|
|
// VersionFlag prints the version for the application
|
|
var VersionFlag Flag = &BoolFlag{
|
|
Name: "version",
|
|
Aliases: []string{"v"},
|
|
Usage: "print the version",
|
|
}
|
|
|
|
// HelpFlag prints the help for all commands and subcommands.
|
|
// Set to nil to disable the flag. The subcommand
|
|
// will still be added unless HideHelp or HideHelpCommand is set to true.
|
|
var HelpFlag Flag = &BoolFlag{
|
|
Name: "help",
|
|
Aliases: []string{"h"},
|
|
Usage: "show help",
|
|
}
|
|
|
|
// FlagStringer converts a flag definition to a string. This is used by help
|
|
// to display a flag.
|
|
var FlagStringer FlagStringFunc = stringifyFlag
|
|
|
|
// Serializer is used to circumvent the limitations of flag.FlagSet.Set
|
|
type Serializer interface {
|
|
Serialize() string
|
|
}
|
|
|
|
// FlagNamePrefixer converts a full flag name and its placeholder into the help
|
|
// message flag prefix. This is used by the default FlagStringer.
|
|
var FlagNamePrefixer FlagNamePrefixFunc = prefixedNames
|
|
|
|
// FlagEnvHinter annotates flag help message with the environment variable
|
|
// details. This is used by the default FlagStringer.
|
|
var FlagEnvHinter FlagEnvHintFunc = withEnvHint
|
|
|
|
// FlagFileHinter annotates flag help message with the environment variable
|
|
// details. This is used by the default FlagStringer.
|
|
var FlagFileHinter FlagFileHintFunc = withFileHint
|
|
|
|
// FlagsByName is a slice of Flag.
|
|
type FlagsByName []Flag
|
|
|
|
func (f FlagsByName) Len() int {
|
|
return len(f)
|
|
}
|
|
|
|
func (f FlagsByName) Less(i, j int) bool {
|
|
if len(f[j].Names()) == 0 {
|
|
return false
|
|
} else if len(f[i].Names()) == 0 {
|
|
return true
|
|
}
|
|
return lexicographicLess(f[i].Names()[0], f[j].Names()[0])
|
|
}
|
|
|
|
func (f FlagsByName) Swap(i, j int) {
|
|
f[i], f[j] = f[j], f[i]
|
|
}
|
|
|
|
// ActionableFlag is an interface that wraps Flag interface and RunAction operation.
|
|
type ActionableFlag interface {
|
|
Flag
|
|
RunAction(*Context) error
|
|
}
|
|
|
|
// Flag is a common interface related to parsing flags in cli.
|
|
// For more advanced flag parsing techniques, it is recommended that
|
|
// this interface be implemented.
|
|
type Flag interface {
|
|
fmt.Stringer
|
|
// Apply Flag settings to the given flag set
|
|
Apply(*flag.FlagSet) error
|
|
Names() []string
|
|
IsSet() bool
|
|
}
|
|
|
|
// RequiredFlag is an interface that allows us to mark flags as required
|
|
// it allows flags required flags to be backwards compatible with the Flag interface
|
|
type RequiredFlag interface {
|
|
Flag
|
|
|
|
IsRequired() bool
|
|
}
|
|
|
|
// DocGenerationFlag is an interface that allows documentation generation for the flag
|
|
type DocGenerationFlag interface {
|
|
Flag
|
|
|
|
// TakesValue returns true if the flag takes a value, otherwise false
|
|
TakesValue() bool
|
|
|
|
// GetUsage returns the usage string for the flag
|
|
GetUsage() string
|
|
|
|
// GetValue returns the flags value as string representation and an empty
|
|
// string if the flag takes no value at all.
|
|
GetValue() string
|
|
|
|
// GetDefaultText returns the default text for this flag
|
|
GetDefaultText() string
|
|
|
|
// GetEnvVars returns the env vars for this flag
|
|
GetEnvVars() []string
|
|
}
|
|
|
|
// DocGenerationSliceFlag extends DocGenerationFlag for slice-based flags.
|
|
type DocGenerationSliceFlag interface {
|
|
DocGenerationFlag
|
|
|
|
// IsSliceFlag returns true for flags that can be given multiple times.
|
|
IsSliceFlag() bool
|
|
}
|
|
|
|
// VisibleFlag is an interface that allows to check if a flag is visible
|
|
type VisibleFlag interface {
|
|
Flag
|
|
|
|
// IsVisible returns true if the flag is not hidden, otherwise false
|
|
IsVisible() bool
|
|
}
|
|
|
|
// CategorizableFlag is an interface that allows us to potentially
|
|
// use a flag in a categorized representation.
|
|
type CategorizableFlag interface {
|
|
VisibleFlag
|
|
|
|
GetCategory() string
|
|
}
|
|
|
|
// Countable is an interface to enable detection of flag values which support
|
|
// repetitive flags
|
|
type Countable interface {
|
|
Count() int
|
|
}
|
|
|
|
func flagSet(name string, flags []Flag) (*flag.FlagSet, error) {
|
|
set := flag.NewFlagSet(name, flag.ContinueOnError)
|
|
|
|
for _, f := range flags {
|
|
if err := f.Apply(set); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
set.SetOutput(ioutil.Discard)
|
|
return set, nil
|
|
}
|
|
|
|
func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) {
|
|
switch ff.Value.(type) {
|
|
case Serializer:
|
|
_ = set.Set(name, ff.Value.(Serializer).Serialize())
|
|
default:
|
|
_ = set.Set(name, ff.Value.String())
|
|
}
|
|
}
|
|
|
|
func normalizeFlags(flags []Flag, set *flag.FlagSet) error {
|
|
visited := make(map[string]bool)
|
|
set.Visit(func(f *flag.Flag) {
|
|
visited[f.Name] = true
|
|
})
|
|
for _, f := range flags {
|
|
parts := f.Names()
|
|
if len(parts) == 1 {
|
|
continue
|
|
}
|
|
var ff *flag.Flag
|
|
for _, name := range parts {
|
|
name = strings.Trim(name, " ")
|
|
if visited[name] {
|
|
if ff != nil {
|
|
return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name)
|
|
}
|
|
ff = set.Lookup(name)
|
|
}
|
|
}
|
|
if ff == nil {
|
|
continue
|
|
}
|
|
for _, name := range parts {
|
|
name = strings.Trim(name, " ")
|
|
if !visited[name] {
|
|
copyFlag(name, ff, set)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func visibleFlags(fl []Flag) []Flag {
|
|
var visible []Flag
|
|
for _, f := range fl {
|
|
if vf, ok := f.(VisibleFlag); ok && vf.IsVisible() {
|
|
visible = append(visible, f)
|
|
}
|
|
}
|
|
return visible
|
|
}
|
|
|
|
func prefixFor(name string) (prefix string) {
|
|
if len(name) == 1 {
|
|
prefix = "-"
|
|
} else {
|
|
prefix = "--"
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Returns the placeholder, if any, and the unquoted usage string.
|
|
func unquoteUsage(usage string) (string, string) {
|
|
for i := 0; i < len(usage); i++ {
|
|
if usage[i] == '`' {
|
|
for j := i + 1; j < len(usage); j++ {
|
|
if usage[j] == '`' {
|
|
name := usage[i+1 : j]
|
|
usage = usage[:i] + name + usage[j+1:]
|
|
return name, usage
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
return "", usage
|
|
}
|
|
|
|
func prefixedNames(names []string, placeholder string) string {
|
|
var prefixed string
|
|
for i, name := range names {
|
|
if name == "" {
|
|
continue
|
|
}
|
|
|
|
prefixed += prefixFor(name) + name
|
|
if placeholder != "" {
|
|
prefixed += " " + placeholder
|
|
}
|
|
if i < len(names)-1 {
|
|
prefixed += ", "
|
|
}
|
|
}
|
|
return prefixed
|
|
}
|
|
|
|
func withEnvHint(envVars []string, str string) string {
|
|
envText := ""
|
|
if len(envVars) > 0 {
|
|
prefix := "$"
|
|
suffix := ""
|
|
sep := ", $"
|
|
if runtime.GOOS == "windows" {
|
|
prefix = "%"
|
|
suffix = "%"
|
|
sep = "%, %"
|
|
}
|
|
|
|
envText = fmt.Sprintf(" [%s%s%s]", prefix, strings.Join(envVars, sep), suffix)
|
|
}
|
|
return str + envText
|
|
}
|
|
|
|
func FlagNames(name string, aliases []string) []string {
|
|
var ret []string
|
|
|
|
for _, part := range append([]string{name}, aliases...) {
|
|
// v1 -> v2 migration warning zone:
|
|
// Strip off anything after the first found comma or space, which
|
|
// *hopefully* makes it a tiny bit more obvious that unexpected behavior is
|
|
// caused by using the v1 form of stringly typed "Name".
|
|
ret = append(ret, commaWhitespace.ReplaceAllString(part, ""))
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func withFileHint(filePath, str string) string {
|
|
fileText := ""
|
|
if filePath != "" {
|
|
fileText = fmt.Sprintf(" [%s]", filePath)
|
|
}
|
|
return str + fileText
|
|
}
|
|
|
|
func formatDefault(format string) string {
|
|
return " (default: " + format + ")"
|
|
}
|
|
|
|
func stringifyFlag(f Flag) string {
|
|
// enforce DocGeneration interface on flags to avoid reflection
|
|
df, ok := f.(DocGenerationFlag)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
placeholder, usage := unquoteUsage(df.GetUsage())
|
|
needsPlaceholder := df.TakesValue()
|
|
|
|
if needsPlaceholder && placeholder == "" {
|
|
placeholder = defaultPlaceholder
|
|
}
|
|
|
|
defaultValueString := ""
|
|
|
|
if s := df.GetDefaultText(); s != "" {
|
|
defaultValueString = fmt.Sprintf(formatDefault("%s"), s)
|
|
}
|
|
|
|
usageWithDefault := strings.TrimSpace(usage + defaultValueString)
|
|
|
|
pn := prefixedNames(df.Names(), placeholder)
|
|
sliceFlag, ok := f.(DocGenerationSliceFlag)
|
|
if ok && sliceFlag.IsSliceFlag() {
|
|
pn = pn + " [ " + pn + " ]"
|
|
}
|
|
|
|
return withEnvHint(df.GetEnvVars(), fmt.Sprintf("%s\t%s", pn, usageWithDefault))
|
|
}
|
|
|
|
func hasFlag(flags []Flag, fl Flag) bool {
|
|
for _, existing := range flags {
|
|
if fl == existing {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Return the first value from a list of environment variables and files
|
|
// (which may or may not exist), a description of where the value was found,
|
|
// and a boolean which is true if a value was found.
|
|
func flagFromEnvOrFile(envVars []string, filePath string) (value string, fromWhere string, found bool) {
|
|
for _, envVar := range envVars {
|
|
envVar = strings.TrimSpace(envVar)
|
|
if value, found := syscall.Getenv(envVar); found {
|
|
return value, fmt.Sprintf("environment variable %q", envVar), true
|
|
}
|
|
}
|
|
for _, fileVar := range strings.Split(filePath, ",") {
|
|
if fileVar != "" {
|
|
if data, err := ioutil.ReadFile(fileVar); err == nil {
|
|
return string(data), fmt.Sprintf("file %q", filePath), true
|
|
}
|
|
}
|
|
}
|
|
return "", "", false
|
|
}
|
|
|
|
func flagSplitMultiValues(val string) []string {
|
|
return strings.Split(val, ",")
|
|
}
|