Repository: certbot/certbot

Issue:
Common subcommand flags help
Currently, when you run commands like `letsencrypt --help install`, you get the following output:

```
$ letsencrypt --help install  
usage: 
  letsencrypt [SUBCOMMAND] [options] [domains]

The Let's Encrypt agent can obtain and install HTTPS/TLS/SSL certificates.  By
default, it will attempt to use a webserver both for obtaining and installing
the cert.  

positional arguments:
  SUBCOMMAND
    run                 Obtain a certificate and install.
    auth                Authenticate & obtain cert, but do not install it.
    install             Install a previously obtained cert in a server.
    revoke              Revoke a previously obtained certificate.
    rollback            Rollback server configuration changes made during
                        install.
    config_changes      Show changes made to server config during installation
    plugins             List server software plugins.

optional arguments:
  -h, --help            show this help message and exit
  -c CONFIG_FILE, --config CONFIG_FILE
                        config file path (default: None)

install:
  Options for modifying how a cert is deployed
```

This to me doesn't seem very helpful. The problem is arguments shared between subcommands. Most of these common flags are now in the `paths` argument subgroup. I think ideally, the common arguments should not be duplicated when running `letsencrypt --help all`, but should be shown when running help for a specific subcommand. I do not believe this behavior is achievable with `argparse` and adds a fair amount of complexity, so we shouldn't worry about doing anything like this for now.

Instead, a simple thing we could do is update the description for the relevant subgroups to point users towards the `paths` group. I'm not sure what other simple fixes could be done here, but I don't think the help for commands like `install` and `revoke` does much to help the user right now.



Discussion:
I'd like to note that this wasn't the problem when flags were specified per-subcommand. May I suggest reverting move of all flags into argument groups?

Again, another evidence that hacking `argparse` backfires us. Is it the time to change approach, and try to bring back sane, well-understood (though at some points harsh) CLI design? All the magical features were meant to make the CLI more user-friendly, but they break other things making CLI less user-firendly than in the first instance, IMHO.

This leads to a very bad UX, IMHO. I just stumbled upon:

```
(venv)root@le:~# ./letsencrypt/letsencrypt-auto install
Updating letsencrypt and virtual environment dependencies......
Running with virtualenv: /root/.local/share/letsencrypt/bin/letsencrypt install
usage: 
  letsencrypt [SUBCOMMAND] [options] [-d domain] [-d domain] ...

The Let's Encrypt agent can obtain and install HTTPS/TLS/SSL certificates.  By
default, it will attempt to use a webserver both for obtaining and installing
the cert. Major SUBCOMMANDS are:

  (default) run        Obtain & install a cert in your current webserver
  certonly             Obtain cert, but do not install it (aka "auth")
  install              Install a previously obtained cert in a server
  revoke               Revoke a previously obtained certificate
  rollback             Rollback server configuration changes made during install
  config_changes       Show changes made to server config during installation
letsencrypt: error: argument --cert-path is required
```

@pde, could you please look into that? I think it should be "1.0" issue.


Resolving patch:
diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py
--- a/letsencrypt/cli.py
+++ b/letsencrypt/cli.py
@@ -650,12 +650,12 @@ def __init__(self, args, plugins):
         help1 = self.prescan_for_flag("-h", self.help_topics)
         help2 = self.prescan_for_flag("--help", self.help_topics)
         assert max(True, "a") == "a", "Gravity changed direction"
-        help_arg = max(help1, help2)
-        if help_arg is True:
+        self.help_arg = max(help1, help2)
+        if self.help_arg is True:
             # just --help with no topic; avoid argparse altogether
             print usage
             sys.exit(0)
-        self.visible_topics = self.determine_help_topics(help_arg)
+        self.visible_topics = self.determine_help_topics(self.help_arg)
         #print self.visible_topics
         self.groups = {}  # elements are added by .add_group()
 
@@ -873,12 +873,12 @@ def prepare_and_parse_args(plugins, args):
         help="Require that all configuration files are owned by the current "
              "user; only needed if your config is somewhere unsafe like /tmp/")
 
+    _create_subparsers(helpful)
     _paths_parser(helpful)
     # _plugins_parsing should be the last thing to act upon the main
     # parser (--help should display plugin-specific options last)
     _plugins_parsing(helpful, plugins)
 
-    _create_subparsers(helpful)
 
     return helpful.parse_args()
 
@@ -914,19 +914,28 @@ def _create_subparsers(helpful):
 def _paths_parser(helpful):
     add = helpful.add
     verb = helpful.verb
+    if verb == "help":
+        verb = helpful.help_arg
     helpful.add_group(
         "paths", description="Arguments changing execution paths & servers")
 
-    cph = "Path to where cert is saved (with auth), installed (with install --csr) or revoked."
+    cph = "Path to where cert is saved (with auth --csr), installed from or revoked."
+    section = "paths"
+    if verb in ("install", "revoke", "certonly"):
+        section = verb
     if verb == "certonly":
-        add("paths", "--cert-path", default=flag_default("auth_cert_path"), help=cph)
+        add(section, "--cert-path", default=flag_default("auth_cert_path"), help=cph)
     elif verb == "revoke":
