mirror of
https://github.com/octoleo/syncthing.git
synced 2024-12-23 11:28:59 +00:00
916ec63af6
This is a new revision of the discovery server. Relevant changes and non-changes: - Protocol towards clients is unchanged. - Recommended large scale design is still to be deployed nehind nginx (I tested, and it's still a lot faster at terminating TLS). - Database backend is leveldb again, only. It scales enough, is easy to setup, and we don't need any backend to take care of. - Server supports replication. This is a simple TCP channel - protect it with a firewall when deploying over the internet. (We deploy this within the same datacenter, and with firewall.) Any incoming client announces are sent over the replication channel(s) to other peer discosrvs. Incoming replication changes are applied to the database as if they came from clients, but without the TLS/certificate overhead. - Metrics are exposed using the prometheus library, when enabled. - The database values and replication protocol is protobuf, because JSON was quite CPU intensive when I tried that and benchmarked it. - The "Retry-After" value for failed lookups gets slowly increased from a default of 120 seconds, by 5 seconds for each failed lookup, independently by each discosrv. This lowers the query load over time for clients that are never seen. The Retry-After maxes out at 3600 after a couple of weeks of this increase. The number of failed lookups is stored in the database, now and then (avoiding making each lookup a database put). All in all this means clients can be pointed towards a cluster using just multiple A / AAAA records to gain both load sharing and redundancy (if one is down, clients will talk to the remaining ones). GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4648
800 lines
20 KiB
Go
800 lines
20 KiB
Go
package cli
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"reflect"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
const defaultPlaceholder = "value"
|
|
|
|
// 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, v",
|
|
Usage: "print the version",
|
|
}
|
|
|
|
// HelpFlag prints the help for all commands and subcommands
|
|
// Set to the zero value (BoolFlag{}) to disable flag -- keeps subcommand
|
|
// unless HideHelp is set to true)
|
|
var HelpFlag Flag = BoolFlag{
|
|
Name: "help, 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
|
|
|
|
// 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 {
|
|
return f[i].GetName() < f[j].GetName()
|
|
}
|
|
|
|
func (f FlagsByName) Swap(i, j int) {
|
|
f[i], f[j] = f[j], f[i]
|
|
}
|
|
|
|
// 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)
|
|
GetName() string
|
|
}
|
|
|
|
// errorableFlag is an interface that allows us to return errors during apply
|
|
// it allows flags defined in this library to return errors in a fashion backwards compatible
|
|
// TODO remove in v2 and modify the existing Flag interface to return errors
|
|
type errorableFlag interface {
|
|
Flag
|
|
|
|
ApplyWithError(*flag.FlagSet) error
|
|
}
|
|
|
|
func flagSet(name string, flags []Flag) (*flag.FlagSet, error) {
|
|
set := flag.NewFlagSet(name, flag.ContinueOnError)
|
|
|
|
for _, f := range flags {
|
|
//TODO remove in v2 when errorableFlag is removed
|
|
if ef, ok := f.(errorableFlag); ok {
|
|
if err := ef.ApplyWithError(set); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
f.Apply(set)
|
|
}
|
|
}
|
|
return set, nil
|
|
}
|
|
|
|
func eachName(longName string, fn func(string)) {
|
|
parts := strings.Split(longName, ",")
|
|
for _, name := range parts {
|
|
name = strings.Trim(name, " ")
|
|
fn(name)
|
|
}
|
|
}
|
|
|
|
// Generic is a generic parseable type identified by a specific flag
|
|
type Generic interface {
|
|
Set(value string) error
|
|
String() string
|
|
}
|
|
|
|
// Apply takes the flagset and calls Set on the generic flag with the value
|
|
// provided by the user for parsing by the flag
|
|
// Ignores parsing errors
|
|
func (f GenericFlag) Apply(set *flag.FlagSet) {
|
|
f.ApplyWithError(set)
|
|
}
|
|
|
|
// ApplyWithError takes the flagset and calls Set on the generic flag with the value
|
|
// provided by the user for parsing by the flag
|
|
func (f GenericFlag) ApplyWithError(set *flag.FlagSet) error {
|
|
val := f.Value
|
|
if f.EnvVar != "" {
|
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
|
envVar = strings.TrimSpace(envVar)
|
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
|
if err := val.Set(envVal); err != nil {
|
|
return fmt.Errorf("could not parse %s as value for flag %s: %s", envVal, f.Name, err)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
eachName(f.Name, func(name string) {
|
|
set.Var(f.Value, name, f.Usage)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// StringSlice is an opaque type for []string to satisfy flag.Value and flag.Getter
|
|
type StringSlice []string
|
|
|
|
// Set appends the string value to the list of values
|
|
func (f *StringSlice) Set(value string) error {
|
|
*f = append(*f, value)
|
|
return nil
|
|
}
|
|
|
|
// String returns a readable representation of this value (for usage defaults)
|
|
func (f *StringSlice) String() string {
|
|
return fmt.Sprintf("%s", *f)
|
|
}
|
|
|
|
// Value returns the slice of strings set by this flag
|
|
func (f *StringSlice) Value() []string {
|
|
return *f
|
|
}
|
|
|
|
// Get returns the slice of strings set by this flag
|
|
func (f *StringSlice) Get() interface{} {
|
|
return *f
|
|
}
|
|
|
|
// Apply populates the flag given the flag set and environment
|
|
// Ignores errors
|
|
func (f StringSliceFlag) Apply(set *flag.FlagSet) {
|
|
f.ApplyWithError(set)
|
|
}
|
|
|
|
// ApplyWithError populates the flag given the flag set and environment
|
|
func (f StringSliceFlag) ApplyWithError(set *flag.FlagSet) error {
|
|
if f.EnvVar != "" {
|
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
|
envVar = strings.TrimSpace(envVar)
|
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
|
newVal := &StringSlice{}
|
|
for _, s := range strings.Split(envVal, ",") {
|
|
s = strings.TrimSpace(s)
|
|
if err := newVal.Set(s); err != nil {
|
|
return fmt.Errorf("could not parse %s as string value for flag %s: %s", envVal, f.Name, err)
|
|
}
|
|
}
|
|
f.Value = newVal
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
eachName(f.Name, func(name string) {
|
|
if f.Value == nil {
|
|
f.Value = &StringSlice{}
|
|
}
|
|
set.Var(f.Value, name, f.Usage)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// IntSlice is an opaque type for []int to satisfy flag.Value and flag.Getter
|
|
type IntSlice []int
|
|
|
|
// Set parses the value into an integer and appends it to the list of values
|
|
func (f *IntSlice) Set(value string) error {
|
|
tmp, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*f = append(*f, tmp)
|
|
return nil
|
|
}
|
|
|
|
// String returns a readable representation of this value (for usage defaults)
|
|
func (f *IntSlice) String() string {
|
|
return fmt.Sprintf("%#v", *f)
|
|
}
|
|
|
|
// Value returns the slice of ints set by this flag
|
|
func (f *IntSlice) Value() []int {
|
|
return *f
|
|
}
|
|
|
|
// Get returns the slice of ints set by this flag
|
|
func (f *IntSlice) Get() interface{} {
|
|
return *f
|
|
}
|
|
|
|
// Apply populates the flag given the flag set and environment
|
|
// Ignores errors
|
|
func (f IntSliceFlag) Apply(set *flag.FlagSet) {
|
|
f.ApplyWithError(set)
|
|
}
|
|
|
|
// ApplyWithError populates the flag given the flag set and environment
|
|
func (f IntSliceFlag) ApplyWithError(set *flag.FlagSet) error {
|
|
if f.EnvVar != "" {
|
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
|
envVar = strings.TrimSpace(envVar)
|
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
|
newVal := &IntSlice{}
|
|
for _, s := range strings.Split(envVal, ",") {
|
|
s = strings.TrimSpace(s)
|
|
if err := newVal.Set(s); err != nil {
|
|
return fmt.Errorf("could not parse %s as int slice value for flag %s: %s", envVal, f.Name, err)
|
|
}
|
|
}
|
|
f.Value = newVal
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
eachName(f.Name, func(name string) {
|
|
if f.Value == nil {
|
|
f.Value = &IntSlice{}
|
|
}
|
|
set.Var(f.Value, name, f.Usage)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Int64Slice is an opaque type for []int to satisfy flag.Value and flag.Getter
|
|
type Int64Slice []int64
|
|
|
|
// Set parses the value into an integer and appends it to the list of values
|
|
func (f *Int64Slice) Set(value string) error {
|
|
tmp, err := strconv.ParseInt(value, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*f = append(*f, tmp)
|
|
return nil
|
|
}
|
|
|
|
// String returns a readable representation of this value (for usage defaults)
|
|
func (f *Int64Slice) String() string {
|
|
return fmt.Sprintf("%#v", *f)
|
|
}
|
|
|
|
// Value returns the slice of ints set by this flag
|
|
func (f *Int64Slice) Value() []int64 {
|
|
return *f
|
|
}
|
|
|
|
// Get returns the slice of ints set by this flag
|
|
func (f *Int64Slice) Get() interface{} {
|
|
return *f
|
|
}
|
|
|
|
// Apply populates the flag given the flag set and environment
|
|
// Ignores errors
|
|
func (f Int64SliceFlag) Apply(set *flag.FlagSet) {
|
|
f.ApplyWithError(set)
|
|
}
|
|
|
|
// ApplyWithError populates the flag given the flag set and environment
|
|
func (f Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error {
|
|
if f.EnvVar != "" {
|
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
|
envVar = strings.TrimSpace(envVar)
|
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
|
newVal := &Int64Slice{}
|
|
for _, s := range strings.Split(envVal, ",") {
|
|
s = strings.TrimSpace(s)
|
|
if err := newVal.Set(s); err != nil {
|
|
return fmt.Errorf("could not parse %s as int64 slice value for flag %s: %s", envVal, f.Name, err)
|
|
}
|
|
}
|
|
f.Value = newVal
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
eachName(f.Name, func(name string) {
|
|
if f.Value == nil {
|
|
f.Value = &Int64Slice{}
|
|
}
|
|
set.Var(f.Value, name, f.Usage)
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// Apply populates the flag given the flag set and environment
|
|
// Ignores errors
|
|
func (f BoolFlag) Apply(set *flag.FlagSet) {
|
|
f.ApplyWithError(set)
|
|
}
|
|
|
|
// ApplyWithError populates the flag given the flag set and environment
|
|
func (f BoolFlag) ApplyWithError(set *flag.FlagSet) error {
|
|
val := false
|
|
if f.EnvVar != "" {
|
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
|
envVar = strings.TrimSpace(envVar)
|
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
|
if envVal == "" {
|
|
val = false
|
|
break
|
|
}
|
|
|
|
envValBool, err := strconv.ParseBool(envVal)
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err)
|
|
}
|
|
|
|
val = envValBool
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
eachName(f.Name, func(name string) {
|
|
if f.Destination != nil {
|
|
set.BoolVar(f.Destination, name, val, f.Usage)
|
|
return
|
|
}
|
|
set.Bool(name, val, f.Usage)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Apply populates the flag given the flag set and environment
|
|
// Ignores errors
|
|
func (f BoolTFlag) Apply(set *flag.FlagSet) {
|
|
f.ApplyWithError(set)
|
|
}
|
|
|
|
// ApplyWithError populates the flag given the flag set and environment
|
|
func (f BoolTFlag) ApplyWithError(set *flag.FlagSet) error {
|
|
val := true
|
|
if f.EnvVar != "" {
|
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
|
envVar = strings.TrimSpace(envVar)
|
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
|
if envVal == "" {
|
|
val = false
|
|
break
|
|
}
|
|
|
|
envValBool, err := strconv.ParseBool(envVal)
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err)
|
|
}
|
|
|
|
val = envValBool
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
eachName(f.Name, func(name string) {
|
|
if f.Destination != nil {
|
|
set.BoolVar(f.Destination, name, val, f.Usage)
|
|
return
|
|
}
|
|
set.Bool(name, val, f.Usage)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Apply populates the flag given the flag set and environment
|
|
// Ignores errors
|
|
func (f StringFlag) Apply(set *flag.FlagSet) {
|
|
f.ApplyWithError(set)
|
|
}
|
|
|
|
// ApplyWithError populates the flag given the flag set and environment
|
|
func (f StringFlag) ApplyWithError(set *flag.FlagSet) error {
|
|
if f.EnvVar != "" {
|
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
|
envVar = strings.TrimSpace(envVar)
|
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
|
f.Value = envVal
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
eachName(f.Name, func(name string) {
|
|
if f.Destination != nil {
|
|
set.StringVar(f.Destination, name, f.Value, f.Usage)
|
|
return
|
|
}
|
|
set.String(name, f.Value, f.Usage)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Apply populates the flag given the flag set and environment
|
|
// Ignores errors
|
|
func (f IntFlag) Apply(set *flag.FlagSet) {
|
|
f.ApplyWithError(set)
|
|
}
|
|
|
|
// ApplyWithError populates the flag given the flag set and environment
|
|
func (f IntFlag) ApplyWithError(set *flag.FlagSet) error {
|
|
if f.EnvVar != "" {
|
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
|
envVar = strings.TrimSpace(envVar)
|
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
|
envValInt, err := strconv.ParseInt(envVal, 0, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err)
|
|
}
|
|
f.Value = int(envValInt)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
eachName(f.Name, func(name string) {
|
|
if f.Destination != nil {
|
|
set.IntVar(f.Destination, name, f.Value, f.Usage)
|
|
return
|
|
}
|
|
set.Int(name, f.Value, f.Usage)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Apply populates the flag given the flag set and environment
|
|
// Ignores errors
|
|
func (f Int64Flag) Apply(set *flag.FlagSet) {
|
|
f.ApplyWithError(set)
|
|
}
|
|
|
|
// ApplyWithError populates the flag given the flag set and environment
|
|
func (f Int64Flag) ApplyWithError(set *flag.FlagSet) error {
|
|
if f.EnvVar != "" {
|
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
|
envVar = strings.TrimSpace(envVar)
|
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
|
envValInt, err := strconv.ParseInt(envVal, 0, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err)
|
|
}
|
|
|
|
f.Value = envValInt
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
eachName(f.Name, func(name string) {
|
|
if f.Destination != nil {
|
|
set.Int64Var(f.Destination, name, f.Value, f.Usage)
|
|
return
|
|
}
|
|
set.Int64(name, f.Value, f.Usage)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Apply populates the flag given the flag set and environment
|
|
// Ignores errors
|
|
func (f UintFlag) Apply(set *flag.FlagSet) {
|
|
f.ApplyWithError(set)
|
|
}
|
|
|
|
// ApplyWithError populates the flag given the flag set and environment
|
|
func (f UintFlag) ApplyWithError(set *flag.FlagSet) error {
|
|
if f.EnvVar != "" {
|
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
|
envVar = strings.TrimSpace(envVar)
|
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
|
envValInt, err := strconv.ParseUint(envVal, 0, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse %s as uint value for flag %s: %s", envVal, f.Name, err)
|
|
}
|
|
|
|
f.Value = uint(envValInt)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
eachName(f.Name, func(name string) {
|
|
if f.Destination != nil {
|
|
set.UintVar(f.Destination, name, f.Value, f.Usage)
|
|
return
|
|
}
|
|
set.Uint(name, f.Value, f.Usage)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Apply populates the flag given the flag set and environment
|
|
// Ignores errors
|
|
func (f Uint64Flag) Apply(set *flag.FlagSet) {
|
|
f.ApplyWithError(set)
|
|
}
|
|
|
|
// ApplyWithError populates the flag given the flag set and environment
|
|
func (f Uint64Flag) ApplyWithError(set *flag.FlagSet) error {
|
|
if f.EnvVar != "" {
|
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
|
envVar = strings.TrimSpace(envVar)
|
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
|
envValInt, err := strconv.ParseUint(envVal, 0, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse %s as uint64 value for flag %s: %s", envVal, f.Name, err)
|
|
}
|
|
|
|
f.Value = uint64(envValInt)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
eachName(f.Name, func(name string) {
|
|
if f.Destination != nil {
|
|
set.Uint64Var(f.Destination, name, f.Value, f.Usage)
|
|
return
|
|
}
|
|
set.Uint64(name, f.Value, f.Usage)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Apply populates the flag given the flag set and environment
|
|
// Ignores errors
|
|
func (f DurationFlag) Apply(set *flag.FlagSet) {
|
|
f.ApplyWithError(set)
|
|
}
|
|
|
|
// ApplyWithError populates the flag given the flag set and environment
|
|
func (f DurationFlag) ApplyWithError(set *flag.FlagSet) error {
|
|
if f.EnvVar != "" {
|
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
|
envVar = strings.TrimSpace(envVar)
|
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
|
envValDuration, err := time.ParseDuration(envVal)
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse %s as duration for flag %s: %s", envVal, f.Name, err)
|
|
}
|
|
|
|
f.Value = envValDuration
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
eachName(f.Name, func(name string) {
|
|
if f.Destination != nil {
|
|
set.DurationVar(f.Destination, name, f.Value, f.Usage)
|
|
return
|
|
}
|
|
set.Duration(name, f.Value, f.Usage)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Apply populates the flag given the flag set and environment
|
|
// Ignores errors
|
|
func (f Float64Flag) Apply(set *flag.FlagSet) {
|
|
f.ApplyWithError(set)
|
|
}
|
|
|
|
// ApplyWithError populates the flag given the flag set and environment
|
|
func (f Float64Flag) ApplyWithError(set *flag.FlagSet) error {
|
|
if f.EnvVar != "" {
|
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
|
envVar = strings.TrimSpace(envVar)
|
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
|
envValFloat, err := strconv.ParseFloat(envVal, 10)
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse %s as float64 value for flag %s: %s", envVal, f.Name, err)
|
|
}
|
|
|
|
f.Value = float64(envValFloat)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
eachName(f.Name, func(name string) {
|
|
if f.Destination != nil {
|
|
set.Float64Var(f.Destination, name, f.Value, f.Usage)
|
|
return
|
|
}
|
|
set.Float64(name, f.Value, f.Usage)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func visibleFlags(fl []Flag) []Flag {
|
|
visible := []Flag{}
|
|
for _, flag := range fl {
|
|
field := flagValue(flag).FieldByName("Hidden")
|
|
if !field.IsValid() || !field.Bool() {
|
|
visible = append(visible, flag)
|
|
}
|
|
}
|
|
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(fullName, placeholder string) string {
|
|
var prefixed string
|
|
parts := strings.Split(fullName, ",")
|
|
for i, name := range parts {
|
|
name = strings.Trim(name, " ")
|
|
prefixed += prefixFor(name) + name
|
|
if placeholder != "" {
|
|
prefixed += " " + placeholder
|
|
}
|
|
if i < len(parts)-1 {
|
|
prefixed += ", "
|
|
}
|
|
}
|
|
return prefixed
|
|
}
|
|
|
|
func withEnvHint(envVar, str string) string {
|
|
envText := ""
|
|
if envVar != "" {
|
|
prefix := "$"
|
|
suffix := ""
|
|
sep := ", $"
|
|
if runtime.GOOS == "windows" {
|
|
prefix = "%"
|
|
suffix = "%"
|
|
sep = "%, %"
|
|
}
|
|
envText = fmt.Sprintf(" [%s%s%s]", prefix, strings.Join(strings.Split(envVar, ","), sep), suffix)
|
|
}
|
|
return str + envText
|
|
}
|
|
|
|
func flagValue(f Flag) reflect.Value {
|
|
fv := reflect.ValueOf(f)
|
|
for fv.Kind() == reflect.Ptr {
|
|
fv = reflect.Indirect(fv)
|
|
}
|
|
return fv
|
|
}
|
|
|
|
func stringifyFlag(f Flag) string {
|
|
fv := flagValue(f)
|
|
|
|
switch f.(type) {
|
|
case IntSliceFlag:
|
|
return withEnvHint(fv.FieldByName("EnvVar").String(),
|
|
stringifyIntSliceFlag(f.(IntSliceFlag)))
|
|
case Int64SliceFlag:
|
|
return withEnvHint(fv.FieldByName("EnvVar").String(),
|
|
stringifyInt64SliceFlag(f.(Int64SliceFlag)))
|
|
case StringSliceFlag:
|
|
return withEnvHint(fv.FieldByName("EnvVar").String(),
|
|
stringifyStringSliceFlag(f.(StringSliceFlag)))
|
|
}
|
|
|
|
placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String())
|
|
|
|
needsPlaceholder := false
|
|
defaultValueString := ""
|
|
|
|
if val := fv.FieldByName("Value"); val.IsValid() {
|
|
needsPlaceholder = true
|
|
defaultValueString = fmt.Sprintf(" (default: %v)", val.Interface())
|
|
|
|
if val.Kind() == reflect.String && val.String() != "" {
|
|
defaultValueString = fmt.Sprintf(" (default: %q)", val.String())
|
|
}
|
|
}
|
|
|
|
if defaultValueString == " (default: )" {
|
|
defaultValueString = ""
|
|
}
|
|
|
|
if needsPlaceholder && placeholder == "" {
|
|
placeholder = defaultPlaceholder
|
|
}
|
|
|
|
usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultValueString))
|
|
|
|
return withEnvHint(fv.FieldByName("EnvVar").String(),
|
|
fmt.Sprintf("%s\t%s", prefixedNames(fv.FieldByName("Name").String(), placeholder), usageWithDefault))
|
|
}
|
|
|
|
func stringifyIntSliceFlag(f IntSliceFlag) string {
|
|
defaultVals := []string{}
|
|
if f.Value != nil && len(f.Value.Value()) > 0 {
|
|
for _, i := range f.Value.Value() {
|
|
defaultVals = append(defaultVals, fmt.Sprintf("%d", i))
|
|
}
|
|
}
|
|
|
|
return stringifySliceFlag(f.Usage, f.Name, defaultVals)
|
|
}
|
|
|
|
func stringifyInt64SliceFlag(f Int64SliceFlag) string {
|
|
defaultVals := []string{}
|
|
if f.Value != nil && len(f.Value.Value()) > 0 {
|
|
for _, i := range f.Value.Value() {
|
|
defaultVals = append(defaultVals, fmt.Sprintf("%d", i))
|
|
}
|
|
}
|
|
|
|
return stringifySliceFlag(f.Usage, f.Name, defaultVals)
|
|
}
|
|
|
|
func stringifyStringSliceFlag(f StringSliceFlag) string {
|
|
defaultVals := []string{}
|
|
if f.Value != nil && len(f.Value.Value()) > 0 {
|
|
for _, s := range f.Value.Value() {
|
|
if len(s) > 0 {
|
|
defaultVals = append(defaultVals, fmt.Sprintf("%q", s))
|
|
}
|
|
}
|
|
}
|
|
|
|
return stringifySliceFlag(f.Usage, f.Name, defaultVals)
|
|
}
|
|
|
|
func stringifySliceFlag(usage, name string, defaultVals []string) string {
|
|
placeholder, usage := unquoteUsage(usage)
|
|
if placeholder == "" {
|
|
placeholder = defaultPlaceholder
|
|
}
|
|
|
|
defaultVal := ""
|
|
if len(defaultVals) > 0 {
|
|
defaultVal = fmt.Sprintf(" (default: %s)", strings.Join(defaultVals, ", "))
|
|
}
|
|
|
|
usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal))
|
|
return fmt.Sprintf("%s\t%s", prefixedNames(name, placeholder), usageWithDefault)
|
|
}
|