Repository: aws/aws-cli

Issue:
Don't shell out to rst2man.py
We've seen that shelling out to `rst2man.py` can be problematic.

In the "ideal" case (installing from a completely brand new python env or a new virtualenv) everything works.

The issue is some distro packages will rename the executable scripts provided by a package (sometimes `rst2man` or `rst2man-2.7.py` for example).  `docutils` specifies this script as `rst2man.py` which is what you get if you use `pip` to install _everything_.  The problem is that even if you `pip install awscli` if you install this into the system site-packages, pip will notice that `docutils` is already installed (for example via "sudo apt-get install python-docutils" or via the distro's package manager) and skip it.

If pip says docutils is installed then `import docutils` will work, but both the location of the `rst2man.py` script as well as what it's named can vary.  This example may not even be on the PATH by default (homebrew puts things in /usr/local/share/python for example).

The code for `rst2man` is simple enough that we can directly use the code via `docutils` rather than piping to `rst2man`.



Discussion:

Resolving patch:
diff --git a/awscli/help.py b/awscli/help.py
--- a/awscli/help.py
+++ b/awscli/help.py
@@ -17,6 +17,7 @@
 from subprocess import Popen, PIPE
 
 from docutils.core import publish_string
+from docutils.writers import manpage
 import bcdoc
 from bcdoc.clidocs import ReSTDocument
 from bcdoc.clidocs import ProviderDocumentEventHandler
@@ -81,21 +82,17 @@ def get_pager_cmdline(self):
         return pager.split()
 
     def render(self, contents):
-        rst2man = self._get_rst2man_name()
-        cmdline = [rst2man]
-        LOG.debug("Running command: %s", cmdline)
-        p2 = self._popen(cmdline, stdin=PIPE, stdout=PIPE)
-        p2.stdin.write(contents)
-        p2.stdin.close()
+        man_contents = publish_string(contents, writer=manpage.Writer())
         if not self._exists_on_path('groff'):
             raise ExecutableNotFoundError('groff')
         cmdline = ['groff', '-man', '-T', 'ascii']
         LOG.debug("Running command: %s", cmdline)
-        p3 = self._popen(cmdline, stdin=p2.stdout, stdout=PIPE)
+        p3 = self._popen(cmdline, stdin=PIPE, stdout=PIPE)
+        groff_output = p3.communicate(input=man_contents)[0]
         cmdline = self.get_pager_cmdline()
         LOG.debug("Running command: %s", cmdline)
-        p4 = self._popen(cmdline, stdin=p3.stdout)
-        p4.communicate()
+        p4 = self._popen(cmdline, stdin=PIPE)
+        p4.communicate(input=groff_output)
         sys.exit(1)
 
     def _get_rst2man_name(self):


Test patch:
diff --git a/tests/unit/test_help.py b/tests/unit/test_help.py
--- a/tests/unit/test_help.py
+++ b/tests/unit/test_help.py
@@ -77,33 +77,10 @@ def test_pager_with_args(self):
                          pager_cmd.split())
 
     @mock.patch('sys.exit', mock.Mock())
-    def test_with_rst2man_py_exists(self):
-        renderer = FakePosixHelpRenderer()
-        renderer.exists_on_path['rst2man.py'] = True
-        renderer.exists_on_path['rst2man'] = False
-        renderer.exists_on_path['groff'] = True
-        renderer.render('foo')
-        # First call should be to rst2man.py not rst2man
-        # List of tuples, so the first popen call's first position args
-        # first element is the cmdlist ['rst2man.py']
-        self.assertEqual(renderer.popen_calls[0][0][0], ['rst2man.py'])
-
-    @mock.patch('sys.exit', mock.Mock())
-    def test_with_rst2man_py_does_not_exist(self):
-        renderer = FakePosixHelpRenderer()
-        renderer.exists_on_path['rst2man.py'] = False
-        renderer.exists_on_path['rst2man'] = True
-        renderer.exists_on_path['groff'] = True
-        renderer.render('foo')
-        # First call should be to rst2man not rst2man.py
-        self.assertEqual(renderer.popen_calls[0][0][0], ['rst2man'])
-
-    @mock.patch('sys.exit', mock.Mock())
-    def test_no_rst2man_exists(self):
+    def test_no_groff_exists(self):
         renderer = FakePosixHelpRenderer()
         # Simulate neither rst2man.py nor rst2man existing on the path.
-        renderer.exists_on_path['rst2man.py'] = False
-        renderer.exists_on_path['rst2man'] = False
+        renderer.exists_on_path['groff'] = False
         with self.assertRaisesRegexp(ExecutableNotFoundError,
-                                     'Could not find executable named "rst2man.py"'):
+                                     'Could not find executable named "groff"'):
             renderer.render('foo')