diff --git a/app_test.go b/app_test.go index f6f9158b05..3e25718442 100644 --- a/app_test.go +++ b/app_test.go @@ -283,7 +283,8 @@ func ExampleApp_Run_bashComplete_withLongFlag() { Aliases: []string{"x"}, }, &StringFlag{ - Name: "some-flag,s", + Name: "some-flag", + Aliases: []string{"s"}, }, &StringFlag{ Name: "similar-flag", @@ -295,6 +296,29 @@ func ExampleApp_Run_bashComplete_withLongFlag() { // --some-flag // --similar-flag } + +func TestApp_Run_ErrorsOnLegacyV1FlagAliasSyntax(t *testing.T) { + app := &App{ + Name: "greet", + Flags: []Flag{ + &StringFlag{ + Name: "config, cfg", + }, + }, + } + + err := app.Run([]string{"greet"}) + if err == nil { + t.Fatalf("expected an error for legacy alias syntax, got nil") + } + + if !strings.Contains(err.Error(), "invalid flag name") { + t.Fatalf("expected invalid flag name error, got %q", err) + } + if !strings.Contains(err.Error(), "Aliases") { + t.Fatalf("expected alias migration hint in error, got %q", err) + } +} func ExampleApp_Run_bashComplete_withMultipleLongFlag() { os.Setenv("SHELL", "bash") os.Args = []string{"greet", "--st", "--generate-bash-completion"} diff --git a/flag.go b/flag.go index 4d04de3da8..6368bccf06 100644 --- a/flag.go +++ b/flag.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "reflect" "regexp" "runtime" "strings" @@ -26,6 +27,13 @@ var ( commaWhitespace = regexp.MustCompile("[, ]+.*") ) +func validateFlagName(name string) error { + if name != "" && (strings.Contains(name, ",") || strings.Contains(name, " ")) { + return fmt.Errorf("invalid flag name %q: move alternate names to Aliases", name) + } + return nil +} + // BashCompletionFlag enables bash-completion for all commands and subcommands var BashCompletionFlag Flag = &BoolFlag{ Name: "generate-bash-completion", @@ -167,10 +175,46 @@ type Countable interface { Count() int } +func rawFlagName(f any) (string, bool) { + if f == nil { + return "", false + } + + rv := reflect.ValueOf(f) + for rv.Kind() == reflect.Pointer { + if rv.IsNil() { + return "", false + } + rv = rv.Elem() + } + + if rv.Kind() != reflect.Struct { + return "", false + } + + if target := rv.FieldByName("Target"); target.IsValid() { + if name, ok := rawFlagName(target.Interface()); ok { + return name, true + } + } + + nameField := rv.FieldByName("Name") + if !nameField.IsValid() || nameField.Kind() != reflect.String { + return "", false + } + + return nameField.String(), true +} + func flagSet(name string, flags []Flag, spec separatorSpec) (*flag.FlagSet, error) { set := flag.NewFlagSet(name, flag.ContinueOnError) for _, f := range flags { + if name, ok := rawFlagName(f); ok { + if err := validateFlagName(name); err != nil { + return nil, err + } + } if c, ok := f.(customizedSeparator); ok { c.WithSeparatorSpec(spec) }