Repository: alecthomas/kong
Pull request #136 (2021-02-09 08:22:34)

Help Option to position flags in help output
When FlagsLast is set to true, flags are printed after commands.

- Added HelpOptions.FlagsLast bool
- Added test TestFlagsLast


Patch:
diff --git a/help.go b/help.go
index e107724..2594946 100644
--- a/help.go
+++ b/help.go
@@ -41,6 +41,9 @@ type HelpOptions struct {
 	// Tree writes command chains in a tree structure instead of listing them separately.
 	Tree bool
 
+	// Place the flags after the commands listing.
+	FlagsLast bool
+
 	// Indenter modulates the given prefix for the next layer in the tree view.
 	// The following exported templates can be used: kong.SpaceIndenter, kong.LineIndenter, kong.TreeIndenter
 	// The kong.SpaceIndenter will be used by default.
@@ -143,20 +146,25 @@ func printNodeDetail(w *helpWriter, node *Node, hide bool) {
 		w.Print("Arguments:")
 		writePositionals(w.Indent(), node.Positional)
 	}
-	if flags := node.AllFlags(true); len(flags) > 0 {
-		groupedFlags := collectFlagGroups(flags)
-		for _, group := range groupedFlags {
-			w.Print("")
-			if group.Metadata.Title != "" {
-				w.Wrap(group.Metadata.Title)
-			}
-			if group.Metadata.Description != "" {
-				w.Indent().Wrap(group.Metadata.Description)
+	printFlags := func() {
+		if flags := node.AllFlags(true); len(flags) > 0 {
+			groupedFlags := collectFlagGroups(flags)
+			for _, group := range groupedFlags {
 				w.Print("")
+				if group.Metadata.Title != "" {
+					w.Wrap(group.Metadata.Title)
+				}
+				if group.Metadata.Description != "" {
+					w.Indent().Wrap(group.Metadata.Description)
+					w.Print("")
+				}
+				writeFlags(w.Indent(), group.Flags)
 			}
-			writeFlags(w.Indent(), group.Flags)
 		}
 	}
+	if !w.FlagsLast {
+		printFlags()
+	}
 	cmds := node.Leaves(hide)
 	if len(cmds) > 0 {
 		iw := w.Indent()
@@ -184,6 +192,9 @@ func printNodeDetail(w *helpWriter, node *Node, hide bool) {
 			}
 		}
 	}
+	if w.FlagsLast {
+		printFlags()
+	}
 }
 
 func writeCommandList(cmds []*Node, iw *helpWriter) {


Test patch:
diff --git a/help_test.go b/help_test.go
index ca0ab10..c7d5fe0 100644
--- a/help_test.go
+++ b/help_test.go
@@ -126,6 +126,115 @@ Flags:
 	})
 }
 
+func TestFlagsLast(t *testing.T) {
+	// nolint: govet
+	var cli struct {
+		String   string         `help:"A string flag."`
+		Bool     bool           `help:"A bool flag with very long help that wraps a lot and is verbose and is really verbose."`
+		Slice    []string       `help:"A slice of strings." placeholder:"STR"`
+		Map      map[string]int `help:"A map of strings to ints."`
+		Required bool           `required help:"A required flag."`
+
+		One struct {
+			Flag string `help:"Nested flag."`
+		} `cmd help:"A subcommand."`
+
+		Two struct {
+			Flag        string `help:"Nested flag under two."`
+			RequiredTwo bool   `required`
+
+			Three threeArg `arg help:"Sub-sub-arg."`
+
+			Four struct {
+			} `cmd help:"Sub-sub-command."`
+		} `cmd help:"Another subcommand."`
+	}
+
+	w := bytes.NewBuffer(nil)
+	exited := false
+	app := mustNew(t, &cli,
+		kong.Name("test-app"),
+		kong.Description("A test app."),
+		kong.HelpOptions{
+			FlagsLast: true,
+		},
+		kong.Writers(w, w),
+		kong.Exit(func(int) {
+			exited = true
+			panic(true) // Panic to fake "exit".
+		}),
+	)
+
+	t.Run("Full", func(t *testing.T) {
+		require.PanicsWithValue(t, true, func() {
+			_, err := app.Parse([]string{"--help"})
+			require.NoError(t, err)
+		})
+		require.True(t, exited)
+		expected := `Usage: test-app --required <command>
+
+A test app.
+
+Commands:
+  one --required
+    A subcommand.
+
+  two <three> --required --required-two --required-three
+    Sub-sub-arg.
+
+  two four --required --required-two
+    Sub-sub-command.
+
+Flags:
+  -h, --help                 Show context-sensitive help.
+      --string=STRING        A string flag.
+      --bool                 A bool flag with very long help that wraps a lot
+                             and is verbose and is really verbose.
+      --slice=STR,...        A slice of strings.
+      --map=KEY=VALUE;...    A map of strings to ints.
+      --required             A required flag.
+
+Run "test-app <command> --help" for more information on a command.
+`
+		t.Log(w.String())
+		t.Log(expected)
+		require.Equal(t, expected, w.String())
+	})
+
+	t.Run("Selected", func(t *testing.T) {
+		exited = false
+		w.Truncate(0)
+		require.PanicsWithValue(t, true, func() {
+			_, err := app.Parse([]string{"two", "hello", "--help"})
+			require.NoError(t, err)
+		})
+		require.True(t, exited)
+		expected := `Usage: test-app two <three> --required --required-two --required-three
+
+Sub-sub-arg.
+
+Detailed help provided through the HelpProvider interface.
+
+Flags:
+  -h, --help                 Show context-sensitive help.
+      --string=STRING        A string flag.
+      --bool                 A bool flag with very long help that wraps a lot
+                             and is verbose and is really verbose.
+      --slice=STR,...        A slice of strings.
+      --map=KEY=VALUE;...    A map of strings to ints.
+      --required             A required flag.
+
+      --flag=STRING          Nested flag under two.
+      --required-two
+
+      --required-three
+`
+		t.Log(expected)
+		t.Log(w.String())
+		require.Equal(t, expected, w.String())
+	})
+}
+
 func TestHelpTree(t *testing.T) {
 	// nolint: govet
 	var cli struct {