Repository: jawher/mow.cli (go)

Issue:
Add support to make a command hidden in cli
Currently, I couldn't find a way to make a command hidden in cli. As soon as the app name is printed, all commands are listed down.

Pull request:
Add a way to hide commands in the help messages
```go
app.Command("login", "login to the backend (DEPRECATED: please use auth instead)", func(cmd *cli.Cmd)) {
	cmd.Hidden = true
}
```

Fixes #99

Patch:
diff --git a/README.md b/README.md
index c713b63..3532925 100644
--- a/README.md
+++ b/README.md
@@ -616,6 +616,16 @@ $ docker job log show
 $ docker job log clear
 ```
 
+Commands can be hidden in the help messages.
+This can prove useful to deprecate a command so that it does not appear to new users in the help, but still exists to not break existing scripts.
+To hide a command, set the Hidden field to true:
+
+```
+app.Command("login", "login to the backend (DEPRECATED: please use auth instead)", func(cmd *cli.Cmd)) {
+    cmd.Hidden = true
+}
+```
+
 As a convenience, to assign an Action to a func with no arguments, use
 ActionCommand when defining the Command. For example, the following two
 statements are equivalent:
@@ -1062,4 +1072,4 @@ This work is published under the MIT license.
 Please see the `LICENSE` file for details.
 
 * * *
-Automatically generated by [autoreadme](https://github.com/jawher/autoreadme) on 2020.07.19
+Automatically generated by [autoreadme](https://github.com/jawher/autoreadme) on 2020.08.08
diff --git a/commands.go b/commands.go
index 775c52e..e737793 100644
--- a/commands.go
+++ b/commands.go
@@ -31,6 +31,8 @@ type Cmd struct {
 	Spec string
 	// The command long description to be shown when help is requested
 	LongDesc string
+	// Hide this command in the help messages
+	Hidden bool
 	// The command error handling strategy
 	ErrorHandling flag.ErrorHandling
 
@@ -563,15 +565,28 @@ func (c *Cmd) printHelp(longDesc bool) {
 		}
 	}
 
-	if len(c.commands) > 0 {
+	commands := make([]*Cmd, 0, len(c.commands))
+	for _, c := range c.commands {
+		if err := c.doInit(); err != nil {
+			panic(err)
+		}
+
+		if c.Hidden {
+			continue
+		}
+
+		commands = append(commands, c)
+	}
+
+	if len(commands) > 0 {
 		fmt.Fprint(w, "\t\nCommands:\t\n")
 
-		for _, c := range c.commands {
+		for _, c := range commands {
 			fmt.Fprintf(w, "  %s\t%s\n", strings.Join(c.aliases, ", "), c.desc)
 		}
 	}
 
-	if len(c.commands) > 0 {
+	if len(commands) > 0 {
 		fmt.Fprintf(w, "\t\nRun '%s COMMAND --help' for more information on a command.\n", path)
 	}
 
diff --git a/doc.go b/doc.go
index 42ab57e..979e727 100644
--- a/doc.go
+++ b/doc.go
@@ -288,6 +288,14 @@ of ways:
     $ docker job log show
     $ docker job log clear
 
+Commands can be hidden in the help messages.
+This can prove useful to deprecate a command so that it does not appear to new users in the help, but still exists to not break existing scripts.
+To hide a command, set the Hidden field to true:
+
+    app.Command("login", "login to the backend (DEPRECATED: please use auth instead)", func(cmd *cli.Cmd)) {
+        cmd.Hidden = true
+    }
+
 As a convenience, to assign an Action to a func with no arguments, use
 ActionCommand when defining the Command. For example, the following two
 statements are equivalent:


Test patch:
diff --git a/cli_test.go b/cli_test.go
index d750eb1..c83175d 100644
--- a/cli_test.go
+++ b/cli_test.go
@@ -780,82 +780,117 @@ func testHelpAndVersionWithOptionsEnd(flag string, t *testing.T) {
 
 func TestHelpAndVersionWithOptionsEnd(t *testing.T) {
 	for _, flag := range []string{"-h", "--help", "-v", "--version"} {
-		testHelpAndVersionWithOptionsEnd(flag, t)
+		t.Run(flag, func(t *testing.T) {
+			testHelpAndVersionWithOptionsEnd(flag, t)
+		})
 	}
 }
 
 var genGolden = flag.Bool("g", false, "Generate golden file(s)")
 
 func TestHelpMessage(t *testing.T) {
-	var out, err string
-	defer captureAndRestoreOutput(&out, &err)()
-
-	exitCalled := false
-	defer exitShouldBeCalledWith(t, 0, &exitCalled)()
-
-	app := App("app", "App Desc")
-	app.Spec = "[-bdsuikqs] BOOL1 [STR1] INT3..."
-
-	// Options
-	app.Bool(BoolOpt{Name: "b bool1 u uuu", Value: false, EnvVar: "BOOL1", Desc: "Bool Option 1"})
-	app.Bool(BoolOpt{Name: "bool2", Value: true, EnvVar: " ", Desc: "Bool Option 2"})
-	app.Bool(BoolOpt{Name: "d", Value: true, EnvVar: "BOOL3", Desc: "Bool Option 3", HideValue: true})
-
-	app.String(StringOpt{Name: "s str1", Value: "", EnvVar: "STR1", Desc: "String Option 1"})
-	app.String(StringOpt{Name: "str2", Value: "a value", Desc: "String Option 2"})
-	app.String(StringOpt{Name: "v", Value: "another value", EnvVar: "STR3", Desc: "String Option 3", HideValue: true})
-
-	app.Int(IntOpt{Name: "i int1", Value: 0, EnvVar: "INT1 ALIAS_INT1"})
-	app.Int(IntOpt{Name: "int2", Value: 1, EnvVar: "INT2", Desc: "Int Option 2"})
-	app.Int(IntOpt{Name: "k", Value: 1, EnvVar: "INT3", Desc: "Int Option 3", HideValue: true})
-
-	app.Strings(StringsOpt{Name: "x strs1", Value: nil, EnvVar: "STRS1", Desc: "Strings Option 1"})
-	app.Strings(StringsOpt{Name: "strs2", Value: []string{"value1", "value2"}, EnvVar: "STRS2", Desc: "Strings Option 2"})
-	app.Strings(StringsOpt{Name: "z", Value: []string{"another value"}, EnvVar: "STRS3", Desc: "Strings Option 3", HideValue: true})
-
-	app.Ints(IntsOpt{Name: "q ints1", Value: nil, EnvVar: "INTS1", Desc: "Ints Option 1"})
-	app.Ints(IntsOpt{Name: "ints2", Value: []int{1, 2, 3}, EnvVar: "INTS2", Desc: "Ints Option 2"})
-	app.Ints(IntsOpt{Name: "j", Value: []int{1}, EnvVar: "INTS3", Desc: "Ints Option 3", HideValue: true})
-
-	// Args
-	app.Bool(BoolArg{Name: "BOOL1", Value: false, EnvVar: "BOOL1", Desc: "Bool Argument 1"})
-	app.Bool(BoolArg{Name: "BOOL2", Value: true, Desc: "Bool Argument 2"})
-	app.Bool(BoolArg{Name: "BOOL3", Value: true, EnvVar: "BOOL3", Desc: "Bool Argument 3", HideValue: true})
-
-	app.String(StringArg{Name: "STR1", Value: "", EnvVar: "STR1", Desc: "String Argument 1"})
-	app.String(StringArg{Name: "STR2", Value: "a value", EnvVar: "STR2", Desc: "String Argument 2"})
-	app.String(StringArg{Name: "STR3", Value: "another value", EnvVar: "STR3", Desc: "String Argument 3", HideValue: true})
-
-	app.Int(IntArg{Name: "INT1", Value: 0, EnvVar: "INT1", Desc: "Int Argument 1"})
-	app.Int(IntArg{Name: "INT2", Value: 1, EnvVar: "INT2", Desc: "Int Argument 2"})
-	app.Int(IntArg{Name: "INT3", Value: 1, EnvVar: "INT3", Desc: "Int Argument 3", HideValue: true})
-
-	app.Strings(StringsArg{Name: "STRS1", Value: nil, EnvVar: "STRS1", Desc: "Strings Argument 1"})
-	app.Strings(StringsArg{Name: "STRS2", Value: []string{"value1", "value2"}, EnvVar: "STRS2"})
-	app.Strings(StringsArg{Name: "STRS3", Value: []string{"another value"}, EnvVar: "STRS3", Desc: "Strings Argument 3", HideValue: true})
+	cases := []struct {
+		name   string
+		params []string
+	}{
+		{name: "top-help", params: []string{"app", "-h"}},
+		{name: "command1", params: []string{"app", "command1", "-h"}},
+		{name: "command2", params: []string{"app", "command2", "-h"}},
+		{name: "command3", params: []string{"app", "command3", "-h"}},
+		{name: "command3-child1", params: []string{"app", "command3", "child1", "-h"}},
+		{name: "command3-child2", params: []string{"app", "command3", "child2", "-h"}},
+		{name: "command4", params: []string{"app", "command4", "-h"}},
+	}
+	for _, cas := range cases {
+		cas := cas
+		t.Run(cas.name, func(t *testing.T) {
+			t.Logf("case: %+v", cas)
+			var out, stdErr string
+			defer captureAndRestoreOutput(&out, &stdErr)()
+
+			exitCalled := false
+			defer exitShouldBeCalledWith(t, 0, &exitCalled)()
+
+			app := App("app", "App Desc")
+			app.Spec = "[-bdsuikqs] [BOOL1 STR1 INT3...]"
+
+			// Options
+			app.Bool(BoolOpt{Name: "b bool1 u uuu", Value: false, EnvVar: "BOOL1", Desc: "Bool Option 1"})
+			app.Bool(BoolOpt{Name: "bool2", Value: true, EnvVar: " ", Desc: "Bool Option 2"})
+			app.Bool(BoolOpt{Name: "d", Value: true, EnvVar: "BOOL3", Desc: "Bool Option 3", HideValue: true})
+
+			app.String(StringOpt{Name: "s str1", Value: "", EnvVar: "STR1", Desc: "String Option 1"})
+			app.String(StringOpt{Name: "str2", Value: "a value", Desc: "String Option 2"})
+			app.String(StringOpt{Name: "v", Value: "another value", EnvVar: "STR3", Desc: "String Option 3", HideValue: true})
+
+			app.Int(IntOpt{Name: "i int1", Value: 0, EnvVar: "INT1 ALIAS_INT1"})
+			app.Int(IntOpt{Name: "int2", Value: 1, EnvVar: "INT2", Desc: "Int Option 2"})
+			app.Int(IntOpt{Name: "k", Value: 1, EnvVar: "INT3", Desc: "Int Option 3", HideValue: true})
+
+			app.Strings(StringsOpt{Name: "x strs1", Value: nil, EnvVar: "STRS1", Desc: "Strings Option 1"})
+			app.Strings(StringsOpt{Name: "strs2", Value: []string{"value1", "value2"}, EnvVar: "STRS2", Desc: "Strings Option 2"})
+			app.Strings(StringsOpt{Name: "z", Value: []string{"another value"}, EnvVar: "STRS3", Desc: "Strings Option 3", HideValue: true})
+
+			app.Ints(IntsOpt{Name: "q ints1", Value: nil, EnvVar: "INTS1", Desc: "Ints Option 1"})
+			app.Ints(IntsOpt{Name: "ints2", Value: []int{1, 2, 3}, EnvVar: "INTS2", Desc: "Ints Option 2"})
+			app.Ints(IntsOpt{Name: "j", Value: []int{1}, EnvVar: "INTS3", Desc: "Ints Option 3", HideValue: true})
+
+			// Args
+			app.Bool(BoolArg{Name: "BOOL1", Value: false, EnvVar: "BOOL1", Desc: "Bool Argument 1"})
+			app.Bool(BoolArg{Name: "BOOL2", Value: true, Desc: "Bool Argument 2"})
+			app.Bool(BoolArg{Name: "BOOL3", Value: true, EnvVar: "BOOL3", Desc: "Bool Argument 3", HideValue: true})
+
+			app.String(StringArg{Name: "STR1", Value: "", EnvVar: "STR1", Desc: "String Argument 1"})
+			app.String(StringArg{Name: "STR2", Value: "a value", EnvVar: "STR2", Desc: "String Argument 2"})
+			app.String(StringArg{Name: "STR3", Value: "another value", EnvVar: "STR3", Desc: "String Argument 3", HideValue: true})
+
+			app.Int(IntArg{Name: "INT1", Value: 0, EnvVar: "INT1", Desc: "Int Argument 1"})
+			app.Int(IntArg{Name: "INT2", Value: 1, EnvVar: "INT2", Desc: "Int Argument 2"})
+			app.Int(IntArg{Name: "INT3", Value: 1, EnvVar: "INT3", Desc: "Int Argument 3", HideValue: true})
+
+			app.Strings(StringsArg{Name: "STRS1", Value: nil, EnvVar: "STRS1", Desc: "Strings Argument 1"})
+			app.Strings(StringsArg{Name: "STRS2", Value: []string{"value1", "value2"}, EnvVar: "STRS2"})
+			app.Strings(StringsArg{Name: "STRS3", Value: []string{"another value"}, EnvVar: "STRS3", Desc: "Strings Argument 3", HideValue: true})
+
+			app.Ints(IntsArg{Name: "INTS1", Value: nil, EnvVar: "INTS1", Desc: "Ints Argument 1"})
+			app.Ints(IntsArg{Name: "INTS2", Value: []int{1, 2, 3}, EnvVar: "INTS2", Desc: "Ints Argument 2"})
+			app.Ints(IntsArg{Name: "INTS3", Value: []int{1}, EnvVar: "INTS3", Desc: "Ints Argument 3", HideValue: true})
+
+			app.Action = func() {}
+
+			app.Command("command1", "command1 description", func(cmd *Cmd) {})
+			app.Command("command2", "command2 description", func(cmd *Cmd) {})
+			app.Command("command3", "command3 description", func(cmd *Cmd) {
+				cmd.Command("child1", "child1 description", func(cmd *Cmd) {
+					cmd.StringArg("ARG1", "", "arg1 desc")
+				})
+				cmd.Command("child2", "child2 description", func(cmd *Cmd) {
+					cmd.Hidden = true
+					cmd.StringOpt("o opt", "", "opt desc")
+				})
+			})
+			app.Command("command4", "command4 description", func(cmd *Cmd) {
+				cmd.Hidden = true
+			})
 
-	app.Ints(IntsArg{Name: "INTS1", Value: nil, EnvVar: "INTS1", Desc: "Ints Argument 1"})
-	app.Ints(IntsArg{Name: "INTS2", Value: []int{1, 2, 3}, EnvVar: "INTS2", Desc: "Ints Argument 2"})
-	app.Ints(IntsArg{Name: "INTS3", Value: []int{1}, EnvVar: "INTS3", Desc: "Ints Argument 3", HideValue: true})
+			fmt.Printf("calling app with %+v\n", cas.params)
+			require.NoError(t,
+				app.Run(cas.params))
 
-	app.Action = func() {}
+			filename := fmt.Sprintf("testdata/help-output-%s.txt", cas.name)
 
-	app.Command("command1", "command1 description", func(cmd *Cmd) {})
-	app.Command("command2", "command2 description", func(cmd *Cmd) {})
-	app.Command("command3", "command3 description", func(cmd *Cmd) {})
+			fmt.Printf("golden file: %s\n", filename)
+			if *genGolden {
+				require.NoError(t,
+					ioutil.WriteFile(filename, []byte(stdErr), 0644))
+			}
 
-	require.NoError(t,
-		app.Run([]string{"app", "-h"}))
+			expected, e := ioutil.ReadFile(filename)
+			require.NoError(t, e, "Failed to read the expected help output from %s", filename)
 
-	if *genGolden {
-		require.NoError(t,
-			ioutil.WriteFile("testdata/help-output.txt.golden", []byte(err), 0644))
+			require.Equal(t, string(expected), stdErr)
+		})
 	}
-
-	expected, e := ioutil.ReadFile("testdata/help-output.txt")
-	require.NoError(t, e, "Failed to read the expected help output from testdata/help-output.txt")
-
-	require.Equal(t, expected, []byte(err))
 }
 
 func TestLongHelpMessage(t *testing.T) {
diff --git a/testdata/help-output-command1.txt b/testdata/help-output-command1.txt
new file mode 100644
index 0000000..df6279d
--- /dev/null
+++ b/testdata/help-output-command1.txt
@@ -0,0 +1,4 @@
+
+Usage: app command1
+
+command1 description
diff --git a/testdata/help-output-command2.txt b/testdata/help-output-command2.txt
new file mode 100644
index 0000000..add3d75
--- /dev/null
+++ b/testdata/help-output-command2.txt
@@ -0,0 +1,4 @@
+
+Usage: app command2
+
+command2 description
diff --git a/testdata/help-output-command3-child1.txt b/testdata/help-output-command3-child1.txt
new file mode 100644
index 0000000..543e181
--- /dev/null
+++ b/testdata/help-output-command3-child1.txt
@@ -0,0 +1,7 @@
+
+Usage: app command3 child1 ARG1
+
+child1 description
+               
+Arguments:     
+  ARG1         arg1 desc
diff --git a/testdata/help-output-command3-child2.txt b/testdata/help-output-command3-child2.txt
new file mode 100644
index 0000000..9707a7c
--- /dev/null
+++ b/testdata/help-output-command3-child2.txt
@@ -0,0 +1,7 @@
+
+Usage: app command3 child2 [OPTIONS]
+
+child2 description
+               
+Options:       
+  -o, --opt    opt desc
diff --git a/testdata/help-output-command3.txt b/testdata/help-output-command3.txt
new file mode 100644
index 0000000..ff2ed90
--- /dev/null
+++ b/testdata/help-output-command3.txt
@@ -0,0 +1,9 @@
+
+Usage: app command3 COMMAND [arg...]
+
+command3 description
+               
+Commands:      
+  child1       child1 description
+               
+Run 'app command3 COMMAND --help' for more information on a command.
diff --git a/testdata/help-output-command4.txt b/testdata/help-output-command4.txt
new file mode 100644
index 0000000..db9c539
--- /dev/null
+++ b/testdata/help-output-command4.txt
@@ -0,0 +1,4 @@
+
+Usage: app command4
+
+command4 description
diff --git a/testdata/help-output.txt b/testdata/help-output-top-help.txt
similarity index 96%
rename from testdata/help-output.txt
rename to testdata/help-output-top-help.txt
index 903748f..dd4772f 100644
--- a/testdata/help-output.txt
+++ b/testdata/help-output-top-help.txt
@@ -1,5 +1,5 @@
 
-Usage: app [-bdsuikqs] BOOL1 [STR1] INT3... COMMAND [arg...]
+Usage: app [-bdsuikqs] [BOOL1 STR1 INT3...] COMMAND [arg...]
 
 App Desc