Repository: StackStorm/st2

Issue:
st2 cli commands in "usage" must be in alphabetical order

```
usage: st2 [-h] [--version] [--url BASE_URL] [--auth-url AUTH_URL]
           [--api-url API_URL] [--stream-url STREAM_URL]
           [--api-version API_VERSION] [--cacert CACERT]
           [--config-file CONFIG_FILE] [--print-config] [--skip-config]
           [--debug]
           {action,action-alias,auth,apikey,execution,key,login,pack,policy,policy-type,rule,run,runner,sensor,trace,trigger,trigger-instance,webhook,whoami,timer,rule-enforcement,role,role-assignment}
```

Backstory: someone didn't see `timer` just because they're out of the order. 


Discussion:
And tbh, we should make the whole output more user-friendly.

Perhaps something like:

```bash

Usage: st2 all the st2 flags

 action {list,get,create}
 action-alias {list,get,create}
 ...
```

cliff has a default output format which looks something like that and it's a a lot more user-friendly, imo.

Resolving patch:
diff --git a/st2client/st2client/shell.py b/st2client/st2client/shell.py
--- a/st2client/st2client/shell.py
+++ b/st2client/st2client/shell.py
@@ -63,6 +63,15 @@
 LOGGER = logging.getLogger(__name__)
 
 CLI_DESCRIPTION = 'CLI for StackStorm event-driven automation platform. https://stackstorm.com'
+USAGE_STRING = """
+Usage: %(prog)s [options] <command> <sub command> [options]
+
+For example:
+
+    %(prog)s action list --pack=st2
+    %(prog)s run core.local cmd=date
+    %(prog)s --debug run core.local cmd=date
+""".strip()
 
 
 class Shell(BaseCLIApp):
@@ -177,7 +186,10 @@ def __init__(self):
 
         # Set up list of commands and subcommands.
         self.subparsers = self.parser.add_subparsers()
-        self.commands = dict()
+        self.commands = {}
+
+        self.commands['run'] = action.ActionRunCommand(
+            models.Action, self, self.subparsers, name='run', add_help=False)
 
         self.commands['action'] = action.ActionBranch(
             'An activity that happens as a response to the external event.',
@@ -190,6 +202,12 @@ def __init__(self):
         self.commands['auth'] = auth.TokenCreateCommand(
             models.Token, self, self.subparsers, name='auth')
 
+        self.commands['login'] = auth.LoginCommand(
+            models.Token, self, self.subparsers, name='login')
+
+        self.commands['whoami'] = auth.WhoamiCommand(
+            models.Token, self, self.subparsers, name='whoami')
+
         self.commands['api-key'] = auth.ApiKeyBranch(
             'API Keys.',
             self, self.subparsers)
@@ -203,9 +221,6 @@ def __init__(self):
             'for reuse in sensors, actions, and rules.',
             self, self.subparsers)
 
-        self.commands['login'] = auth.LoginCommand(
-            models.Token, self, self.subparsers, name='login')
-
         self.commands['pack'] = pack.PackBranch(
             'A group of related integration resources: '
             'actions, rules, and sensors.',
@@ -224,8 +239,13 @@ def __init__(self):
             'based on some criteria.',
             self, self.subparsers)
 
-        self.commands['run'] = action.ActionRunCommand(
-            models.Action, self, self.subparsers, name='run', add_help=False)
+        self.commands['webhook'] = webhook.WebhookBranch(
+            'Webhooks.',
+            self, self.subparsers)
+
+        self.commands['timer'] = timer.TimerBranch(
+            'Timers.',
+            self, self.subparsers)
 
         self.commands['runner'] = resource.ResourceBranch(
             models.RunnerType,
@@ -249,17 +269,6 @@ def __init__(self):
             'Actual instances of triggers received by st2.',
             self, self.subparsers)
 
-        self.commands['webhook'] = webhook.WebhookBranch(
-            'Webhooks.',
-            self, self.subparsers)
-
-        self.commands['whoami'] = auth.WhoamiCommand(
-            models.Token, self, self.subparsers, name='whoami')
-
-        self.commands['timer'] = timer.TimerBranch(
-            'Timers.',
-            self, self.subparsers)
-
         self.commands['rule-enforcement'] = rule_enforcement.RuleEnforcementBranch(
             'Models that represent enforcement of rules.',
             self, self.subparsers)
@@ -275,6 +284,17 @@ def __init__(self):
     def run(self, argv):
         debug = False
 
+        parser = self.parser
+
+        if len(argv) == 0:
+            # Print a more user-friendly help string if no arguments are provided
+            # Note: We only set usage variable for the main parser. If we passed "usage" argument
+            # to the main ArgumentParser class above, this would also set a custom usage string for
+            # sub-parsers which we don't want.
+            parser.usage = USAGE_STRING
+            sys.stderr.write(parser.format_help())
+            return 2
+
         # Provide autocomplete for shell
         argcomplete.autocomplete(self.parser)
 


Test patch:
diff --git a/st2client/tests/unit/test_shell.py b/st2client/tests/unit/test_shell.py
--- a/st2client/tests/unit/test_shell.py
+++ b/st2client/tests/unit/test_shell.py
@@ -49,6 +49,49 @@ def __init__(self, *args, **kwargs):
         super(TestShell, self).__init__(*args, **kwargs)
         self.shell = Shell()
 
+    def test_commands_usage_and_help_strings(self):
+        # No command, should print out user friendly usage / help string
+        self.assertEqual(self.shell.run([]), 2)
+
+        self.stderr.seek(0)
+        stderr = self.stderr.read()
+        self.assertTrue('Usage: ' in stderr)
+        self.assertTrue('For example:' in stderr)
+        self.assertTrue('CLI for StackStorm' in stderr)
+        self.assertTrue('positional arguments:' in stderr)
+
+        self.stdout.truncate()
+        self.stderr.truncate()
+
+        # --help should result in the same output
+        try:
+            self.assertEqual(self.shell.run(['--help']), 0)
+        except SystemExit as e:
+            self.assertEqual(e.code, 0)
+
+        self.stdout.seek(0)
+        stdout = self.stdout.read()
+        self.assertTrue('Usage: ' in stdout)
+        self.assertTrue('For example:' in stdout)
+        self.assertTrue('CLI for StackStorm' in stdout)
+        self.assertTrue('positional arguments:' in stdout)
+
+        self.stdout.truncate()
+        self.stderr.truncate()
+
+        # Sub command with no args
+        try:
+            self.assertEqual(self.shell.run(['action']), 2)
+        except SystemExit as e:
+            self.assertEqual(e.code, 2)
+
+        self.stderr.seek(0)
+        stderr = self.stderr.read()
+
+        self.assertTrue('usage' in stderr)
+        self.assertTrue('{list,get,create,update' in stderr)
+        self.assertTrue('error: too few arguments' in stderr)
+
     def test_endpoints_default(self):
         base_url = 'http://127.0.0.1'
         auth_url = 'http://127.0.0.1:9100'