Repository: urfave/cli (go)

Issue:
Don't want any commands in the CLI interface (including help)
Is it possible to use cli for tools without subcommands?

Currently the help always prints out something like this in the usage:
scratch [global options] command [command options] [arguments...]

Is there anyway to get rid of the help command and have it print something more like:
scratch [global options] [arguments...]

It appears if I set "app.HideHelp = true" it gets close to what I want but I would still like to keep the global options of -h, --help.


Pull request description:
Add HideHelpCommand
<!--
  This template provides some ideas of things to include in your PR description.
  To start, try providing a short summary of your changes in the Title above.
  If a section of the PR template does not apply to this PR, then delete that section.
 -->

## What type of PR is this?

- [X] feature

## What this PR does / why we need it:


<!--
  What goal is this change working towards?
  Provide a bullet pointed summary of how each file was changed.
  Briefly explain any decisions you made with respect to the changes.
  Include anything here that you didn't include in *Release Notes*
  above, such as changes to CI or changes to internal methods.
-->

While `HideHelp` hides both `help` command and `--help` flag, `HideHelpCommand`
only hides `help` command and leave `--help` flag as-is.

The behavior of `HideHelp` is untouched in this commit.

## Which issue(s) this PR fixes:

<!--
  If this PR fixes one of more issues, list them here.
  One line each, like so:
    Fixes #123
    Fixes #39
-->
Fixes #523
Replaces #636

## Testing

`go test -v -run TestHideHelpCommand`


## Release Notes

<!--
  If this PR makes user facing changes, please describe them here. This
  description will be copied into the release notes/changelog, whenever the
  next version is released. Keep this section short, and focus on high level
  changes.

  Put your text between the block. To omit notes, use NONE within the block.
-->

```release-note
Added `HideHelpCommand`. While `HideHelp` hides both `help` command and `--help` flag, `HideHelpCommand` only hides `help` command and leave `--help` flag as-is.
```

