Repository: certbot/certbot

Issue:
"plugins" command shows no output
Another regression in the CLI:

```
# ./letsencrypt-auto --text -vvvv --agree-dev-preview plugins
-vvvv
Updating letsencrypt and virtual environment dependencies...
Requirement already up-to-date: setuptools in /root/.local/share/letsencrypt/lib/python2.7/site-packages
Requirement already up-to-date: pip in /root/.local/share/letsencrypt/lib/python2.7/site-packages
Requirement already up-to-date: letsencrypt in /root/.local/share/letsencrypt/lib/python2.7/site-packages
Requirement already up-to-date: letsencrypt-apache in /root/.local/share/letsencrypt/lib/python2.7/site-packages
Collecting ConfigArgParse from git+https://github.com/kuba/ConfigArgParse.git@python2.6-0.9.3#egg=ConfigArgParse (from -r py26reqs.txt (line 1))
  Cloning https://github.com/kuba/ConfigArgParse.git (to python2.6-0.9.3) to /tmp/pip-build-GRDICH/ConfigArgParse
Requirement already up-to-date: zope.interface in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from letsencrypt)
Requirement already up-to-date: setuptools in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from letsencrypt)
Requirement already up-to-date: python2-pythondialog>=3.2.2rc1 in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from letsencrypt)
Requirement already up-to-date: PyOpenSSL in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from letsencrypt)
Requirement already up-to-date: requests in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from letsencrypt)
Requirement already up-to-date: parsedatetime in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from letsencrypt)
Requirement already up-to-date: configobj in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from letsencrypt)
Requirement already up-to-date: pytz in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from letsencrypt)
Requirement already up-to-date: psutil>=2.1.0 in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from letsencrypt)
Requirement already up-to-date: six in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from letsencrypt)
Requirement already up-to-date: cryptography>=0.7 in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from letsencrypt)
Requirement already up-to-date: acme==0.0.0.dev20151107 in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from letsencrypt)
Requirement already up-to-date: zope.component in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from letsencrypt)
Requirement already up-to-date: mock in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from letsencrypt)
Requirement already up-to-date: pyrfc3339 in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from letsencrypt)
Requirement already up-to-date: python-augeas in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from letsencrypt-apache)
Requirement already up-to-date: enum34 in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from cryptography>=0.7->letsencrypt)
Requirement already up-to-date: ipaddress in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from cryptography>=0.7->letsencrypt)
Requirement already up-to-date: pyasn1>=0.1.8 in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from cryptography>=0.7->letsencrypt)
Requirement already up-to-date: idna>=2.0 in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from cryptography>=0.7->letsencrypt)
Requirement already up-to-date: cffi>=1.1.0 in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from cryptography>=0.7->letsencrypt)
Requirement already up-to-date: ndg-httpsclient in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from acme==0.0.0.dev20151107->letsencrypt)
Requirement already up-to-date: werkzeug in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from acme==0.0.0.dev20151107->letsencrypt)
Requirement already up-to-date: zope.event in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from zope.component->letsencrypt)
Requirement already up-to-date: funcsigs in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from mock->letsencrypt)
Requirement already up-to-date: pbr>=0.11 in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from mock->letsencrypt)
Requirement already up-to-date: pycparser in /root/.local/share/letsencrypt/lib/python2.7/site-packages (from cffi>=1.1.0->cryptography>=0.7->letsencrypt)
Installing collected packages: ConfigArgParse
  Found existing installation: ConfigArgParse 0.9.3
    Uninstalling ConfigArgParse-0.9.3:
      Successfully uninstalled ConfigArgParse-0.9.3
  Running setup.py install for ConfigArgParse
Successfully installed ConfigArgParse-0.9.3
Running with virtualenv: /root/.local/share/letsencrypt/bin/letsencrypt --text -vvvv --agree-dev-preview plugins
2015-11-07 13:47:38,928:DEBUG:letsencrypt.cli:Root logging level set at -10
2015-11-07 13:47:38,932:INFO:letsencrypt.cli:Saving debug log to /var/log/letsencrypt/letsencrypt.log
2015-11-07 13:47:38,933:DEBUG:letsencrypt.cli:letsencrypt version: 0.0.0.dev20151107
2015-11-07 13:47:38,933:DEBUG:letsencrypt.cli:Arguments: ['--text', '-vvvv', '--agree-dev-preview']
2015-11-07 13:47:38,935:DEBUG:letsencrypt.cli:Discovered plugins: PluginsRegistry(PluginEntryPoint#apache,PluginEntryPoint#webroot,PluginEntryPoint#null,PluginEntryPoint#manual,PluginEntryPoint#standalone)
2015-11-07 13:47:38,936:DEBUG:letsencrypt.cli:Expected interfaces: None
2015-11-07 13:47:38,938:DEBUG:letsencrypt.cli:Filtered plugins: PluginsRegistry(PluginEntryPoint#apache,PluginEntryPoint#webroot,PluginEntryPoint#standalone)
#
```

