Skip to content

Commit

Permalink
Support paging through "more" on windows
Browse files Browse the repository at this point in the history
This refactors some of the internals to support paging
to "more" on windows.  This is available both in powershell
and in cmd.
  • Loading branch information
jamesls committed Mar 3, 2015
1 parent b07d1df commit 1b8b091
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 43 deletions.
77 changes: 40 additions & 37 deletions awscli/help.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -50,82 +49,86 @@ 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.
"""

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')
cmdline = ['groff', '-man', '-T', 'ascii']
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)
return groff_output

def _exists_on_path(self, name):
# Since we're only dealing with POSIX systems, we can
# ignore things like PATHEXT.
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):
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/customizations/test_cloudsearchdomain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
29 changes: 25 additions & 4 deletions tests/unit/test_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"'):
Expand All @@ -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'],))

0 comments on commit 1b8b091

Please sign in to comment.