Patch:
diff --git a/app.go b/app.go
index dd8f1de..d0c8f84 100644
--- a/app.go
+++ b/app.go
@@ -43,8 +43,11 @@ type App struct {
 	Flags []Flag
 	// Boolean to enable bash completion commands
 	EnableBashCompletion bool
-	// Boolean to hide built-in help command
+	// Boolean to hide built-in help command and help flag
 	HideHelp bool
+	// Boolean to hide built-in help command but keep help flag.
+	// Ignored if HideHelp is true.
+	HideHelpCommand bool
 	// Boolean to hide built-in version flag and the VERSION section of help
 	HideVersion bool
 	// categories contains the categorized commands and is populated on app startup
@@ -170,7 +173,9 @@ func (a *App) Setup() {
 	a.Commands = newCommands
 
 	if a.Command(helpCommand.Name) == nil && !a.HideHelp {
-		a.appendCommand(helpCommand)
+		if !a.HideHelpCommand {
+			a.appendCommand(helpCommand)
+		}
 
 		if HelpFlag != nil {
 			a.appendFlag(HelpFlag)
@@ -328,19 +333,9 @@ func (a *App) RunAndExitOnError() {
 // RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() to
 // generate command-specific flags
 func (a *App) RunAsSubcommand(ctx *Context) (err error) {
+	// Setup also handles HideHelp and HideHelpCommand
 	a.Setup()
 
-	// append help to commands
-	if len(a.Commands) > 0 {
-		if a.Command(helpCommand.Name) == nil && !a.HideHelp {
-			a.appendCommand(helpCommand)
-
-			if HelpFlag != nil {
-				a.appendFlag(HelpFlag)
-			}
-		}
-	}
-
 	var newCmds []*Command
 	for _, c := range a.Commands {
 		if c.HelpName == "" {
diff --git a/command.go b/command.go
index db6c802..95840f3 100644
--- a/command.go
+++ b/command.go
@@ -41,8 +41,11 @@ type Command struct {
 	Flags []Flag
 	// Treat all flags as normal arguments if true
 	SkipFlagParsing bool
-	// Boolean to hide built-in help command
+	// Boolean to hide built-in help command and help flag
 	HideHelp bool
+	// Boolean to hide built-in help command but keep help flag
+	// Ignored if HideHelp is true.
+	HideHelpCommand bool
 	// Boolean to hide this command from help or completion
 	Hidden bool
 	// Boolean to enable short-option handling so user can combine several
@@ -236,6 +239,7 @@ func (c *Command) startApp(ctx *Context) error {
 	app.Commands = c.Subcommands
 	app.Flags = c.Flags
 	app.HideHelp = c.HideHelp
+	app.HideHelpCommand = c.HideHelpCommand
 
 	app.Version = ctx.App.Version
 	app.HideVersion = ctx.App.HideVersion
diff --git a/flag.go b/flag.go
index 712eca9..6257f7d 100644
--- a/flag.go
+++ b/flag.go
@@ -36,7 +36,7 @@ var VersionFlag Flag = &BoolFlag{
 
 // HelpFlag prints the help for all commands and subcommands.
 // Set to nil to disable the flag.  The subcommand
-// will still be added unless HideHelp is set to true.
+// will still be added unless HideHelp or HideHelpCommand is set to true.
 var HelpFlag Flag = &BoolFlag{
 	Name:    "help",
 	Aliases: []string{"h"},
@@ -244,10 +244,6 @@ func flagValue(f Flag) reflect.Value {
 	return fv
 }
 
-func formatDefault(format string) string {
-	return " (default: " + format + ")"
-}
-
 func stringifyFlag(f Flag) string {
 	fv := flagValue(f)
 
@@ -273,20 +269,20 @@ func stringifyFlag(f Flag) string {
 	val := fv.FieldByName("Value")
 	if val.IsValid() {
 		needsPlaceholder = val.Kind() != reflect.Bool
-		defaultValueString = fmt.Sprintf(formatDefault("%v"), val.Interface())
+		defaultValueString = fmt.Sprintf(" (default: %v)", val.Interface())
 
 		if val.Kind() == reflect.String && val.String() != "" {
-			defaultValueString = fmt.Sprintf(formatDefault("%q"), val.String())
+			defaultValueString = fmt.Sprintf(" (default: %q)", val.String())
 		}
 	}
 
 	helpText := fv.FieldByName("DefaultText")
 	if helpText.IsValid() && helpText.String() != "" {
 		needsPlaceholder = val.Kind() != reflect.Bool
-		defaultValueString = fmt.Sprintf(formatDefault("%s"), helpText.String())
+		defaultValueString = fmt.Sprintf(" (default: %s)", helpText.String())
 	}
 
-	if defaultValueString == formatDefault("") {
+	if defaultValueString == " (default: )" {
 		defaultValueString = ""
 	}
 
@@ -355,7 +351,7 @@ func stringifySliceFlag(usage string, names, defaultVals []string) string {
 
 	defaultVal := ""
 	if len(defaultVals) > 0 {
-		defaultVal = fmt.Sprintf(formatDefault("%s"), strings.Join(defaultVals, ", "))
+		defaultVal = fmt.Sprintf(" (default: %s)", strings.Join(defaultVals, ", "))
 	}
 
 	usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal))


Test patch:
diff --git a/help_test.go b/help_test.go
index 9f182e8..5f292b7 100644
--- a/help_test.go
+++ b/help_test.go
@@ -5,6 +5,7 @@ import (
 	"flag"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"runtime"
 	"strings"
 	"testing"
@@ -762,3 +763,147 @@ VERSION:
 		t.Errorf("expected output to include \"VERSION:, 2.0.0\"; got: %q", output.String())
 	}
 }
+
+func TestHideHelpCommand(t *testing.T) {
+	app := &App{
+		HideHelpCommand: true,
+		Writer:          ioutil.Discard,
+	}
+
+	err := app.Run([]string{"foo", "help"})
+	if err == nil {
+		t.Fatalf("expected a non-nil error")
+	}
+	if !strings.Contains(err.Error(), "No help topic for 'help'") {
+		t.Errorf("Run returned unexpected error: %v", err)
+	}
+
+	err = app.Run([]string{"foo", "--help"})
+	if err != nil {
+		t.Errorf("Run returned unexpected error: %v", err)
+	}
+}
+
+func TestHideHelpCommand_False(t *testing.T) {
+	app := &App{
+		HideHelpCommand: false,
+		Writer:          ioutil.Discard,
+	}
+
+	err := app.Run([]string{"foo", "help"})
+	if err != nil {
+		t.Errorf("Run returned unexpected error: %v", err)
+	}
+
+	err = app.Run([]string{"foo", "--help"})
+	if err != nil {
+		t.Errorf("Run returned unexpected error: %v", err)
+	}
+}
+
+func TestHideHelpCommand_WithHideHelp(t *testing.T) {
+	app := &App{
+		HideHelp:        true, // effective (hides both command and flag)
+		HideHelpCommand: true, // ignored
+		Writer:          ioutil.Discard,
+	}
+
+	err := app.Run([]string{"foo", "help"})
+	if err == nil {
+		t.Fatalf("expected a non-nil error")
+	}
+	if !strings.Contains(err.Error(), "No help topic for 'help'") {
+		t.Errorf("Run returned unexpected error: %v", err)
+	}
+
+	err = app.Run([]string{"foo", "--help"})
+	if err == nil {
+		t.Fatalf("expected a non-nil error")
+	}
+	if !strings.Contains(err.Error(), "flag: help requested") {
+		t.Errorf("Run returned unexpected error: %v", err)
+	}
+}
+
+func newContextFromStringSlice(ss []string) *Context {
+	set := flag.NewFlagSet("", flag.ContinueOnError)
+	_ = set.Parse(ss)
+	return &Context{flagSet: set}
+}
+
+func TestHideHelpCommand_RunAsSubcommand(t *testing.T) {
+	app := &App{
+		HideHelpCommand: true,
+		Writer:          ioutil.Discard,
+		Commands: []*Command{
+			{
+				Name: "dummy",
+			},
+		},
+	}
+
+	err := app.RunAsSubcommand(newContextFromStringSlice([]string{"", "help"}))
+	if err == nil {
+		t.Fatalf("expected a non-nil error")
+	}
+	if !strings.Contains(err.Error(), "No help topic for 'help'") {
+		t.Errorf("Run returned unexpected error: %v", err)
+	}
+
+	err = app.RunAsSubcommand(newContextFromStringSlice([]string{"", "--help"}))
+	if err != nil {
+		t.Errorf("Run returned unexpected error: %v", err)
+	}
+}
+
+func TestHideHelpCommand_RunAsSubcommand_False(t *testing.T) {
+	app := &App{
+		HideHelpCommand: false,
+		Writer:          ioutil.Discard,
+		Commands: []*Command{
+			{
+				Name: "dummy",
+			},
+		},
+	}
+
+	err := app.RunAsSubcommand(newContextFromStringSlice([]string{"", "help"}))
+	if err != nil {
+		t.Errorf("Run returned unexpected error: %v", err)
+	}
+
+	err = app.RunAsSubcommand(newContextFromStringSlice([]string{"", "--help"}))
+	if err != nil {
+		t.Errorf("Run returned unexpected error: %v", err)
+	}
+}
+
+func TestHideHelpCommand_WithSubcommands(t *testing.T) {
+	app := &App{
+		Writer: ioutil.Discard,
+		Commands: []*Command{
+			{
+				Name: "dummy",
+				Subcommands: []*Command{
+					{
+						Name: "dummy2",
+					},
+				},
+				HideHelpCommand: true,
+			},
+		},
+	}
+
+	err := app.Run([]string{"foo", "dummy", "help"})
+	if err == nil {
+		t.Fatalf("expected a non-nil error")
+	}
+	if !strings.Contains(err.Error(), "No help topic for 'help'") {
+		t.Errorf("Run returned unexpected error: %v", err)
+	}
+
+	err = app.Run([]string{"foo", "dummy", "--help"})
+	if err != nil {
+		t.Errorf("Run returned unexpected error: %v", err)
+	}
+}