Repository: buchgr/bazel-remote
Pull request #418 (2021-04-05 16:03:12)

[doc] use a custom format for the help text, wrapped to the terminal
Some of our command line flags have long descriptions, which are difficult to read unless they're wrapped. This change attempts to wrap at the terminal width, which can be overridden by setting the COLUMNS environment variable.

The format has also been tweaked a little: there is now an empty line between arguments, the mostly-redundant DESCRIPTION section has been removed, and the GLOBAL OPTIONS section was renamed to simply OPTIONS.

The help text in README.md was generated with this command:
COLUMNS=80 ./bazel-remote -h


Patch:
diff --git a/BUILD.bazel b/BUILD.bazel
index 298e8b3..0fd6bd7 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -21,6 +21,7 @@ go_library(
         "//cache/s3proxy:go_default_library",
         "//config:go_default_library",
         "//server:go_default_library",
+        "//utils/flags:go_default_library",
         "//utils/idle:go_default_library",
         "//utils/rlimit:go_default_library",
         "@com_github_abbot_go_http_auth//:go_default_library",
diff --git a/README.md b/README.md
index bc50325..1be63c0 100644
--- a/README.md
+++ b/README.md
@@ -126,54 +126,144 @@ override the corresponding environment variables).
 
 ```
 $ ./bazel-remote --help
-NAME:
-   bazel-remote - A remote build cache for Bazel
+bazel-remote - A remote build cache for Bazel and other REAPI clients
 
 USAGE:
-   bazel-remote [global options] [arguments...]
-
-DESCRIPTION:
-   A remote build cache for Bazel.
-
-GLOBAL OPTIONS:
-   --config_file value                      Path to a YAML configuration file. If this flag is specified then all other flags are ignored. [$BAZEL_REMOTE_CONFIG_FILE]
-   --dir value                              Directory path where to store the cache contents. This flag is required. [$BAZEL_REMOTE_DIR]
-   --max_size value                         The maximum size of the remote cache in GiB. This flag is required. (default: -1) [$BAZEL_REMOTE_MAX_SIZE]
-   --storage_mode value                     Which format to store CAS blobs in. Must be one of "zstd" or "uncompressed". (default: "zstd") [$BAZEL_REMOTE_STORAGE_MODE]
-   --host value                             Address to listen on. Listens on all network interfaces by default. [$BAZEL_REMOTE_HOST]
-   --port value                             The port the HTTP server listens on. (default: 8080) [$BAZEL_REMOTE_PORT]
-   --grpc_port value                        The port the gRPC server listens on. Set to 0 to disable. (default: 9092) [$BAZEL_REMOTE_GRPC_PORT]
-   --profile_host value                     A host address to listen on for profiling, if enabled by a valid --profile_port setting. (default: "127.0.0.1") [$BAZEL_REMOTE_PROFILE_HOST]
-   --profile_port value                     If a positive integer, serve /debug/pprof/* URLs from http://profile_host:profile_port. (default: 0, ie profiling disabled) [$BAZEL_REMOTE_PROFILE_PORT]
-   --http_read_timeout value                The HTTP read timeout for a client request in seconds (does not apply to the proxy backends or the profiling endpoint) (default: 0s, ie disabled) [$BAZEL_REMOTE_HTTP_READ_TIMEOUT]
-   --http_write_timeout value               The HTTP write timeout for a server response in seconds (does not apply to the proxy backends or the profiling endpoint) (default: 0s, ie disabled) [$BAZEL_REMOTE_HTTP_WRITE_TIMEOUT]
-   --htpasswd_file value                    Path to a .htpasswd file. This flag is optional. Please read https://httpd.apache.org/docs/2.4/programs/htpasswd.html. [$BAZEL_REMOTE_HTPASSWD_FILE]
-   --tls_enabled                            This flag has been deprecated. Specify tls_cert_file and tls_key_file instead. (default: false) [$BAZEL_REMOTE_TLS_ENABLED]
-   --tls_ca_file value                      Optional. Enables mTLS (authenticating client certificates), should be the certificate authority that signed the client certificates. [$BAZEL_REMOTE_TLS_CA_FILE]
-   --tls_cert_file value                    Path to a pem encoded certificate file. [$BAZEL_REMOTE_TLS_CERT_FILE]
-   --tls_key_file value                     Path to a pem encoded key file. [$BAZEL_REMOTE_TLS_KEY_FILE]
-   --idle_timeout value                     The maximum period of having received no request after which the server will shut itself down. (default: 0s, ie disabled) [$BAZEL_REMOTE_IDLE_TIMEOUT]
-   --max_queued_uploads value               When using proxy backends, sets the maximum number of objects in queue for upload. If the queue is full, uploads will be skipped until the queue has space again. (default: 1000000) [$BAZEL_REMOTE_MAX_QUEUED_UPLOADS]
-   --num_uploaders value                    When using proxy backends, sets the number of Goroutines to process parallel uploads to backend. (default: 100) [$BAZEL_REMOTE_NUM_UPLOADERS]
-   --http_proxy.url value                   The base URL to use for a http proxy backend. [$BAZEL_REMOTE_HTTP_PROXY_URL]
-   --gcs_proxy.bucket value                 The bucket to use for the Google Cloud Storage proxy backend. [$BAZEL_REMOTE_GCS_BUCKET]
-   --gcs_proxy.use_default_credentials      Whether or not to use authentication for the Google Cloud Storage proxy backend. (default: false) [$BAZEL_REMOTE_GCS_USE_DEFAULT_CREDENTIALS]
-   --gcs_proxy.json_credentials_file value  Path to a JSON file that contains Google credentials for the Google Cloud Storage proxy backend. [$BAZEL_REMOTE_GCS_JSON_CREDENTIALS_FILE]
-   --s3.endpoint value                      The S3/minio endpoint to use when using S3 proxy backend. [$BAZEL_REMOTE_S3_ENDPOINT]
-   --s3.bucket value                        The S3/minio bucket to use when using S3 proxy backend. [$BAZEL_REMOTE_S3_BUCKET]
-   --s3.prefix value                        The S3/minio object prefix to use when using S3 proxy backend. [$BAZEL_REMOTE_S3_PREFIX]
-   --s3.access_key_id value                 The S3/minio access key to use when using S3 proxy backend. [$BAZEL_REMOTE_S3_ACCESS_KEY_ID]
-   --s3.secret_access_key value             The S3/minio secret access key to use when using S3 proxy backend. [$BAZEL_REMOTE_S3_SECRET_ACCESS_KEY]
-   --s3.disable_ssl                         Whether to disable TLS/SSL when using the S3 proxy backend. (default: false, ie enable TLS/SSL) [$BAZEL_REMOTE_S3_DISABLE_SSL]
-   --s3.iam_role_endpoint value             Endpoint for using IAM security credentials. By default it will look for credentials in the standard locations for the AWS platform. [$BAZEL_REMOTE_S3_IAM_ROLE_ENDPOINT]
-   --s3.region value                        The AWS region. Required when not specifying S3/minio access keys. [$BAZEL_REMOTE_S3_REGION]
-   --s3.key_version value                   Set to 1 for the legacy flat key format, or 2 for the newer format that reduces the impact of S3 rate limits. (default: 1) [$BAZEL_REMOTE_S3_KEY_VERSION]
-   --disable_http_ac_validation             Whether to disable ActionResult validation for HTTP requests. (default: false, ie enable validation) [$BAZEL_REMOTE_DISABLE_HTTP_AC_VALIDATION]
-   --disable_grpc_ac_deps_check             Whether to disable ActionResult dependency checks for gRPC GetActionResult requests. (default: false, ie enable ActionCache dependency checks) [$BAZEL_REMOTE_DISABLE_GRPS_AC_DEPS_CHECK]
-   --enable_ac_key_instance_mangling        Whether to enable mangling ActionCache keys with non-empty instance names. (default: false, ie disable mangling) [$BAZEL_REMOTE_ENABLE_AC_KEY_INSTANCE_MANGLING]
-   --enable_endpoint_metrics                Whether to enable metrics for each HTTP/gRPC endpoint. (default: false, ie disable metrics) [$BAZEL_REMOTE_ENABLE_ENDPOINT_METRICS]
-   --experimental_remote_asset_api          Whether to enable the experimental remote asset API implementation. (default: false, ie disable remote asset API) [$BAZEL_REMOTE_EXPERIMENTAL_REMOTE_ASSET_API]
-   --help, -h                               show help (default: false)
+   bazel-remote [options]
+
+OPTIONS:
+   --config_file value Path to a YAML configuration file. If this flag is
+      specified then all other flags are ignored. [$BAZEL_REMOTE_CONFIG_FILE]
+
+   --dir value Directory path where to store the cache contents. This flag is
+      required. [$BAZEL_REMOTE_DIR]
+
+   --max_size value The maximum size of the remote cache in GiB. This flag is
+      required. (default: -1) [$BAZEL_REMOTE_MAX_SIZE]
+
+   --storage_mode value Which format to store CAS blobs in. Must be one of
+      "zstd" or "uncompressed". (default: "zstd") [$BAZEL_REMOTE_STORAGE_MODE]
+
+   --host value Address to listen on. Listens on all network interfaces by
+      default. [$BAZEL_REMOTE_HOST]
+
+   --port value The port the HTTP server listens on. (default: 8080)
+      [$BAZEL_REMOTE_PORT]
+
+   --grpc_port value The port the gRPC server listens on. Set to 0 to
+      disable. (default: 9092) [$BAZEL_REMOTE_GRPC_PORT]
+
+   --profile_host value A host address to listen on for profiling, if enabled
+      by a valid --profile_port setting. (default: "127.0.0.1")
+      [$BAZEL_REMOTE_PROFILE_HOST]
+
+   --profile_port value If a positive integer, serve /debug/pprof/* URLs from
+      http://profile_host:profile_port. (default: 0, ie profiling disabled)
+      [$BAZEL_REMOTE_PROFILE_PORT]
+
+   --http_read_timeout value The HTTP read timeout for a client request in
+      seconds (does not apply to the proxy backends or the profiling endpoint)
+      (default: 0s, ie disabled) [$BAZEL_REMOTE_HTTP_READ_TIMEOUT]
+
+   --http_write_timeout value The HTTP write timeout for a server response in
+      seconds (does not apply to the proxy backends or the profiling endpoint)
+      (default: 0s, ie disabled) [$BAZEL_REMOTE_HTTP_WRITE_TIMEOUT]
+
+   --htpasswd_file value Path to a .htpasswd file. This flag is optional.
+      Please read https://httpd.apache.org/docs/2.4/programs/htpasswd.html.
+      [$BAZEL_REMOTE_HTPASSWD_FILE]
+
+   --tls_enabled This flag has been deprecated. Specify tls_cert_file and
+      tls_key_file instead. (default: false) [$BAZEL_REMOTE_TLS_ENABLED]
+
+   --tls_ca_file value Optional. Enables mTLS (authenticating client
+      certificates), should be the certificate authority that signed the client
+      certificates. [$BAZEL_REMOTE_TLS_CA_FILE]
+
+   --tls_cert_file value Path to a pem encoded certificate file.
+      [$BAZEL_REMOTE_TLS_CERT_FILE]
+
+   --tls_key_file value Path to a pem encoded key file.
+      [$BAZEL_REMOTE_TLS_KEY_FILE]
+
+   --idle_timeout value The maximum period of having received no request
+      after which the server will shut itself down. (default: 0s, ie disabled)
+      [$BAZEL_REMOTE_IDLE_TIMEOUT]
+
+   --max_queued_uploads value When using proxy backends, sets the maximum
+      number of objects in queue for upload. If the queue is full, uploads will
+      be skipped until the queue has space again. (default: 1000000)
+      [$BAZEL_REMOTE_MAX_QUEUED_UPLOADS]
+
+   --num_uploaders value When using proxy backends, sets the number of
+      Goroutines to process parallel uploads to backend. (default: 100)
+      [$BAZEL_REMOTE_NUM_UPLOADERS]
+
+   --http_proxy.url value The base URL to use for a http proxy backend.
+      [$BAZEL_REMOTE_HTTP_PROXY_URL]
+
+   --gcs_proxy.bucket value The bucket to use for the Google Cloud Storage
+      proxy backend. [$BAZEL_REMOTE_GCS_BUCKET]
+
+   --gcs_proxy.use_default_credentials Whether or not to use authentication
+      for the Google Cloud Storage proxy backend. (default: false)
+      [$BAZEL_REMOTE_GCS_USE_DEFAULT_CREDENTIALS]
+
+   --gcs_proxy.json_credentials_file value Path to a JSON file that contains
+      Google credentials for the Google Cloud Storage proxy backend.
+      [$BAZEL_REMOTE_GCS_JSON_CREDENTIALS_FILE]
+
+   --s3.endpoint value The S3/minio endpoint to use when using S3 proxy
+      backend. [$BAZEL_REMOTE_S3_ENDPOINT]
+
+   --s3.bucket value The S3/minio bucket to use when using S3 proxy backend.
+      [$BAZEL_REMOTE_S3_BUCKET]
+
+   --s3.prefix value The S3/minio object prefix to use when using S3 proxy
+      backend. [$BAZEL_REMOTE_S3_PREFIX]
+
+   --s3.access_key_id value The S3/minio access key to use when using S3
+      proxy backend. [$BAZEL_REMOTE_S3_ACCESS_KEY_ID]
+
+   --s3.secret_access_key value The S3/minio secret access key to use when
+      using S3 proxy backend. [$BAZEL_REMOTE_S3_SECRET_ACCESS_KEY]
+
+   --s3.disable_ssl Whether to disable TLS/SSL when using the S3 proxy
+      backend. (default: false, ie enable TLS/SSL)
+      [$BAZEL_REMOTE_S3_DISABLE_SSL]
+
+   --s3.iam_role_endpoint value Endpoint for using IAM security credentials.
+      By default it will look for credentials in the standard locations for the
+      AWS platform. [$BAZEL_REMOTE_S3_IAM_ROLE_ENDPOINT]
+
+   --s3.region value The AWS region. Required when not specifying S3/minio
+      access keys. [$BAZEL_REMOTE_S3_REGION]
+
+   --s3.key_version value Set to 1 for the legacy flat key format, or 2 for
+      the newer format that reduces the impact of S3 rate limits. (default: 1)
+      [$BAZEL_REMOTE_S3_KEY_VERSION]
+
+   --disable_http_ac_validation Whether to disable ActionResult validation
+      for HTTP requests. (default: false, ie enable validation)
+      [$BAZEL_REMOTE_DISABLE_HTTP_AC_VALIDATION]
+
+   --disable_grpc_ac_deps_check Whether to disable ActionResult dependency
+      checks for gRPC GetActionResult requests. (default: false, ie enable
+      ActionCache dependency checks) [$BAZEL_REMOTE_DISABLE_GRPS_AC_DEPS_CHECK]
+
+   --enable_ac_key_instance_mangling Whether to enable mangling ActionCache
+      keys with non-empty instance names. (default: false, ie disable mangling)
+      [$BAZEL_REMOTE_ENABLE_AC_KEY_INSTANCE_MANGLING]
+
+   --enable_endpoint_metrics Whether to enable metrics for each HTTP/gRPC
+      endpoint. (default: false, ie disable metrics)
+      [$BAZEL_REMOTE_ENABLE_ENDPOINT_METRICS]
+
+   --experimental_remote_asset_api Whether to enable the experimental remote
+      asset API implementation. (default: false, ie disable remote asset API)
+      [$BAZEL_REMOTE_EXPERIMENTAL_REMOTE_ASSET_API]
+
+   --help, -h  show help (default: false)
 ```
 
 ### Example configuration file
