From ec1b3f9e96f8bb7d2e54569eb9e3fe361051f5ae Mon Sep 17 00:00:00 2001
From: Aleksei Slaikovskii <aslaikov@redhat.com>
Date: Mon, 30 Oct 2017 16:09:14 +0100
Subject: [PATCH] View plugin/command help in pager

ipa help code invokes pager if help lines length is more then
current terminal height.

https://pagure.io/freeipa/issue/7225
---
 ipalib/cli.py  | 105 +++++++++++++++++++++++++++++++++++++++------------------
 ipalib/util.py |  41 ++++++++++++++++++++++
 2 files changed, 113 insertions(+), 33 deletions(-)

diff --git a/ipalib/cli.py b/ipalib/cli.py
index 21563befbc..6abc348d94 100644
--- a/ipalib/cli.py
+++ b/ipalib/cli.py
@@ -38,6 +38,10 @@
 import six
 from six.moves import input
 
+from ipalib.util import (
+    check_client_configuration, get_terminal_height, open_in_pager
+)
+
 if six.PY3:
     unicode = str
 
@@ -55,7 +59,6 @@
 from ipalib.parameters import File, Str, Enum, Any, Flag
 from ipalib.text import _
 from ipalib import api  # pylint: disable=unused-import
-from ipalib.util import check_client_configuration
 from ipapython.dnsutil import DNSName
 from ipapython.admintool import ScriptError
 
@@ -680,10 +683,39 @@ def select_entry(self, entries, format, attrs, display_count=True):
         self.print_line('')
         return selection
 
+
 class help(frontend.Local):
     """
     Display help for a command or topic.
     """
