Repository: urfave/cli (go)

Issue:
fix categorized flags alongside non categorized
## My urfave/cli version is

latest v2

## Checklist

- [X] Are you running the latest v2 release? The list of releases is [here](https://github.com/urfave/cli/releases).
- [X] Did you check the manual for your release? The v2 manual is [here](https://github.com/urfave/cli/blob/main/docs/v2/manual.md)
- [X] Did you perform a search about this problem? Here's the [GitHub guide](https://help.github.com/en/github/managing-your-work-on-github/using-search-to-filter-issues-and-pull-requests) about searching.

## Describe the bug

Commit: https://github.com/urfave/cli/commit/8227be1fe9df91e3d69e17514df21b8e609d6cd0 
introduced a backward compatibility issue.  As before non categorized flags would show alongside categorized flags.  After this commit non categorized do not show at all.


## To reproduce

TestCommand_VisibleFlagCategories is a direct test of the breaking change, Just because no category is set, the flag should not be invisible.

## Expected behavior

Flags should show in help unless explicitly hidden

## Want to fix this yourself?

Sure, I'll introduce a PR shortly

## Run `go version` and paste its output here

```
go version go1.19.4 linux/amd64
```


Pull request:
Show non categorized flags with categorized on help
## What type of PR is this?

- bug
- feature

## What this PR does / why we need it:

Reverting the removal of visible flags from help if their sibling flags have categories defined.

Example:
```
App:
    Flags:
        Flag1: // non categorized
        Flag2: Category1
        
Help Before PR:
    Options:
      Category1
        Flag2
        
Help After:
    Options:
        Flag1
      
      Category1
        Flag2
```

## Which issue(s) this PR fixes:

fixes #1672 

## Release Notes

```release-note
Reverted removal of non categorized flags alongside categorized flags
```


Patch:
diff --git a/app.go b/app.go
index 10198f4..9512392 100644
--- a/app.go
+++ b/app.go
@@ -250,14 +250,7 @@ func (a *App) Setup() {
 	}
 	sort.Sort(a.categories.(*commandCategories))
 
-	a.flagCategories = newFlagCategories()
-	for _, fl := range a.Flags {
-		if cf, ok := fl.(CategorizableFlag); ok {
-			if cf.GetCategory() != "" {
-				a.flagCategories.AddFlag(cf.GetCategory(), cf)
-			}
-		}
-	}
+	a.flagCategories = newFlagCategoriesFromFlags(a.Flags)
 
 	if a.Metadata == nil {
 		a.Metadata = make(map[string]interface{})
diff --git a/category.go b/category.go
index 7aca0c7..ccc043c 100644
--- a/category.go
+++ b/category.go
@@ -100,10 +100,23 @@ func newFlagCategories() FlagCategories {
 
 func newFlagCategoriesFromFlags(fs []Flag) FlagCategories {
 	fc := newFlagCategories()
+
+	var categorized bool
 	for _, fl := range fs {
 		if cf, ok := fl.(CategorizableFlag); ok {
-			if cf.GetCategory() != "" {
-				fc.AddFlag(cf.GetCategory(), cf)
+			if cat := cf.GetCategory(); cat != "" {
+				fc.AddFlag(cat, cf)
+				categorized = true
+			}
+		}
+	}
+
+	if categorized == true {
+		for _, fl := range fs {
+			if cf, ok := fl.(CategorizableFlag); ok {
+				if cf.GetCategory() == "" {
+					fc.AddFlag("", fl)
+				}
 			}
 		}
 	}


Test patch:
diff --git a/app_test.go b/app_test.go
index 7f2a214..d2cbb67 100644
--- a/app_test.go
+++ b/app_test.go
@@ -2303,37 +2303,6 @@ func TestApp_VisibleCategories(t *testing.T) {
 	expect(t, []CommandCategory{}, app.VisibleCategories())
 }
 
-func TestApp_VisibleFlagCategories(t *testing.T) {
-	app := &App{
-		Flags: []Flag{
-			&StringFlag{
-				Name: "strd", // no category set
-			},
-			&Int64Flag{
-				Name:     "intd",
-				Aliases:  []string{"altd1", "altd2"},
-				Category: "cat1",
-			},
-		},
-	}
-	app.Setup()
-	vfc := app.VisibleFlagCategories()
-	if len(vfc) != 1 {
-		t.Fatalf("unexpected visible flag categories %+v", vfc)
-	}
-	if vfc[0].Name() != "cat1" {
-		t.Errorf("expected category name cat1 got %s", vfc[0].Name())
-	}
-	if len(vfc[0].Flags()) != 1 {
-		t.Fatalf("expected flag category to have just one flag got %+v", vfc[0].Flags())
-	}
-
-	fl := vfc[0].Flags()[0]
-	if !reflect.DeepEqual(fl.Names(), []string{"intd", "altd1", "altd2"}) {
-		t.Errorf("unexpected flag %+v", fl.Names())
-	}
-}
-
 func TestApp_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) {
 	app := &App{
 		Action: func(c *Context) error { return nil },
diff --git a/command_test.go b/command_test.go
index 478c325..70caee7 100644
--- a/command_test.go
+++ b/command_test.go
@@ -470,17 +470,17 @@ func TestCommand_VisibleFlagCategories(t *testing.T) {
 	}
 
 	vfc := c.VisibleFlagCategories()
-	if len(vfc) != 1 {
+	if len(vfc) != 2 {
 		t.Fatalf("unexpected visible flag categories %+v", vfc)
 	}
-	if vfc[0].Name() != "cat1" {
+	if vfc[1].Name() != "cat1" {
 		t.Errorf("expected category name cat1 got %s", vfc[0].Name())
 	}
-	if len(vfc[0].Flags()) != 1 {
+	if len(vfc[1].Flags()) != 1 {
 		t.Fatalf("expected flag category to have just one flag got %+v", vfc[0].Flags())
 	}
 
-	fl := vfc[0].Flags()[0]
+	fl := vfc[1].Flags()[0]
 	if !reflect.DeepEqual(fl.Names(), []string{"intd", "altd1", "altd2"}) {
 		t.Errorf("unexpected flag %+v", fl.Names())
 	}
diff --git a/help_test.go b/help_test.go
index 0a2d9f9..8e9396d 100644
--- a/help_test.go
+++ b/help_test.go
@@ -1520,3 +1520,68 @@ OPTIONS:
 			output.String(), expected)
 	}
 }
+
+func TestCategorizedHelp(t *testing.T) {
+	// Reset HelpPrinter after this test.
+	defer func(old helpPrinter) {
+		HelpPrinter = old
+	}(HelpPrinter)
+
+	output := new(bytes.Buffer)
+	app := &App{
+		Name:   "cli.test",
+		Writer: output,
+		Action: func(ctx *Context) error { return nil },
+		Flags: []Flag{
+			&StringFlag{
+				Name: "strd", // no category set
+			},
+			&Int64Flag{
+				Name:     "intd",
+				Aliases:  []string{"altd1", "altd2"},
+				Category: "cat1",
+			},
+		},
+	}
+
+	c := NewContext(app, nil, nil)
+	app.Setup()
+
+	HelpPrinter = func(w io.Writer, templ string, data interface{}) {
+		funcMap := map[string]interface{}{
+			"wrapAt": func() int {
+				return 30
+			},
+		}
+
+		HelpPrinterCustom(w, templ, data, funcMap)
+	}
+
+	_ = ShowAppHelp(c)
+
+	expected := `NAME:
+   cli.test - A new cli
+              application
+
+USAGE:
+   cli.test [global options] command [command options] [arguments...]
+
+COMMANDS:
+   help, h  Shows a list of
+            commands or help
+            for one command
+
+GLOBAL OPTIONS:
+   --help, -h    show help
+   --strd value  
+
+   cat1
+
+   --intd value, --altd1 value, --altd2 value  (default: 0)
+
+`
+	if output.String() != expected {
+		t.Errorf("Unexpected wrapping, got:\n%s\nexpected:\n%s",
+			output.String(), expected)
+	}
+}