diff --git a/main.go b/main.go
index 05d7d22..1c4cbb4 100644
--- a/main.go
+++ b/main.go
@@ -24,6 +24,7 @@ import (
 
 	"github.com/buchgr/bazel-remote/config"
 	"github.com/buchgr/bazel-remote/server"
+	"github.com/buchgr/bazel-remote/utils/flags"
 	"github.com/buchgr/bazel-remote/utils/idle"
 	"github.com/buchgr/bazel-remote/utils/rlimit"
 
@@ -58,10 +59,11 @@ func main() {
 		runtime.Version(), maybeGitCommitMsg)
 
 	app := cli.NewApp()
-	app.Description = "A remote build cache for Bazel."
-	app.Usage = "A remote build cache for Bazel"
-	app.HideVersion = true
-	app.HideHelpCommand = true
+
+	cli.AppHelpTemplate = flags.Template
+	cli.HelpPrinterCustom = flags.HelpPrinter
+	// Force the use of cli.HelpPrinterCustom.
+	app.ExtraInfo = func() map[string]string { return map[string]string{} }
 
 	app.Flags = []cli.Flag{
 		&cli.StringFlag{
diff --git a/utils/flags/BUILD.bazel b/utils/flags/BUILD.bazel
new file mode 100644
index 0000000..fff026d
--- /dev/null
+++ b/utils/flags/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["usage.go"],
+    importpath = "github.com/buchgr/bazel-remote/utils/flags",
+    visibility = ["//visibility:public"],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = ["usage_test.go"],
+    embed = [":go_default_library"],
+    deps = ["@com_github_urfave_cli_v2//:go_default_library"],
+)
diff --git a/utils/flags/usage.go b/utils/flags/usage.go
new file mode 100644
index 0000000..f4ed38a
--- /dev/null
+++ b/utils/flags/usage.go
@@ -0,0 +1,158 @@
+package flags
+
+import (
+	"io"
+	"log"
+	"os"
+	"os/exec"
+	"strconv"
+	"strings"
+	"text/tabwriter"
+	"text/template"
+)
+
+const (
+	// An arbitrarily large width, that we never expect to hit
+	// in practice, which satisfies our internal API.
+	defaultWidth = 10000
+
+	// Don't attempt to wrap any more narrow than this.
+	minimumWidth = 30
+)
+
+// Template describes the help text format.
+var Template = `bazel-remote - A remote build cache for Bazel and other REAPI clients
+
+USAGE:
+   {{.Name}} [options]
+
+OPTIONS:
+   {{range $index, $option := .VisibleFlags}}{{if $index}}
+   {{end}}{{wrap $option.String 6}}
+{{end}}`
+
+// HelpPrinter writes our custom-formatted help text to `out`.
+func HelpPrinter(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) {
+	maxLineLength := getConsoleWidth()
+
+	funcMap := template.FuncMap{
+		"wrap": func(input string, offset int) string {
+			return wrap(input, offset, maxLineLength)
+		},
+	}
+
+	w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0)
+	t := template.Must(template.New("help").Funcs(funcMap).Parse(templ))
+
+	err := t.Execute(w, data)
+	if err != nil {
+		log.Fatalf("Failed to apply the template or write the results: %q", err)
+	}
+	err = w.Flush()
+	if err != nil {
+		log.Fatalf("Failed to flush help text: %q", err)
+	}
+}
+
+// Wrap a possibly multiline input string at word boundaries when it
+// reaches `wrapAt`, and prefix wrapped lines with `offset` spaces.
+func wrap(input string, offset int, wrapAt int) string {
+	var sb strings.Builder
+
+	lines := strings.Split(input, "\n")
+
+	prefix := strings.Repeat(" ", offset)
+
+	for i, line := range lines {
+		if i != 0 {
+			sb.WriteString(prefix)
+		}
+
+		sb.WriteString(wrapLine(line, wrapAt, prefix))
+
+		if i != len(lines)-1 {
+			sb.WriteString("\n")
+		}
+	}
+
+	return sb.String()
+}
+
+// Wrap a single line input string at word boundaries, once it reaches
+// `wrapAt`, with `prefix` added to the start of wrapped lines.
+// Note that this does not preserve whitespace exactly.
+func wrapLine(input string, wrapAt int, padding string) string {
+
+	offset := len(padding)
+
+	if wrapAt <= offset {
+		// We can't meaningfully wrap the input, return it unchanged.
+		return input
+	}
+
+	if len(input) <= wrapAt-offset {
+		// Simple case- the input is small enough to return unchanged.
+		return input
+	}
+
+	targetWidth := wrapAt - offset
+	words := strings.Fields(input)
+	if len(words) == 0 {
+		// Input must be empty or all whitespace, return the input unchanged.
+		return input
+	}
+
+	// Place at least one word on the first line.
+	wrapped := words[0]
+	spaceLeft := targetWidth - len(wrapped)
+
+	for _, word := range words[1:] {
+		if len(word)+1 > spaceLeft {
+			// Not enough room for the word, start a new line with it.
+			wrapped += "\n" + padding + word
+			spaceLeft = targetWidth - len(word)
+		} else {
+			// There's room on the current line for this word.
+			wrapped += " " + word
+			spaceLeft -= 1 + len(word)
+		}
+	}
+
+	return wrapped
+}
+
+func getConsoleWidth() int {
+
+	// If the COLUMNS environment variable is set, try to use it.
+	columns := os.Getenv("COLUMNS")
+	if columns != "" {
+		width, err := strconv.Atoi(strings.TrimSpace(string(columns)))
+		if err == nil {
+
+			if width < minimumWidth {
+				return minimumWidth
+			}
+
+			return width
+		}
+	}
+
+	// Otherwise query the terminal for the width.
+	// This seems to work on both linux and mac.
+	cmd := exec.Command("tput", "cols")
+	cmd.Stdin = os.Stdin
+	output, err := cmd.Output()
+	if err != nil {
+		return defaultWidth
+	}
+
+	width, err := strconv.Atoi(strings.TrimSpace(string(output)))
+	if err != nil {
+		return defaultWidth
+	}
+	if width < minimumWidth {
+		return minimumWidth
+	}
+
+	return width
+}


Test patch:
diff --git a/utils/flags/usage_test.go b/utils/flags/usage_test.go
new file mode 100644
index 0000000..955e7c8
--- /dev/null
+++ b/utils/flags/usage_test.go
@@ -0,0 +1,161 @@
+package flags
+
+import (
+	"bytes"
+	"io"
+	"os"
+	"testing"
+
+	"github.com/urfave/cli/v2"
+)
+
+func TestWrapLine(t *testing.T) {
+	t.Parallel()
+
+	tcs := []struct {
+		input    string
+		offset   int
+		wrapAt   int
+		padding  string
+		expected string
+	}{
+		{
+			input:   "the quick brown fox jumped over the lazy dog",
+			offset:  2,
+			wrapAt:  10,
+			padding: "__",
+			expected: `the
+__quick
+__brown
+__fox
+__jumped
+__over the
+__lazy dog`,
+		},
+		{
+			input:    "the quick brown fox jumped over the lazy dog",
+			wrapAt:   50,
+			padding:  "__",
+			expected: "the quick brown fox jumped over the lazy dog",
+		},
+	}
+
+	for _, tc := range tcs {
+		result := wrapLine(tc.input, tc.wrapAt, tc.padding)
+		if result != tc.expected {
+			t.Errorf("Got %q, expected %q", result, tc.expected)
+		}
+	}
+}
+
+func TestWrap(t *testing.T) {
+	t.Parallel()
+
+	tcs := []struct {
+		input    string
+		offset   int
+		wrapAt   int
+		expected string
+	}{
+		{
+			input: `the quick brown fox jumped over the lazy dog
+the second line is even longer than the first, with some super important
+information that overflows
+and finally a fourth line with some gibberish`,
+			offset: 2,
+			wrapAt: 25,
+			expected: `the quick brown fox
+  jumped over the lazy
+  dog
+  the second line is even
+  longer than the first,
+  with some super
+  important
+  information that
+  overflows
+  and finally a fourth
+  line with some
+  gibberish`,
+		},
+	}
+
+	for _, tc := range tcs {
+		result := wrap(tc.input, tc.offset, tc.wrapAt)
+		if result != tc.expected {
+			t.Errorf("Got %q, expected %q", result, tc.expected)
+		}
+	}
+}
+
+func TestHelpPrinter(t *testing.T) {
+
+	// Setting an environment variable in a test is not great, but we
+	// don't have a better way to unit test this at the moment.
+	os.Setenv("COLUMNS", "35")
+	defer os.Unsetenv("COLUMNS")
+
+	flags := []cli.Flag{
+		&cli.StringFlag{
+			Name:    "foo",
+			Value:   "42",
+			Usage:   "you really should specify this value, otherwise some terrible things will happen",
+			EnvVars: []string{"FOO"},
+		},
+		&cli.IntFlag{
+			Name:    "bar",
+			Value:   1,
+			Usage:   "this is another flag with a description long enough to test the wrapping",
+			EnvVars: []string{"BAR"},
+		},
+	}
+
+	expected := `bazel-remote - A remote build cache for Bazel and other REAPI clients
+
+USAGE:
+   cli.test [options]
+
+OPTIONS:
+   --foo value you really should
+      specify this value, otherwise
+      some terrible things will
+      happen (default: "42") [$FOO]
+
+   --bar value this is another
+      flag with a description long
+      enough to test the wrapping
+      (default: 1) [$BAR]
+
+   --help, -h show help
+      (default: false)
+`
+
+	output := new(bytes.Buffer)
+
+	app := &cli.App{
+		Name:   "cli.test",
+		Writer: output,
+		Flags:  flags,
+	}
+
+	// Reset HelpPrinter after this test.
+	defer func(old func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{})) {
+		cli.HelpPrinterCustom = old
+	}(cli.HelpPrinterCustom)
+
+	cli.HelpPrinterCustom = HelpPrinter
+	cli.AppHelpTemplate = Template
+
+	// Force the use of cli.HelpPrinterCustom.
+	app.ExtraInfo = func() map[string]string { return map[string]string{} }
+
+	app.Action = func(c *cli.Context) error { return nil }
+	err := app.Run([]string{"appName", "-h"})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if string(output.Bytes()) != expected {
+		t.Fatalf("Expected:\n%s\n\nGot:\n%s\n", expected,
+			string(output.Bytes()))
+	}
+}