Stray "False False" in help output
```
$ letsencrypt --server https://acme-v01.api.letsencrypt.org/directory --installer nginx --authenticator nginx --text --agree-eula -d 44.le-test.hoffman-andrews.com run -vvvv
False False
usage: 
  letsencrypt [SUBCOMMAND] [options] [-d domain] [-d domain] ...
```



Discussion:

I know for a fact that this is caused by one of the recently merged PRs because I had to update my only 10 days old local repo to be able to reproduce this issue.

There are a couple of debug statements in the code that should be removed:
- https://github.com/letsencrypt/letsencrypt/blob/b1508f38506376faabe753b343b93c7aeaa96121/letsencrypt/cli.py#L552
- https://github.com/letsencrypt/letsencrypt/blob/b1508f38506376faabe753b343b93c7aeaa96121/letsencrypt/cli.py#L560
- https://github.com/letsencrypt/letsencrypt/blob/b1508f38506376faabe753b343b93c7aeaa96121/letsencrypt/cli.py#L566
- https://github.com/letsencrypt/letsencrypt/blob/b1508f38506376faabe753b343b93c7aeaa96121/letsencrypt/cli.py#L938

@TheNavigat, would you like to fix it?

Also, https://github.com/letsencrypt/letsencrypt/blob/b1508f38506376faabe753b343b93c7aeaa96121/letsencrypt/cli.py#L658 should be changed to `sys.exit(usage)`

@kuba Yes, please. I've actually spent 15 minutes trying to understand why that happens, and the only progress I made was that it's probably in cli.py. It looks like I'll need way more time to understand how this works than I did with HTTPS Everywhere.

I'll create a pull request in a few.

My comment about `sys.exit` was wrong: `sys.exit("string")` will trigger non-zero exit code, but for `--help` we should still return 0, sorry!


Resolving patch:
diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py
--- a/letsencrypt/cli.py
+++ b/letsencrypt/cli.py
@@ -549,6 +549,7 @@ def plugins_cmd(args, config, plugins):  # TODO: Use IDisplay rather than print
     logger.debug("Filtered plugins: %r", filtered)
 
     if not args.init and not args.prepare:
+        print str(filtered)
         return
 
     filtered.init(config)
@@ -556,11 +557,13 @@ def plugins_cmd(args, config, plugins):  # TODO: Use IDisplay rather than print
     logger.debug("Verified plugins: %r", verified)
 
     if not args.prepare:
+        print str(verified)
         return
 
     verified.prepare()
     available = verified.available()
     logger.debug("Prepared plugins: %s", available)
+    print str(available)
 
 
 def read_file(filename, mode="rb"):


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
@@ -168,6 +168,48 @@ def test_plugins(self):
                   for r in xrange(len(flags)))):
             self._call(['plugins'] + list(args))
 
+    @mock.patch('letsencrypt.cli.plugins_disco')
+    def test_plugins_no_args(self, mock_disco):
+        ifaces = []
+        plugins = mock_disco.PluginsRegistry.find_all()
+
+        _, stdout, _, _ = self._call(['plugins'])
+        plugins.visible.assert_called_once_with()
+        plugins.visible().ifaces.assert_called_once_with(ifaces)
+        filtered = plugins.visible().ifaces()
+        stdout.write.called_once_with(str(filtered))
+
+    @mock.patch('letsencrypt.cli.plugins_disco')
+    def test_plugins_init(self, mock_disco):
+        ifaces = []
+        plugins = mock_disco.PluginsRegistry.find_all()
+
+        _, stdout, _, _ = self._call(['plugins', '--init'])
+        plugins.visible.assert_called_once_with()
+        plugins.visible().ifaces.assert_called_once_with(ifaces)
+        filtered = plugins.visible().ifaces()
+        self.assertEqual(filtered.init.call_count, 1)
+        filtered.verify.assert_called_once_with(ifaces)
+        verified = filtered.verify()
+        stdout.write.called_once_with(str(verified))
+
+    @mock.patch('letsencrypt.cli.plugins_disco')
+    def test_plugins_prepare(self, mock_disco):
+        ifaces = []
+        plugins = mock_disco.PluginsRegistry.find_all()
+
+        _, stdout, _, _ = self._call(['plugins', '--init', '--prepare'])
+        plugins.visible.assert_called_once_with()
+        plugins.visible().ifaces.assert_called_once_with(ifaces)
+        filtered = plugins.visible().ifaces()
+        self.assertEqual(filtered.init.call_count, 1)
+        filtered.verify.assert_called_once_with(ifaces)
+        verified = filtered.verify()
+        verified.prepare.assert_called_once_with()
+        verified.available.assert_called_once_with()
+        available = verified.available()
+        stdout.write.called_once_with(str(available))
+
     def test_certonly_bad_args(self):
         ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR])
         self.assertEqual(ret, '--domain and --csr are mutually exclusive')