类似apt列式输出的Python库

3

Debian的apt工具以统一宽度列的形式输出结果。例如,尝试运行“aptitude search svn”..所有名称都将以相同宽度的第一列显示。

现在,如果调整终端大小,列宽度将相应调整。

是否有一个Python库可以实现这个功能?注意,该库必须知道终端宽度并以表格作为输入-可能是[('rapidsvn','subversion的GUI客户端'), ...] ..您还可以指定第一列(或任何列)的最大宽度。请注意,如下面第二列中的字符串超过终端宽度则将其修剪,从而不会引入不需要的第二行。

$ aptitude search svn
[...]
p   python-svn-dbg                    - A(nother) Python interface to Subversion (d
v   python2.5-svn                     -                                            
v   python2.6-svn                     -                                            
p   rapidsvn                          - A GUI client for subversion                
p   statsvn                           - SVN repository statistics                  
p   svn-arch-mirror                   - one-way mirroring from Subversion to Arch r
p   svn-autoreleasedeb                - Automatically release/upload debian package
p   svn-buildpackage                  - helper programs to maintain Debian packages
p   svn-load                          - An enhanced import facility for Subversion 
p   svn-workbench                     - A Workbench for Subversion                 
p   svnmailer                         - extensible Subversion commit notification t
p   websvn                            - interface for subversion repositories writt
$
编辑: (回应Alex下面的答案) ... 输出结果类似于'aptitude search',即 1) 只有最后一列(该行中唯一具有最长字符串的列)需要被修剪,2) 通常只有2-4列,但最后一列("description")预计至少占用终端宽度的一半。3) 所有行包含相等数量的列,4) 所有条目仅为字符串。

我猜它使用curses / ncurses。 - tonfa
为什么apt需要ncurses呢?在Linux上,apt只需要查看COLUMNS环境变量就可以获取终端的宽度。然而,我正在寻找一个跨平台(*nix和Windows)的解决方案。 - Sridhar Ratnakumar
1
当我通过ssh登录到Linux时(从Terminal.App,但我认为这并不重要),我的环境中没有COLUMNS。尽管如此,aptitude search显示正常。所以,我认为你在关于“只需要查看COLUMNS环境变量”的断言上是完全错误的 - 这不是一个可用的解决方案,aptitude也不是使用它。 - Alex Martelli
谢谢,那确实是一个大胆(且错误)的断言。 (nosklo指出apt使用cwidget) - Sridhar Ratnakumar
补充一下Alex说的,COLUMNS不是一个环境变量,而是一个外壳变量。因此,它无法从另一个子进程中访问。http://mail.python.org/pipermail/python-list/2000-May/033682.html - Sridhar Ratnakumar
显示剩余2条评论
4个回答

4
更新: 现在可以在Python库applib中找到colprint例程,托管在GitHub上。对于那些感兴趣的人,这是完整的程序:
# This function was written by Alex Martelli
# https://dev59.com/0EfYs4cB2Jgan1znIvNr
def colprint(table, totwidth=None):
    """Print the table in terminal taking care of wrapping/alignment

    - `table`:    A table of strings. Elements must not be `None`
    - `totwidth`: If None, console width is used
    """
    if not table: return
    if totwidth is None:
        totwidth = find_console_width()
        totwidth -= 1 # for not printing an extra empty line on windows
    numcols = max(len(row) for row in table)
    # ensure all rows have >= numcols columns, maybe empty
    padded = [row+numcols*('',) for row in table]
    # compute col widths, including separating space (except for last one)
    widths = [ 1 + max(len(x) for x in column) for column in zip(*padded)]
    widths[-1] -= 1
    # drop or truncate columns from the right in order to fit
    while sum(widths) > totwidth:
        mustlose = sum(widths) - totwidth
        if widths[-1] <= mustlose:
            del widths[-1]
        else:
            widths[-1] -= mustlose
            break
    # and finally, the output phase!
    for row in padded:
        print(''.join([u'%*s' % (-w, i[:w])
                       for w, i in zip(widths, row)]))

def find_console_width():
    if sys.platform.startswith('win'):
        return _find_windows_console_width()
    else:
        return _find_unix_console_width()
def _find_unix_console_width():
    """Return the width of the Unix terminal

    If `stdout` is not a real terminal, return the default value (80)
    """
    import termios, fcntl, struct, sys

    # fcntl.ioctl will fail if stdout is not a tty
    if not sys.stdout.isatty():
        return 80

    s = struct.pack("HHHH", 0, 0, 0, 0)
    fd_stdout = sys.stdout.fileno()
    size = fcntl.ioctl(fd_stdout, termios.TIOCGWINSZ, s)
    height, width = struct.unpack("HHHH", size)[:2]
    return width
def _find_windows_console_width():
    """Return the width of the Windows console

    If the width cannot be determined, return the default value (80)
    """
    # http://code.activestate.com/recipes/440694/
    from ctypes import windll, create_string_buffer
    STDIN, STDOUT, STDERR = -10, -11, -12

    h = windll.kernel32.GetStdHandle(STDERR)
    csbi = create_string_buffer(22)
    res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi)

    if res:
        import struct
        (bufx, bufy, curx, cury, wattr,
         left, top, right, bottom,
         maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw)
        sizex = right - left + 1
        sizey = bottom - top + 1
    else:
        sizex, sizey = 80, 25

    return sizex

