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

Reviewed-By: Tibor Dudlak <tdudlak@redhat.com>
This commit is contained in:
Aleksei Slaikovskii 2017-10-30 16:09:14 +01:00 committed by Alexander Bokovoy
parent 443ecbc29e
commit caed210bc2
2 changed files with 113 additions and 33 deletions

View File

@ -38,6 +38,10 @@ import traceback
import six import six
from six.moves import input from six.moves import input
from ipalib.util import (
check_client_configuration, get_terminal_height, open_in_pager
)
if six.PY3: if six.PY3:
unicode = str unicode = str
@ -55,7 +59,6 @@ from ipalib.constants import CLI_TAB, LDAP_GENERALIZED_TIME_FORMAT
from ipalib.parameters import File, Str, Enum, Any, Flag from ipalib.parameters import File, Str, Enum, Any, Flag
from ipalib.text import _ from ipalib.text import _
from ipalib import api # pylint: disable=unused-import from ipalib import api # pylint: disable=unused-import
from ipalib.util import check_client_configuration
from ipapython.dnsutil import DNSName from ipapython.dnsutil import DNSName
from ipapython.admintool import ScriptError from ipapython.admintool import ScriptError
@ -680,10 +683,39 @@ class textui(backend.Backend):
self.print_line('') self.print_line('')
return selection return selection
class help(frontend.Local): class help(frontend.Local):
""" """
Display help for a command or topic. 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 = ( takes_args = (
Str('command?', cli_name='topic', label=_('Topic or Command'), Str('command?', cli_name='topic', label=_('Topic or Command'),
@ -702,7 +734,7 @@ class help(frontend.Local):
parent_topic = None parent_topic = None
for package in self.api.packages: for package in self.api.packages:
module_name = '%s.%s' % (package.__name__, topic) module_name = '{0}.{1}'.format(package.__name__, topic)
try: try:
module = sys.modules[module_name] module = sys.modules[module_name]
except KeyError: except KeyError:
@ -725,7 +757,8 @@ class help(frontend.Local):
self._topics[topic_name][1] = mcl self._topics[topic_name][1] = mcl
def _on_finalize(self): def _on_finalize(self):
# {topic: ["description", mcl, {"subtopic": ["description", mcl, [commands]]}]} # {topic: ["description", mcl, {
# "subtopic": ["description", mcl, [commands]]}]}
# {topic: ["description", mcl, [commands]]} # {topic: ["description", mcl, [commands]]}
self._topics = {} self._topics = {}
# [builtin_commands] # [builtin_commands]
@ -749,22 +782,26 @@ class help(frontend.Local):
self._topics[topic_name] = [doc, 0, [c]] self._topics[topic_name] = [doc, 0, [c]]
mcl = max((self._topics[topic_name][1], len(c.name))) mcl = max((self._topics[topic_name][1], len(c.name)))
self._topics[topic_name][1] = mcl 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) topic = self._get_topic(topic_name)
mod_name = c.topic mod_name = c.topic
if topic_name in self._topics: if topic_name in self._topics:
if mod_name in self._topics[topic_name][2]: if mod_name in self._topics[topic_name][2]:
self._topics[topic_name][2][mod_name][2].append(c) self._topics[topic_name][2][mod_name][2].append(c)
else: 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) self._count_topic_mcl(topic_name, mod_name)
# count mcl for for the subtopic # 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 self._topics[topic_name][2][mod_name][1] = mcl
else: else:
self._topics[topic_name] = [topic[0].split('\n', 1)[0], self._topics[topic_name] = [
0, topic[0].split('\n', 1)[0],
{mod_name: [doc, 0, [c]]}] 0,
{mod_name: [doc, 0, [c]]}]
self._count_topic_mcl(topic_name, mod_name) self._count_topic_mcl(topic_name, mod_name)
else: else:
self._builtins.append(c) self._builtins.append(c)
@ -778,8 +815,10 @@ class help(frontend.Local):
def run(self, key=None, outfile=None, **options): def run(self, key=None, outfile=None, **options):
if outfile is None: if outfile is None:
outfile = sys.stdout outfile = sys.stdout
writer = self._writer(outfile)
writer = self.Writer(outfile)
name = from_cli(key) name = from_cli(key)
if key is None: if key is None:
self.api.parser.print_help(outfile) self.api.parser.print_help(outfile)
return return
@ -804,33 +843,30 @@ class help(frontend.Local):
if cmd_plugin.NO_CLI: if cmd_plugin.NO_CLI:
continue continue
mcl = max(mcl, len(cmd_plugin.name)) mcl = max(mcl, len(cmd_plugin.name))
writer('%s %s' % (to_cli(cmd_plugin.name).ljust(mcl), writer.append('{0} {1}'.format(
cmd_plugin.summary)) to_cli(cmd_plugin.name).ljust(mcl), cmd_plugin.summary))
else: else:
raise HelpError(topic=name) raise HelpError(topic=name)
writer.write()
def _writer(self, outfile):
def writer(string=''):
try:
print(unicode(string), file=outfile)
except IOError:
pass
return writer
def print_topics(self, outfile): def print_topics(self, outfile):
writer = self._writer(outfile) writer = self.Writer(outfile)
for t, topic in sorted(self._topics.items()): 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): 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: if topic in self._topics and type(self._topics[topic][2]) is dict:
# we want to display topic which has subtopics # we want to display topic which has subtopics
for subtopic in self._topics[topic][2]: for subtopic in self._topics[topic][2]:
doc = self._topics[topic][2][subtopic][0] doc = self._topics[topic][2][subtopic][0]
mcl = self._topics[topic][1] 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: else:
# we want to display subtopic or a topic which has no subtopics # we want to display subtopic or a topic which has no subtopics
if topic in self._topics: if topic in self._topics:
@ -852,17 +888,20 @@ class help(frontend.Local):
if topic not in self.Command and len(commands) == 0: if topic not in self.Command and len(commands) == 0:
raise HelpError(topic=topic) raise HelpError(topic=topic)
writer(doc) writer.append(doc)
if commands: if commands:
writer() writer.append()
writer(_('Topic commands:')) writer.append(_('Topic commands:'))
for c in commands: for c in commands:
writer( writer.append(
' %s %s' % (to_cli(c.name).ljust(mcl), c.summary)) ' {0} {1}'.format(
writer() to_cli(c.name).ljust(mcl), c.summary))
writer(_('To get command help, use:')) writer.append()
writer(_(' ipa <command> --help')) writer.append(_('To get command help, use:'))
writer() writer.append(_(' ipa <command> --help'))
writer.append()
writer.write()
class show_mappings(frontend.Command): class show_mappings(frontend.Command):
""" """

View File

@ -35,6 +35,10 @@ import dns
import encodings import encodings
import sys import sys
import ssl import ssl
import termios
import fcntl
import struct
import subprocess
import netaddr import netaddr
from dns import resolver, rdatatype from dns import resolver, rdatatype
@ -1157,3 +1161,40 @@ def no_matching_interface_for_ip_address_warning(addr_list):
"{}".format(ip), "{}".format(ip),
file=sys.stderr 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