Repository: spf13/cobra
Pull request #922 (2019-07-30 06:43:53)

Emit non-zero exit code upon non-runnable subcommand
This is a refresh of PR #642 and fixes issue #582.

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/spf13/cobra?pullRequest=922) <br/>Thank you for your submission, we really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/spf13/cobra?pullRequest=922) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/spf13/cobra?pullRequest=922) it.</sub>
bruceadowns: I tried to sign the cla, but cla-assistant.io fails.

Patch:
diff --git a/.gitignore b/.gitignore
index 3b053c5..b2b848e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,7 +32,8 @@ Session.vim
 tags
 
 *.exe
-
+cobra
 cobra.test
 
-.idea/*
+.idea/
+*.iml
diff --git a/cobra.go b/cobra.go
index 6505c07..d01becc 100644
--- a/cobra.go
+++ b/cobra.go
@@ -52,7 +52,7 @@ var EnableCommandSorting = true
 // if the CLI is started from explorer.exe.
 // To disable the mousetrap, just set this variable to blank string ("").
 // Works only on Microsoft Windows.
-var MousetrapHelpText string = `This is a command line tool.
+var MousetrapHelpText = `This is a command line tool.
 
 You need to open cmd.exe and run it from there.
 `
@@ -61,7 +61,7 @@ You need to open cmd.exe and run it from there.
 // if the CLI is started from explorer.exe. Set to 0 to wait for the return key to be pressed.
 // To disable the mousetrap, just set MousetrapHelpText to blank string ("").
 // Works only on Microsoft Windows.
-var MousetrapDisplayDuration time.Duration = 5 * time.Second
+var MousetrapDisplayDuration = 5 * time.Second
 
 // AddTemplateFunc adds a template function that's available to Usage and Help
 // template generation.
diff --git a/cobra/cmd/init.go b/cobra/cmd/init.go
index 63397d1..dcf5ada 100644
--- a/cobra/cmd/init.go
+++ b/cobra/cmd/init.go
@@ -15,10 +15,11 @@ package cmd
 
 import (
 	"fmt"
-	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
 	"os"
 	"path"
+
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
 )
 
 var (
diff --git a/cobra/cmd/project.go b/cobra/cmd/project.go
index dd2f7ea..a53893c 100644
--- a/cobra/cmd/project.go
+++ b/cobra/cmd/project.go
@@ -2,9 +2,10 @@ package cmd
 
 import (
 	"fmt"
-	"github.com/spf13/cobra/cobra/tpl"
 	"os"
 	"text/template"
+
+	"github.com/spf13/cobra/cobra/tpl"
 )
 
 // Project contains name, license and paths to projects.
@@ -25,7 +26,6 @@ type Command struct {
 }
 
 func (p *Project) Create() error {
-
 	// check if AbsolutePath exists
 	if _, err := os.Stat(p.AbsolutePath); os.IsNotExist(err) {
 		// create directory
diff --git a/cobra/cmd/root.go b/cobra/cmd/root.go
index 624c717..97f404b 100644
--- a/cobra/cmd/root.go
+++ b/cobra/cmd/root.go
@@ -36,8 +36,8 @@ to quickly create a Cobra application.`,
 )
 
 // Execute executes the root command.
-func Execute() {
-	rootCmd.Execute()
+func Execute() error {
+	return rootCmd.Execute()
 }
 
 func init() {
diff --git a/cobra/main.go b/cobra/main.go
index c3a9d9c..eeaf982 100644
--- a/cobra/main.go
+++ b/cobra/main.go
@@ -13,8 +13,14 @@
 
 package main
 
-import "github.com/spf13/cobra/cobra/cmd"
+import (
+	"os"
+
+	"github.com/spf13/cobra/cobra/cmd"
+)
 
 func main() {
-	cmd.Execute()
+	if err := cmd.Execute(); err != nil {
+		os.Exit(1)
+	}
 }
diff --git a/command.go b/command.go
index c7e8983..42e500d 100644
--- a/command.go
+++ b/command.go
@@ -17,6 +17,7 @@ package cobra
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"io"
 	"os"
@@ -27,6 +28,8 @@ import (
 	flag "github.com/spf13/pflag"
 )
 
+var ErrSubCommandRequired = errors.New("subcommand is required")
+
 // FParseErrWhitelist configures Flag parse errors to be ignored
 type FParseErrWhitelist flag.ParseErrorsWhitelist
 
@@ -228,7 +231,7 @@ func (c *Command) SetErr(newErr io.Writer) {
 	c.errWriter = newErr
 }
 
-// SetOut sets the source for input data
+// SetIn sets the source for input data
 // If newIn is nil, os.Stdin is used.
 func (c *Command) SetIn(newIn io.Reader) {
 	c.inReader = newIn
@@ -297,7 +300,7 @@ func (c *Command) ErrOrStderr() io.Writer {
 	return c.getErr(os.Stderr)
 }
 
-// ErrOrStderr returns output to stderr
+// InOrStdin returns output to stderr
 func (c *Command) InOrStdin() io.Reader {
 	return c.getIn(os.Stdin)
 }
@@ -369,7 +372,7 @@ func (c *Command) HelpFunc() func(*Command, []string) {
 	}
 	return func(c *Command, a []string) {
 		c.mergePersistentFlags()
-		err := tmpl(c.OutOrStdout(), c.HelpTemplate(), c)
+		err := tmpl(c.OutOrStderr(), c.HelpTemplate(), c)
 		if err != nil {
 			c.Println(err)
 		}
@@ -786,7 +789,7 @@ func (c *Command) execute(a []string) (err error) {
 	}
 
 	if !c.Runnable() {
-		return flag.ErrHelp
+		return ErrSubCommandRequired
 	}
 
 	c.preRun()
@@ -920,6 +923,14 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
 			return cmd, nil
 		}
 
+		// If command wasn't runnable, show full help, but do return the error.
+		// This will result in apps by default returning a non-success exit code, but also gives them the option to
+		// handle specially.
+		if err == ErrSubCommandRequired {
+			cmd.HelpFunc()(cmd, args)
+			return cmd, err
+		}
+
 		// If root command has SilentErrors flagged,
 		// all subcommands should respect it
 		if !cmd.SilenceErrors && !c.SilenceErrors {


Test patch:
diff --git a/command_test.go b/command_test.go
index 2fa2003..b26bd4a 100644
--- a/command_test.go
+++ b/command_test.go
@@ -836,8 +836,8 @@ func TestHelpExecutedOnNonRunnableChild(t *testing.T) {
 	rootCmd.AddCommand(childCmd)
 
 	output, err := executeCommand(rootCmd, "child")
-	if err != nil {
-		t.Errorf("Unexpected error: %v", err)
+	if err != ErrSubCommandRequired {
+		t.Errorf("Expected error")
 	}
 
 	checkStringContains(t, output, childCmd.Long)