请你能提供一个小的使用示例吗?一个"table"对象应该是什么样子的?如果我用这样的表格调用它:table = [["HEAD1", "HEAD2", "HEAD3"],["text1", "text2", "text3"]],我会得到一个错误信息TypeError: can only concatenate list (not "tuple") to list - Wolkenarchitekt
@ifischer - 你能试一下上面代码在GitHub的最新版本吗?链接是https://github.com/ActiveState/applib/blob/master/applib/textui.py#L213 - 如果可以的话,请告诉我它是否有效(这样我就可以更新答案了)。 - Sridhar Ratnakumar
1
是的,它有效了,在使用我先前评论中的表调用时不再出现TypeError错误。 - Wolkenarchitekt

2
首先,使用ioctl获取TTY的大小:
import termios, fcntl, struct, sys

def get_tty_size():
    s = struct.pack("HHHH", 0, 0, 0, 0)
    fd_stdout = sys.stdout.fileno()
    size = fcntl.ioctl(fd_stdout, termios.TIOCGWINSZ, s)
    return struct.unpack("HHHH", size)[:2]

print get_tty_size()

然后使用以下函数来创建列:
pad = lambda s, n=20: "%s%s" % (s,' '*(n-len(s)))

把这些内容结合起来,你就能够为控制台调整列的大小!


termiosfcntl在Windows上不可用。 - Sridhar Ratnakumar
1
对于Windows系统,有一种方法可以获取控制台的宽度:http://code.activestate.com/recipes/440694/ - Sridhar Ratnakumar
如果stdout不是一个tty,这段代码将无法工作。请查看下面更新的程序。 - Sridhar Ratnakumar

2

我认为没有一种通用的跨平台方法可以“获取终端的宽度” - 绝对不是“查看COLUMNS环境变量”(请参见我在问题上的评论)。 在Linux和Mac OS X(以及我期望的所有现代Unix版本)上,

curses.wrapper(lambda _: curses.tigetnum('cols'))

返回列数; 但我不确定wcurses是否在Windows上支持此功能。

一旦您拥有所需的输出宽度(可以从os.environ['COLUMNS']获取,如果您坚持使用,或通过curses,或从oracle获取,或默认为80,或以任何其他方式),其余部分就相当可行。这是一个棘手的工作,很容易出现一些偏差错误,并且非常容易受到许多详细规格的影响,您并没有完全清楚这些规格,例如:哪一列被切割以避免换行 - 它总是最后一列,还是...? 根据您的问题,样本输出中显示3列,但只传递了两列,这是怎么回事...? 如果不是所有行都具有相同数量的列,则会发生什么?表中的所有条目都必须是字符串吗?以及许多其他类似的奥秘。

因此,针对您未明确表达的所有规格进行某种任意猜测的方法可能是以下内容:

import sys

def colprint(totwidth, table):
  numcols = max(len(row) for row in table)
  # ensure all rows have >= numcols columns, maybe empty
  padded = [row+numcols*('',) for row in table]
  # compute col widths, including separating space (except for last one)
  widths = [ 1 + max(len(x) for x in column) for column in zip(*padded)]
  widths[-1] -= 1
  # drop or truncate columns from the right in order to fit
  while sum(widths) > totwidth:
    mustlose = sum(widths) - totwidth
    if widths[-1] <= mustlose:
      del widths[-1]
    else:
      widths[-1] -= mustlose
      break
  # and finally, the output phase!
  for row in padded:
    for w, i in zip(widths, row):
      sys.stdout.write('%*s' % (-w, i[:w]))
    sys.stdout.write('\n')

就假设而言,通常情况下...输出将类似于“aptitude search”,即1)只有最后一列(即行中最长字符串的唯一列)需要修剪,2)通常只有2-4列,但预计最后一列(“描述”)将占据至少终端宽度的一半。3)所有行包含相同数量的列,4)所有条目仅为字符串。 - Sridhar Ratnakumar
你可能想要对没有换行和缩进的代码进行格式化 - Sridhar Ratnakumar
@Sridhar,谢谢你提醒格式问题(我在两小时前看到你的评论后立即编辑了回复)。顺便说一下,我的代码应该满足你现在给出的所有规格要求——它们大致上是我猜测的;你可以试一下。 - Alex Martelli
太好了!我直到今天才看到这个评论,当时我正准备开始编写一个包含类和其他内容的“库”来完成这个任务。哈哈!我还是有一种过度复杂化事物的倾向。 - Sridhar Ratnakumar
Alex,我进一步修改了你的代码(如下所示),使用print而不是sys.stdout,这样它就能够处理Unicode字符了。参考链接:https://dev59.com/B3M_5IYBdhLWcg3wNwUv#1473764 - Sridhar Ratnakumar
请你能提供一个小的使用示例吗?一个"table"对象应该是什么样子的?如果我用这样的表格调用它:table = [["HEAD1", "HEAD2", "HEAD3"],["text1", "text2", "text3"]],我会得到一个错误信息TypeError: can only concatenate list (not "tuple") to list - Wolkenarchitekt

2

好的,aptitude使用cwidget在纯文本显示中格式化列。您可以调用cwidget编写其Python扩展程序,但我认为这并不值得麻烦...您可以使用所需的实际水平大小来计算字符。


1
aptitude在运行命令'aptitude search foo'时,是否也使用cwidget来打印表格输出? - Sridhar Ratnakumar
1
好的,是的。我也刚刚查看了源代码;它使用了columnizer.layout_columnsde_columnize,这两者又依赖于cwidget API。 - Sridhar Ratnakumar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接