Repository: peak/s5cmd (go)

Issue:
s5cmd ls --recursive ... prints error text to stdout
I am converting a workflow from s3cmd to s5cmd, and didn't notice that s5cmd doesn't support recursive.  I had redirected the `ls` to a file(for later processing with shell tools), and the script was failing(via set -e).  I would have expected an error to be printed to stderr, like any other tool, so I was initially confused.

If s5cmd detects an error with cmdline arguments, it should print the help text to stderr, not stdout.

Pull request description:
write usage errors to stderr
Fixes #399. 

```
❯ ./s5cmd --recursive ls
Incorrect Usage: flag provided but not defined: -recursive
See 's5cmd --help' for usage
```

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4232db1..973f35d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 #### Breaking changes
 - Dropped inline comment feature for `run` command. Previously s5cmd supported a command with an inline comment like `ls s3://bucket/object.gz  # inline comment`. ([#309](https://github.com/peak/s5cmd/issues/309))
 - Changed homebrew installation command on macOS. Users can install s5cmd via `brew install peak/tap/s5cmd`. ([#356](https://github.com/peak/s5cmd/issues/356))
+- Print usage errors to stderr instead of stdout and do not show help text on usage error. ([#399](https://github.com/peak/s5cmd/issues/399))
 
 #### Features
 - Added `sync` command to synchronize two given buckets, prefixes, or objects. ([#3](https://github.com/peak/s5cmd/issues/3))
diff --git a/command/app.go b/command/app.go
index 74ea2a1..f10a759 100644
--- a/command/app.go
+++ b/command/app.go
@@ -3,6 +3,7 @@ package command
 import (
 	"context"
 	"fmt"
+	"os"
 
 	cmpinstall "github.com/posener/complete/cmd/install"
 	"github.com/urfave/cli/v2"
@@ -111,6 +112,15 @@ var app = &cli.App{
 		parallel.Close()
 		log.Close()
 	},
+	OnUsageError: func(c *cli.Context, err error, isSubcommand bool) error {
+		if err != nil {
+			_, _ = fmt.Fprintf(os.Stderr, "%s %s\n", "Incorrect Usage:", err.Error())
+			_, _ = fmt.Fprintf(os.Stderr, "See 's5cmd --help' for usage\n")
+			return err
+		}
+
+		return nil
+	},
 	Action: func(c *cli.Context) error {
 		if c.Bool("install-completion") {
 			if cmpinstall.IsInstalled(appName) {


Test patch:
diff --git a/e2e/app_test.go b/e2e/app_test.go
index c68d7ce..2b45a0c 100644
--- a/e2e/app_test.go
+++ b/e2e/app_test.go
@@ -101,3 +101,21 @@ func TestAppUnknownCommand(t *testing.T) {
 		0: equals(`ERROR "unknown-command": command not found`),
 	})
 }
+
+func TestUsageError(t *testing.T) {
+	t.Parallel()
+
+	_, s5cmd, cleanup := setup(t)
+	defer cleanup()
+
+	cmd := s5cmd("--recursive", "ls")
+	result := icmd.RunCmd(cmd)
+
+	result.Assert(t, icmd.Expected{ExitCode: 1})
+
+	assertLines(t, result.Stdout(), map[int]compareFunc{})
+	assertLines(t, result.Stderr(), map[int]compareFunc{
+		0: equals("Incorrect Usage: flag provided but not defined: -recursive"),
+		1: equals("See 's5cmd --help' for usage"),
+	})
+}