| package cli |
| |
| import ( |
| "errors" |
| "flag" |
| "fmt" |
| "io" |
| "os" |
| "regexp" |
| "runtime" |
| "strings" |
| "syscall" |
| "time" |
| ) |
| |
| const defaultPlaceholder = "value" |
| |
| const ( |
| defaultSliceFlagSeparator = "," |
| disableSliceFlagSeparator = false |
| ) |
| |
| 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", |
| DisableDefaultText: true, |
| } |
| |
| // 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", |
| DisableDefaultText: true, |
| } |
| |
| // 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, spec separatorSpec) (*flag.FlagSet, error) { |
| set := flag.NewFlagSet(name, flag.ContinueOnError) |
| |
| for _, f := range flags { |
| if c, ok := f.(customizedSeparator); ok { |
| c.WithSeparatorSpec(spec) |
| } |
| if err := f.Apply(set); err != nil { |
| return nil, err |
| } |
| } |
| set.SetOutput(io.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 envFormat(envVars []string, prefix, sep, suffix string) string { |
| if len(envVars) > 0 { |
| return fmt.Sprintf(" [%s%s%s]", prefix, strings.Join(envVars, sep), suffix) |
| } |
| return "" |
| } |
| |
| func defaultEnvFormat(envVars []string) string { |
| return envFormat(envVars, "$", ", $", "") |
| } |
| |
| func withEnvHint(envVars []string, str string) string { |
| envText := "" |
| if runtime.GOOS != "windows" || os.Getenv("PSHOME") != "" { |
| envText = defaultEnvFormat(envVars) |
| } else { |
| envText = envFormat(envVars, "%", "%, %", "%") |
| } |
| 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 := "" |
| |
| // set default text for all flags except bool flags |
| // for bool flags display default text if DisableDefaultText is not |
| // set |
| if bf, ok := f.(*BoolFlag); !ok || !bf.DisableDefaultText { |
| 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 := os.ReadFile(fileVar); err == nil { |
| return string(data), fmt.Sprintf("file %q", filePath), true |
| } |
| } |
| } |
| return "", "", false |
| } |
| |
| type customizedSeparator interface { |
| WithSeparatorSpec(separatorSpec) |
| } |
| |
| type separatorSpec struct { |
| sep string |
| disabled bool |
| customized bool |
| } |
| |
| func (s separatorSpec) flagSplitMultiValues(val string) []string { |
| var ( |
| disabled bool = s.disabled |
| sep string = s.sep |
| ) |
| if !s.customized { |
| disabled = disableSliceFlagSeparator |
| sep = defaultSliceFlagSeparator |
| } |
| if disabled { |
| return []string{val} |
| } |
| |
| return strings.Split(val, sep) |
| } |