Repository: aws/aws-cli

Issue:
Add terminal paging to help for windows
Add terminal paging for windows.  In linux, the "help" gets paginated with a terminal PAGER, but in windows it just prints to the screen. This fixes that problem. Note that pydoc is a python standard library and on windows it defaults to using "more" which is a windows utility much like less.



Discussion:
[![Coverage Status](https://coveralls.io/builds/1967429/badge)](https://coveralls.io/builds/1967429)

Coverage decreased (-0.03%) to 91.97% when pulling **a67915bbf862ac08629972cb20548bcb5700fd4e on nhumrich:develop** into **ee499af525fc39d683f4a7f666554bdac4ac239d on aws:develop**.

[![Coverage Status](https://coveralls.io/builds/1967659/badge)](https://coveralls.io/builds/1967659)

Coverage decreased (-0.02%) to 91.98% when pulling **7c82542da931cd18984d7662989b48793617e438 on nhumrich:develop** into **ee499af525fc39d683f4a7f666554bdac4ac239d on aws:develop**.

Seems like we should just pull up `get_pager_cmdline` and other paging methods into the base help class and default to a PAGER of `more` for windows.  That way we'd have consistent behavior across platforms.

Or we could just let pydoc.pager do all the work and remove some code. But I'm not sure how much that will change behavior.

Not opposed to that.  My only concern is that if we're going to make a change to support paging on windows, we should make it as consistent cross platform as possible.  If you could maintain these features, then feel free to switch over to pydoc:
1. Works "out of the box" for both powershell/cmd on windows.
2. Honors the MANPAGER/PAGER env vars that are standard in posix.

Updated. Changed pydoc to be the pager for all platforms. PosixRender just does the man page formating. Works on powershell and cmd as well as python 2 and 3. Pydoc honors PAGER but not MANPAGER, added a line to handle MANPAGER.


Resolving patch:
diff --git a/awscli/help.py b/awscli/help.py
--- a/awscli/help.py
+++ b/awscli/help.py
@@ -1,4 +1,4 @@
-# Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# Copyright 2012-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License"). You
 # may not use this file except in compliance with the License. A copy of
@@ -10,7 +10,6 @@
 # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
 # ANY KIND, either express or implied. See the License for the specific
 # language governing permissions and limitations under the License.
-import sys
 import logging
 import os
 import platform
@@ -50,23 +49,47 @@ def get_renderer():
         return PosixHelpRenderer()
 
 
-class HelpRenderer(object):
+class PagingHelpRenderer(object):
     """
     Interface for a help renderer.
 
     The renderer is responsible for displaying the help content on
     a particular platform.
+
     """
 
+    PAGER = None
+
+    def get_pager_cmdline(self):
+        pager = self.PAGER
+        if 'MANPAGER' in os.environ:
+            pager = os.environ['MANPAGER']
+        elif 'PAGER' in os.environ:
+            pager = os.environ['PAGER']
+        return shlex.split(pager)
+
     def render(self, contents):
         """
         Each implementation of HelpRenderer must implement this
         render method.
         """
-        pass
+        converted_content = self._convert_doc_content(contents)
+        self._send_output_to_pager(converted_content)
 
+    def _send_output_to_pager(self, output):
+        cmdline = self.get_pager_cmdline()
+        LOG.debug("Running command: %s", cmdline)
+        p = self._popen(cmdline, stdin=PIPE)
+        p.communicate(input=output)
+
+    def _popen(self, *args, **kwargs):
+        return Popen(*args, **kwargs)
 
-class PosixHelpRenderer(HelpRenderer):
+    def _convert_doc_content(self, contents):
+        return contents
+
+
+class PosixHelpRenderer(PagingHelpRenderer):
     """
     Render help content on a Posix-like system.  This includes
     Linux and MacOS X.
@@ -74,15 +97,7 @@ class PosixHelpRenderer(HelpRenderer):
 
     PAGER = 'less -R'
 
-    def get_pager_cmdline(self):
-        pager = self.PAGER
-        if 'MANPAGER' in os.environ:
-            pager = os.environ['MANPAGER']
-        elif 'PAGER' in os.environ:
-            pager = os.environ['PAGER']
-        return shlex.split(pager)
-
-    def render(self, contents):
+    def _convert_doc_content(self, contents):
         man_contents = publish_string(contents, writer=manpage.Writer())
         if not self._exists_on_path('groff'):
             raise ExecutableNotFoundError('groff')
@@ -90,25 +105,7 @@ def render(self, contents):
         LOG.debug("Running command: %s", cmdline)
         p3 = self._popen(cmdline, stdin=PIPE, stdout=PIPE, stderr=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=PIPE)
-        p4.communicate(input=groff_output)
-        sys.exit(1)
-
-    def _get_rst2man_name(self):
-        if self._exists_on_path('rst2man.py'):
-            return 'rst2man.py'
-        elif self._exists_on_path('rst2man'):
-            # Some distros like ubuntu will rename rst2man.py to rst2man
-            # if you install their version (i.e. "apt-get install
-            # python-docutils").  Though they could technically rename
-            # this to anything we'll support it renamed to 'rst2man' by
-            # explicitly checking for this case ourself.
-            return 'rst2man'
-        else:
-            # Give them the original name as set from docutils.
-            raise ExecutableNotFoundError('rst2man.py')
+        return groff_output
 
     def _exists_on_path(self, name):
         # Since we're only dealing with POSIX systems, we can
@@ -116,30 +113,22 @@ def _exists_on_path(self, name):
         return any([os.path.exists(os.path.join(p, name))
                     for p in os.environ.get('PATH', '').split(os.pathsep)])
 
-    def _popen(self, *args, **kwargs):
-        return Popen(*args, **kwargs)
 
+class WindowsHelpRenderer(PagingHelpRenderer):
+    """Render help content on a Windows platform."""
 
-class WindowsHelpRenderer(HelpRenderer):
-    """
-    Render help content on a Windows platform.
-    """
+    PAGER = 'more'
 
-    def render(self, contents):
+    def _convert_doc_content(self, contents):
         text_output = publish_string(contents,
                                      writer=TextWriter())
-        sys.stdout.write(text_output.decode('utf-8'))
-        sys.exit(1)
-
-
-class RawRenderer(HelpRenderer):
-    """
-    Render help as the raw ReST document.
-    """
+        return text_output
 
-    def render(self, contents):
-        sys.stdout.write(contents)
-        sys.exit(1)
+    def _popen(self, *args, **kwargs):
+        # Also set the shell value to True.  To get any of the
+        # piping to a pager to work, we need to use shell=True.
+        kwargs['shell'] = True
+        return Popen(*args, **kwargs)
 
 
 class HelpCommand(object):


Test patch:
diff --git a/tests/unit/customizations/test_cloudsearchdomain.py b/tests/unit/customizations/test_cloudsearchdomain.py
--- a/tests/unit/customizations/test_cloudsearchdomain.py
+++ b/tests/unit/customizations/test_cloudsearchdomain.py
@@ -12,7 +12,7 @@
 # language governing permissions and limitations under the License.
 from awscli.testutils import unittest
 from awscli.testutils import BaseAWSCommandParamsTest
-from awscli.help import HelpRenderer
+from awscli.help import PagingHelpRenderer
 from awscli.customizations.cloudsearchdomain import validate_endpoint_url
 
 import mock
@@ -46,7 +46,7 @@ def test_endpoint_is_required(self):
     def test_endpoint_not_required_for_help(self):
         cmd = self.prefix + 'help'
         with mock.patch('awscli.help.get_renderer') as get_renderer:
-            mock_render = mock.Mock(spec=HelpRenderer)
+            mock_render = mock.Mock(spec=PagingHelpRenderer)
             get_renderer.return_value = mock_render
             stdout, stderr, rc = self.run_cmd(cmd, expected_rc=None)
             # If we get this far we've succeeded, but we can do
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
@@ -17,19 +17,29 @@
 import mock
 
 from awscli.help import PosixHelpRenderer, ExecutableNotFoundError
+from awscli.help import WindowsHelpRenderer
 
 
-class FakePosixHelpRenderer(PosixHelpRenderer):
+class HelpSpyMixin(object):
     def __init__(self):
         self.exists_on_path = {}
         self.popen_calls = []
+        self.mock_popen = mock.Mock()
 
     def _exists_on_path(self, name):
         return self.exists_on_path.get(name)
 
     def _popen(self, *args, **kwargs):
         self.popen_calls.append((args, kwargs))
-        return mock.Mock()
+        return self.mock_popen
+
+
+class FakePosixHelpRenderer(HelpSpyMixin, PosixHelpRenderer):
+    pass
+
+
+class FakeWindowsHelpRenderer(HelpSpyMixin, WindowsHelpRenderer):
+    pass
 
 
 class TestHelpPager(unittest.TestCase):
@@ -78,10 +88,8 @@ def test_pager_with_args(self):
                          pager_cmd.split())
 
     @unittest.skipIf(sys.platform.startswith('win'), "requires posix system")
-    @mock.patch('sys.exit', mock.Mock())
     def test_no_groff_exists(self):
         renderer = FakePosixHelpRenderer()
-        # Simulate neither rst2man.py nor rst2man existing on the path.
         renderer.exists_on_path['groff'] = False
         with self.assertRaisesRegexp(ExecutableNotFoundError,
                                      'Could not find executable named "groff"'):
@@ -92,3 +100,16 @@ def test_shlex_split_for_pager_var(self):
         os.environ['PAGER'] = pager_cmd
         self.assertEqual(self.renderer.get_pager_cmdline(),
                          ['/bin/sh', '-c', "col -bx | vim -c 'set ft=man' -"])
+
+    def test_can_render_contents(self):
+        renderer = FakePosixHelpRenderer()
+        renderer.exists_on_path['groff'] = True
+        renderer.mock_popen.communicate.return_value = ('rendered', '')
+        renderer.render('foo')
+        self.assertEqual(renderer.popen_calls[-1][0], (['less', '-R'],))
+
+    def test_can_page_output_on_windows(self):
+        renderer = FakeWindowsHelpRenderer()
+        renderer.mock_popen.communicate.return_value = ('rendered', '')
+        renderer.render('foo')
+        self.assertEqual(renderer.popen_calls[-1][0], (['more'],))