-        add("paths", "--cert-path", type=read_file, required=True, help=cph)
+        add(section, "--cert-path", type=read_file, required=True, help=cph)
     else:
-        add("paths", "--cert-path", help=cph, required=(verb == "install"))
+        add(section, "--cert-path", help=cph, required=(verb == "install"))
 
+    section = "paths"
+    if verb in ("install", "revoke"):
+        section = verb
+    print helpful.help_arg, helpful.help_arg == "install"
     # revoke --key-path reads a file, install --key-path takes a string
-    add("paths", "--key-path", type=((verb == "revoke" and read_file) or str),
+    add(section, "--key-path", type=((verb == "revoke" and read_file) or str),
         required=(verb == "install"),
         help="Path to private key for cert creation or revocation (if account key is missing)")
 


Test patch:
diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py
--- a/letsencrypt/tests/cli_test.py
+++ b/letsencrypt/tests/cli_test.py
@@ -64,47 +64,56 @@ def test_no_flags(self):
             self._call([])
             self.assertEqual(1, mock_run.call_count)
 
-    def test_help(self):
-        self.assertRaises(SystemExit, self._call, ['--help'])
-        self.assertRaises(SystemExit, self._call, ['--help', 'all'])
+    def _help_output(self, args):
+        "Run a help command, and return the help string for scrutiny"
         output = StringIO.StringIO()
         with mock.patch('letsencrypt.cli.sys.stdout', new=output):
-            plugins = disco.PluginsRegistry.find_all()
-            self.assertRaises(SystemExit, self._call_stdout, ['--help', 'all'])
+            self.assertRaises(SystemExit, self._call_stdout, args)
             out = output.getvalue()
-            self.assertTrue("--configurator" in out)
-            self.assertTrue("how a cert is deployed" in out)
-            self.assertTrue("--manual-test-mode" in out)
-            output.truncate(0)
+            return out
 
-            self.assertRaises(SystemExit, self._call_stdout, ['-h', 'nginx'])
-            out = output.getvalue()
-            if "nginx" in plugins:
-                # may be false while building distributions without plugins
-                self.assertTrue("--nginx-ctl" in out)
-            self.assertTrue("--manual-test-mode" not in out)
-            self.assertTrue("--checkpoints" not in out)
-            output.truncate(0)
-
-            self.assertRaises(SystemExit, self._call_stdout, ['-h'])
-            out = output.getvalue()
-            if "nginx" in plugins:
-                self.assertTrue("Use the Nginx plugin" in out)
-            else:
-                self.assertTrue("(nginx support is experimental" in out)
-            output.truncate(0)
-
-            self.assertRaises(SystemExit, self._call_stdout, ['--help', 'plugins'])
-            out = output.getvalue()
-            self.assertTrue("--manual-test-mode" not in out)
-            self.assertTrue("--prepare" in out)
-            self.assertTrue("Plugin options" in out)
-            output.truncate(0)
-
-            self.assertRaises(SystemExit, self._call_stdout, ['-h'])
-            out = output.getvalue()
-            from letsencrypt import cli
-            self.assertTrue(cli.usage_strings(plugins)[0] in out)
+    def test_help(self):
+        self.assertRaises(SystemExit, self._call, ['--help'])
+        self.assertRaises(SystemExit, self._call, ['--help', 'all'])
+        plugins = disco.PluginsRegistry.find_all()
+        out = self._help_output(['--help', 'all'])
+        self.assertTrue("--configurator" in out)
+        self.assertTrue("how a cert is deployed" in out)
+        self.assertTrue("--manual-test-mode" in out)
+
+        out = self._help_output(['-h', 'nginx'])
+        if "nginx" in plugins:
+            # may be false while building distributions without plugins
+            self.assertTrue("--nginx-ctl" in out)
+        self.assertTrue("--manual-test-mode" not in out)
+        self.assertTrue("--checkpoints" not in out)
+
+        out = self._help_output(['-h'])
+        if "nginx" in plugins:
+            self.assertTrue("Use the Nginx plugin" in out)
+        else:
+            self.assertTrue("(nginx support is experimental" in out)
+
+        out = self._help_output(['--help', 'plugins'])
+        self.assertTrue("--manual-test-mode" not in out)
+        self.assertTrue("--prepare" in out)
+        self.assertTrue("Plugin options" in out)
+
+        out = self._help_output(['--help', 'install'])
+        self.assertTrue("--cert-path" in out)
+        self.assertTrue("--key-path" in out)
+
+        out = self._help_output(['--help', 'revoke'])
+        self.assertTrue("--cert-path" in out)
+        self.assertTrue("--key-path" in out)
+
+        out = self._help_output(['-h', 'config_changes'])
+        self.assertTrue("--cert-path" not in out)
+        self.assertTrue("--key-path" not in out)
+
+        out = self._help_output(['-h'])
+        from letsencrypt import cli
+        self.assertTrue(cli.usage_strings(plugins)[0] in out)
 
     def test_configurator_selection(self):
         real_plugins = disco.PluginsRegistry.find_all()