+    class Writer(object):
+        """
+        Writer abstraction
+        """
+        def __init__(self, outfile):
+            self.outfile = outfile
+            self.buffer = []
+
+        @property
+        def buffer_length(self):
+            length = 0
+            for line in self.buffer:
+                length += len(line.split("\n"))
+            return length
+
+        def append(self, string=u""):
+            self.buffer.append(unicode(string))
+
+        def write(self):
+            if self.buffer_length > get_terminal_height():
+                data = "\n".join(self.buffer).encode("utf-8")
+                open_in_pager(data)
+            else:
+                try:
+                    for line in self.buffer:
+                        print(line, file=self.outfile)
+                except IOError:
+                    pass
 
     takes_args = (
         Str('command?', cli_name='topic', label=_('Topic or Command'),
@@ -702,7 +734,7 @@ def _get_topic(self, topic):
         parent_topic = None
 
         for package in self.api.packages:
-            module_name = '%s.%s' % (package.__name__, topic)
+            module_name = '{0}.{1}'.format(package.__name__, topic)
             try:
                 module = sys.modules[module_name]
             except KeyError:
@@ -725,7 +757,8 @@ def _count_topic_mcl(self, topic_name, mod_name):
         self._topics[topic_name][1] = mcl
 
     def _on_finalize(self):
-        # {topic: ["description", mcl, {"subtopic": ["description", mcl, [commands]]}]}
+        # {topic: ["description", mcl, {
+        #     "subtopic": ["description", mcl, [commands]]}]}
         # {topic: ["description", mcl, [commands]]}
         self._topics = {}
         # [builtin_commands]
@@ -749,22 +782,26 @@ def _on_finalize(self):
                         self._topics[topic_name] = [doc, 0, [c]]
                     mcl = max((self._topics[topic_name][1], len(c.name)))
                     self._topics[topic_name][1] = mcl
-                else: # a module grouped in a topic
+                else:  # a module grouped in a topic
                     topic = self._get_topic(topic_name)
                     mod_name = c.topic
                     if topic_name in self._topics:
                         if mod_name in self._topics[topic_name][2]:
                             self._topics[topic_name][2][mod_name][2].append(c)
                         else:
-                            self._topics[topic_name][2][mod_name] = [doc, 0, [c]]
+                            self._topics[topic_name][2][mod_name] = [
+                                doc, 0, [c]]
                             self._count_topic_mcl(topic_name, mod_name)
                         # count mcl for for the subtopic
-                        mcl = max((self._topics[topic_name][2][mod_name][1], len(c.name)))
+                        mcl = max((
+                            self._topics[topic_name][2][mod_name][1],
+                            len(c.name)))
                         self._topics[topic_name][2][mod_name][1] = mcl
                     else:
-                        self._topics[topic_name] = [topic[0].split('\n', 1)[0],
-                                                    0,
-                                                    {mod_name: [doc, 0, [c]]}]
+                        self._topics[topic_name] = [
+                            topic[0].split('\n', 1)[0],
+                            0,
+                            {mod_name: [doc, 0, [c]]}]
                         self._count_topic_mcl(topic_name, mod_name)
             else:
                 self._builtins.append(c)
@@ -778,8 +815,10 @@ def _on_finalize(self):
     def run(self, key=None, outfile=None, **options):
         if outfile is None:
             outfile = sys.stdout
-        writer = self._writer(outfile)
+
+        writer = self.Writer(outfile)
         name = from_cli(key)
+
         if key is None:
             self.api.parser.print_help(outfile)
             return
@@ -804,33 +843,30 @@ def run(self, key=None, outfile=None, **options):
                 if cmd_plugin.NO_CLI:
                     continue
                 mcl = max(mcl, len(cmd_plugin.name))
-                writer('%s  %s' % (to_cli(cmd_plugin.name).ljust(mcl),
-                                   cmd_plugin.summary))
+                writer.append('{0}  {1}'.format(
+                    to_cli(cmd_plugin.name).ljust(mcl), cmd_plugin.summary))
         else:
             raise HelpError(topic=name)
-
-    def _writer(self, outfile):
-        def writer(string=''):
-            try:
-                print(unicode(string), file=outfile)
-            except IOError:
-                pass
-        return writer
+        writer.write()
 
     def print_topics(self, outfile):
-        writer = self._writer(outfile)
+        writer = self.Writer(outfile)
 
         for t, topic in sorted(self._topics.items()):
-            writer('%s  %s' % (to_cli(t).ljust(self._mtl), topic[0]))
+            writer.append('{0}  {1}'.format(
+                to_cli(t).ljust(self._mtl), topic[0]))
+        writer.write()
 
     def print_commands(self, topic, outfile):
-        writer = self._writer(outfile)
+        writer = self.Writer(outfile)
+
         if topic in self._topics and type(self._topics[topic][2]) is dict:
             # we want to display topic which has subtopics
             for subtopic in self._topics[topic][2]:
                 doc = self._topics[topic][2][subtopic][0]
                 mcl = self._topics[topic][1]
-                writer('  %s  %s' % (to_cli(subtopic).ljust(mcl), doc))
+                writer.append('  {0}  {1}'.format(
+                    to_cli(subtopic).ljust(mcl), doc))
         else:
             # we want to display subtopic or a topic which has no subtopics
             if topic in self._topics:
@@ -852,17 +888,20 @@ def print_commands(self, topic, outfile):
             if topic not in self.Command and len(commands) == 0:
                 raise HelpError(topic=topic)
 
-            writer(doc)
+            writer.append(doc)
             if commands:
-                writer()
-                writer(_('Topic commands:'))
+                writer.append()
+                writer.append(_('Topic commands:'))
                 for c in commands:
-                    writer(
-                        '  %s  %s' % (to_cli(c.name).ljust(mcl), c.summary))
-                writer()
-                writer(_('To get command help, use:'))
-                writer(_('  ipa <command> --help'))
-            writer()
+                    writer.append(
+                        '  {0}  {1}'.format(
+                            to_cli(c.name).ljust(mcl), c.summary))
+                writer.append()
+                writer.append(_('To get command help, use:'))
+                writer.append(_('  ipa <command> --help'))
+            writer.append()
+        writer.write()
+
 
 class show_mappings(frontend.Command):
     """
diff --git a/ipalib/util.py b/ipalib/util.py
index 05fefc2b4c..2aa484efb7 100644
--- a/ipalib/util.py
+++ b/ipalib/util.py
@@ -35,6 +35,10 @@
 import encodings
 import sys
 import ssl
+import termios
+import fcntl
+import struct
+import subprocess
 
 import netaddr
 from dns import resolver, rdatatype
@@ -1150,3 +1154,40 @@ def no_matching_interface_for_ip_address_warning(addr_list):
                 "{}".format(ip),
                 file=sys.stderr
             )
+
+
+def get_terminal_height(fd=1):
+    """
+    Get current terminal height
+
+    Args:
+        fd (int): file descriptor. Default: 1 (stdout)
+
+    Returns:
+        int: Terminal height
+    """
+    try:
+        return struct.unpack(
+            'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, b'1234'))[0]
+    except (IOError, OSError, struct.error):
+        return os.environ.get("LINES", 25)
+
+
+def open_in_pager(data):
+    """
+    Open text data in pager
+
+    Args:
+        data (bytes): data to view in pager
+
+    Returns:
+        None
+    """
+    pager = os.environ.get("PAGER", "less")
+    pager_process = subprocess.Popen([pager], stdin=subprocess.PIPE)
+
+    try:
+        pager_process.stdin.write(data)
+        pager_process.communicate()
+    except IOError:
+        pass
