Repository: python-gitlab/python-gitlab

Issue:
CLI: List objects vertically
When running `gitlab --help` to get the list of objects that can be modified the output is unformatted JSON. I'd suggest formatting it as a list like below for easier human reading:

Current output:
```
usage: gitlab [-h] [--version] [-v] [-d] [-c CONFIG_FILE] [-g GITLAB]
              [-o {json,legacy,yaml}] [-f FIELDS]
              {application-settings,broadcast-message,current-user,current-user-email,current-user-gp-gkey,current-user-key,deploy-key,dockerfile,event,feature,gitignore,gitlabciyml,group,group-access-request,group-custom-attribute,group-issue,group-member,group-milestone,group-notification-settings,group-project,group-subgroup,group-variable,hook,issue,license,namespace,notification-settings,pages-domain,project,project-access-request,project-board,project-board-list,project-branch,project-commit,project-commit-comment,project-commit-status,project-custom-attribute,project-deployment,project-environment,project-event,project-file,project-fork,project-hook,project-issue,project-issue-award-emoji,project-issue-note,project-issue-note-award-emoji,project-job,project-key,project-label,project-member,project-merge-request,project-merge-request-award-emoji,project-merge-request-diff,project-merge-request-note,project-merge-request-note-award-emoji,project-milestone,project-note,project-notification-settings,project-pages-domain,project-pipeline,project-pipeline-job,project-pipeline-schedule,project-pipeline-schedule-variable,project-protected-branch,project-runner,project-service,project-snippet,project-snippet-award-emoji,project-snippet-note,project-snippet-note-award-emoji,project-tag,project-trigger,project-user,project-variable,project-wiki,runner,snippet,todo,user,user-activities,user-custom-attribute,user-email,user-event,user-gp-gkey,user-impersonation-token,user-key,user-project}
```

Suggested output:
```
usage: gitlab [-h] [--version] [-v] [-d] [-c CONFIG_FILE] [-g GITLAB]
              [-o {json,legacy,yaml}] [-f FIELDS] <OBJECT> <ACTION>

Where OBJECT is one of:
   application-settings
   broadcast-message
   current-user
   current-user-email
   current-user-gp-gkey
   current-user-key
   deploy-key
   dockerfile
   event
   feature
   gitignore
   gitlabciyml
   group
   group-access-request
   group-custom-attribute
   group-issue
   group-member
   group-milestone
   group-notification-settings
   group-project
   group-subgroup
   group-variable
   hook
   issue
   license
   namespace
   notification-settings
   pages-domain
   project
   project-access-request
   project-board
   project-board-list
   project-branch
   project-commit
   project-commit-comment
   project-commit-status
   project-custom-attribute
   project-deployment
   project-environment
   project-event
   project-file
   project-fork
   project-hook
   project-issue
   project-issue-award-emoji
   project-issue-note
   project-issue-note-award-emoji
   project-job
   project-key
   project-label
   project-member
   project-merge-request
   project-merge-request-award-emoji
   project-merge-request-diff
   project-merge-request-note
   project-merge-request-note-award-emoji
   project-milestone
   project-note
   project-notification-settings
   project-pages-domain
   project-pipeline
   project-pipeline-job
   project-pipeline-schedule
   project-pipeline-schedule-variable
   project-protected-branch
   project-runner
   project-service
   project-snippet
   project-snippet-award-emoji
   project-snippet-note
   project-snippet-note-award-emoji
   project-tag
   project-trigger
   project-user
   project-variable
   project-wiki
   runner
   snippet
   todo
   user
   user-activities
   user-custom-attribute
   user-email
   user-event
   user-gp-gkey
   user-impersonation-token
   user-key
   user-project
```


Discussion:

Resolving patch:
diff --git a/gitlab/cli.py b/gitlab/cli.py
--- a/gitlab/cli.py
+++ b/gitlab/cli.py
@@ -50,6 +50,23 @@
 __F = TypeVar("__F", bound=Callable[..., Any])
 
 
+class VerticalHelpFormatter(argparse.HelpFormatter):
+    def format_help(self) -> str:
+        result = super().format_help()
+        output = ""
+        indent = self._indent_increment * " "
+        for line in result.splitlines(keepends=True):
+            # All of our resources are on one line and wrapped inside braces.
+            # For example: {application,resource1,resource2}
+            # We then put each resource on its own line to make it easier to read.
+            if line.strip().startswith("{"):
+                choices = line.strip().strip("{}").split(",")
+                choices_str = f"\n{indent}".join(choices)
+                line = f"{indent}{choices_str}\n"
+            output += line
+        return output
+
+
 def register_custom_action(
     cls_names: Union[str, Tuple[str, ...]],
     mandatory: Tuple[str, ...] = (),
@@ -110,6 +127,7 @@ def _get_base_parser(add_help: bool = True) -> argparse.ArgumentParser:
     parser = argparse.ArgumentParser(
         add_help=add_help,
         description="GitLab API Command Line Interface",
+        formatter_class=VerticalHelpFormatter,
         allow_abbrev=False,
     )
     parser.add_argument("--version", help="Display the version.", action="store_true")
diff --git a/gitlab/v4/cli.py b/gitlab/v4/cli.py
--- a/gitlab/v4/cli.py
+++ b/gitlab/v4/cli.py
@@ -377,7 +377,9 @@ def extend_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
 
     for cls in sorted(classes, key=operator.attrgetter("__name__")):
         arg_name = cli.cls_to_gitlab_resource(cls)
-        object_group = subparsers.add_parser(arg_name)
+        object_group = subparsers.add_parser(
+            arg_name, formatter_class=cli.VerticalHelpFormatter
+        )
 
         object_subparsers = object_group.add_subparsers(
             title="action",


Test patch:
diff --git a/tests/functional/cli/test_cli.py b/tests/functional/cli/test_cli.py
--- a/tests/functional/cli/test_cli.py
+++ b/tests/functional/cli/test_cli.py
@@ -34,6 +34,18 @@ def test_config_error_with_help_prints_help(script_runner):
     assert ret.returncode == 0
 
 
+def test_global_help_prints_resources_vertically(script_runner):
+    ret = script_runner.run("gitlab", "--help")
+    assert """resource:\n  application\n  application-appearance\n""" in ret.stdout
+    assert ret.returncode == 0
+
+
+def test_resource_help_prints_actions_vertically(script_runner):
+    ret = script_runner.run("gitlab", "project", "--help")
+    assert """action:\n  list\n  get""" in ret.stdout
+    assert ret.returncode == 0
+
+
 @pytest.mark.script_launch_mode("inprocess")
 @responses.activate
 def test_defaults_to_gitlab_com(script_runner, resp_get_project, monkeypatch):