1package cli 2 3import ( 4 "flag" 5 "fmt" 6 "io/ioutil" 7 "reflect" 8 "regexp" 9 "runtime" 10 "strconv" 11 "strings" 12 "syscall" 13 "time" 14) 15 16const defaultPlaceholder = "value" 17 18var ( 19 slPfx = fmt.Sprintf("sl:::%d:::", time.Now().UTC().UnixNano()) 20 21 commaWhitespace = regexp.MustCompile("[, ]+.*") 22) 23 24// BashCompletionFlag enables bash-completion for all commands and subcommands 25var BashCompletionFlag Flag = &BoolFlag{ 26 Name: "generate-bash-completion", 27 Hidden: true, 28} 29 30// VersionFlag prints the version for the application 31var VersionFlag Flag = &BoolFlag{ 32 Name: "version", 33 Aliases: []string{"v"}, 34 Usage: "print the version", 35} 36 37// HelpFlag prints the help for all commands and subcommands. 38// Set to nil to disable the flag. The subcommand 39// will still be added unless HideHelp or HideHelpCommand is set to true. 40var HelpFlag Flag = &BoolFlag{ 41 Name: "help", 42 Aliases: []string{"h"}, 43 Usage: "show help", 44} 45 46// FlagStringer converts a flag definition to a string. This is used by help 47// to display a flag. 48var FlagStringer FlagStringFunc = stringifyFlag 49 50// Serializer is used to circumvent the limitations of flag.FlagSet.Set 51type Serializer interface { 52 Serialize() string 53} 54 55// FlagNamePrefixer converts a full flag name and its placeholder into the help 56// message flag prefix. This is used by the default FlagStringer. 57var FlagNamePrefixer FlagNamePrefixFunc = prefixedNames 58 59// FlagEnvHinter annotates flag help message with the environment variable 60// details. This is used by the default FlagStringer. 61var FlagEnvHinter FlagEnvHintFunc = withEnvHint 62 63// FlagFileHinter annotates flag help message with the environment variable 64// details. This is used by the default FlagStringer. 65var FlagFileHinter FlagFileHintFunc = withFileHint 66 67// FlagsByName is a slice of Flag. 68type FlagsByName []Flag 69 70func (f FlagsByName) Len() int { 71 return len(f) 72} 73 74func (f FlagsByName) Less(i, j int) bool { 75 if len(f[j].Names()) == 0 { 76 return false 77 } else if len(f[i].Names()) == 0 { 78 return true 79 } 80 return lexicographicLess(f[i].Names()[0], f[j].Names()[0]) 81} 82 83func (f FlagsByName) Swap(i, j int) { 84 f[i], f[j] = f[j], f[i] 85} 86 87// Flag is a common interface related to parsing flags in cli. 88// For more advanced flag parsing techniques, it is recommended that 89// this interface be implemented. 90type Flag interface { 91 fmt.Stringer 92 // Apply Flag settings to the given flag set 93 Apply(*flag.FlagSet) error 94 Names() []string 95 IsSet() bool 96} 97 98// RequiredFlag is an interface that allows us to mark flags as required 99// it allows flags required flags to be backwards compatible with the Flag interface 100type RequiredFlag interface { 101 Flag 102 103 IsRequired() bool 104} 105 106// DocGenerationFlag is an interface that allows documentation generation for the flag 107type DocGenerationFlag interface { 108 Flag 109 110 // TakesValue returns true if the flag takes a value, otherwise false 111 TakesValue() bool 112 113 // GetUsage returns the usage string for the flag 114 GetUsage() string 115 116 // GetValue returns the flags value as string representation and an empty 117 // string if the flag takes no value at all. 118 GetValue() string 119} 120 121func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { 122 set := flag.NewFlagSet(name, flag.ContinueOnError) 123 124 for _, f := range flags { 125 if err := f.Apply(set); err != nil { 126 return nil, err 127 } 128 } 129 set.SetOutput(ioutil.Discard) 130 return set, nil 131} 132 133func visibleFlags(fl []Flag) []Flag { 134 var visible []Flag 135 for _, f := range fl { 136 field := flagValue(f).FieldByName("Hidden") 137 if !field.IsValid() || !field.Bool() { 138 visible = append(visible, f) 139 } 140 } 141 return visible 142} 143 144func prefixFor(name string) (prefix string) { 145 if len(name) == 1 { 146 prefix = "-" 147 } else { 148 prefix = "--" 149 } 150 151 return 152} 153 154// Returns the placeholder, if any, and the unquoted usage string. 155func unquoteUsage(usage string) (string, string) { 156 for i := 0; i < len(usage); i++ { 157 if usage[i] == '`' { 158 for j := i + 1; j < len(usage); j++ { 159 if usage[j] == '`' { 160 name := usage[i+1 : j] 161 usage = usage[:i] + name + usage[j+1:] 162 return name, usage 163 } 164 } 165 break 166 } 167 } 168 return "", usage 169} 170 171func prefixedNames(names []string, placeholder string) string { 172 var prefixed string 173 for i, name := range names { 174 if name == "" { 175 continue 176 } 177 178 prefixed += prefixFor(name) + name 179 if placeholder != "" { 180 prefixed += " " + placeholder 181 } 182 if i < len(names)-1 { 183 prefixed += ", " 184 } 185 } 186 return prefixed 187} 188 189func withEnvHint(envVars []string, str string) string { 190 envText := "" 191 if envVars != nil && len(envVars) > 0 { 192 prefix := "$" 193 suffix := "" 194 sep := ", $" 195 if runtime.GOOS == "windows" { 196 prefix = "%" 197 suffix = "%" 198 sep = "%, %" 199 } 200 201 envText = fmt.Sprintf(" [%s%s%s]", prefix, strings.Join(envVars, sep), suffix) 202 } 203 return str + envText 204} 205 206func flagNames(name string, aliases []string) []string { 207 var ret []string 208 209 for _, part := range append([]string{name}, aliases...) { 210 // v1 -> v2 migration warning zone: 211 // Strip off anything after the first found comma or space, which 212 // *hopefully* makes it a tiny bit more obvious that unexpected behavior is 213 // caused by using the v1 form of stringly typed "Name". 214 ret = append(ret, commaWhitespace.ReplaceAllString(part, "")) 215 } 216 217 return ret 218} 219 220func flagStringSliceField(f Flag, name string) []string { 221 fv := flagValue(f) 222 field := fv.FieldByName(name) 223 224 if field.IsValid() { 225 return field.Interface().([]string) 226 } 227 228 return []string{} 229} 230 231func withFileHint(filePath, str string) string { 232 fileText := "" 233 if filePath != "" { 234 fileText = fmt.Sprintf(" [%s]", filePath) 235 } 236 return str + fileText 237} 238 239func flagValue(f Flag) reflect.Value { 240 fv := reflect.ValueOf(f) 241 for fv.Kind() == reflect.Ptr { 242 fv = reflect.Indirect(fv) 243 } 244 return fv 245} 246 247func formatDefault(format string) string { 248 return " (default: " + format + ")" 249} 250 251func stringifyFlag(f Flag) string { 252 fv := flagValue(f) 253 254 switch f := f.(type) { 255 case *IntSliceFlag: 256 return withEnvHint(flagStringSliceField(f, "EnvVars"), 257 stringifyIntSliceFlag(f)) 258 case *Int64SliceFlag: 259 return withEnvHint(flagStringSliceField(f, "EnvVars"), 260 stringifyInt64SliceFlag(f)) 261 case *Float64SliceFlag: 262 return withEnvHint(flagStringSliceField(f, "EnvVars"), 263 stringifyFloat64SliceFlag(f)) 264 case *StringSliceFlag: 265 return withEnvHint(flagStringSliceField(f, "EnvVars"), 266 stringifyStringSliceFlag(f)) 267 } 268 269 placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String()) 270 271 needsPlaceholder := false 272 defaultValueString := "" 273 val := fv.FieldByName("Value") 274 if val.IsValid() { 275 needsPlaceholder = val.Kind() != reflect.Bool 276 defaultValueString = fmt.Sprintf(formatDefault("%v"), val.Interface()) 277 278 if val.Kind() == reflect.String && val.String() != "" { 279 defaultValueString = fmt.Sprintf(formatDefault("%q"), val.String()) 280 } 281 } 282 283 helpText := fv.FieldByName("DefaultText") 284 if helpText.IsValid() && helpText.String() != "" { 285 needsPlaceholder = val.Kind() != reflect.Bool 286 defaultValueString = fmt.Sprintf(formatDefault("%s"), helpText.String()) 287 } 288 289 if defaultValueString == formatDefault("") { 290 defaultValueString = "" 291 } 292 293 if needsPlaceholder && placeholder == "" { 294 placeholder = defaultPlaceholder 295 } 296 297 usageWithDefault := strings.TrimSpace(usage + defaultValueString) 298 299 return withEnvHint(flagStringSliceField(f, "EnvVars"), 300 fmt.Sprintf("%s\t%s", prefixedNames(f.Names(), placeholder), usageWithDefault)) 301} 302 303func stringifyIntSliceFlag(f *IntSliceFlag) string { 304 var defaultVals []string 305 if f.Value != nil && len(f.Value.Value()) > 0 { 306 for _, i := range f.Value.Value() { 307 defaultVals = append(defaultVals, strconv.Itoa(i)) 308 } 309 } 310 311 return stringifySliceFlag(f.Usage, f.Names(), defaultVals) 312} 313 314func stringifyInt64SliceFlag(f *Int64SliceFlag) string { 315 var defaultVals []string 316 if f.Value != nil && len(f.Value.Value()) > 0 { 317 for _, i := range f.Value.Value() { 318 defaultVals = append(defaultVals, strconv.FormatInt(i, 10)) 319 } 320 } 321 322 return stringifySliceFlag(f.Usage, f.Names(), defaultVals) 323} 324 325func stringifyFloat64SliceFlag(f *Float64SliceFlag) string { 326 var defaultVals []string 327 328 if f.Value != nil && len(f.Value.Value()) > 0 { 329 for _, i := range f.Value.Value() { 330 defaultVals = append(defaultVals, strings.TrimRight(strings.TrimRight(fmt.Sprintf("%f", i), "0"), ".")) 331 } 332 } 333 334 return stringifySliceFlag(f.Usage, f.Names(), defaultVals) 335} 336 337func stringifyStringSliceFlag(f *StringSliceFlag) string { 338 var defaultVals []string 339 if f.Value != nil && len(f.Value.Value()) > 0 { 340 for _, s := range f.Value.Value() { 341 if len(s) > 0 { 342 defaultVals = append(defaultVals, strconv.Quote(s)) 343 } 344 } 345 } 346 347 return stringifySliceFlag(f.Usage, f.Names(), defaultVals) 348} 349 350func stringifySliceFlag(usage string, names, defaultVals []string) string { 351 placeholder, usage := unquoteUsage(usage) 352 if placeholder == "" { 353 placeholder = defaultPlaceholder 354 } 355 356 defaultVal := "" 357 if len(defaultVals) > 0 { 358 defaultVal = fmt.Sprintf(formatDefault("%s"), strings.Join(defaultVals, ", ")) 359 } 360 361 usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal)) 362 return fmt.Sprintf("%s\t%s", prefixedNames(names, placeholder), usageWithDefault) 363} 364 365func hasFlag(flags []Flag, fl Flag) bool { 366 for _, existing := range flags { 367 if fl == existing { 368 return true 369 } 370 } 371 372 return false 373} 374 375func flagFromEnvOrFile(envVars []string, filePath string) (val string, ok bool) { 376 for _, envVar := range envVars { 377 envVar = strings.TrimSpace(envVar) 378 if val, ok := syscall.Getenv(envVar); ok { 379 return val, true 380 } 381 } 382 for _, fileVar := range strings.Split(filePath, ",") { 383 if data, err := ioutil.ReadFile(fileVar); err == nil { 384 return string(data), true 385 } 386 } 387 return "", false 388} 389