3

Debian's apt tool outputs results in uniform width columns. For instance, try running "aptitude search svn" .. and all names appear in the first column of the same width.

Now if you resize the terminal, the column width is adjusted accordingly.

Is there a Python library that enables one to do this? Note that the library has to be aware of the terminal width and take a table as input - which could be, for instance, [('rapidsvn', 'A GUI client for subversion'), ...] .. and you may also specify a max-width for the first column (or any column). Also note how the string in the second column below is trimmed if exceeds the terminal width .. thus not introducing the undesired second line.

$ 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
$

EDIT: (in response to Alex's answer below) ... the output will be similar to 'aptitude search' in that 1) only the last column (which is the only column with the longest string in a row) is to be trimmed, 2) there are typically 2-4 columns only, but the last column ("description") is expected to take at least half the terminal width. 3) all rows contain equal number of columns, 4) all entries are strings only

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Sridhar Ratnakumar
  • 81,433
  • 63
  • 146
  • 187
  • I guess it uses curses / ncurses – tonfa Sep 08 '09 at 23:17
  • Why is ncurses needed at all for apt? apt, on Linux, only needs to look at the `COLUMNS` environment variable to get the width of the terminal. However, I am looking for a cross-platform (*nix and Windows) solution. – Sridhar Ratnakumar Sep 08 '09 at 23:23
  • 1
    When I ssh into Linux (from a Terminal.App, but I don't think that matters) there is no COLUMNS in my environment. Nevertheless, `aptitude search` displays fine. So, I think you couldn't possibly be any wronger in your assertion about " only needs to look at the COLUMNS environment variable" -- it's NOT a usable solution, and it's NOT what aptitude is using, either. – Alex Martelli Sep 09 '09 at 02:21
  • Thanks, that was a bold (and wrong) assertion indeed. (nosklo points out that apt uses cwidget) – Sridhar Ratnakumar Sep 09 '09 at 16:39
  • To add to what Alex said, `COLUMNS` is not an environment variable .. but looks like a shell variable. Hence, it cannot be access from another subprocess. http://mail.python.org/pipermail/python-list/2000-May/033682.html – Sridhar Ratnakumar Sep 10 '09 at 18:07
  • Since this was last answered, there do seem to be a lot more options that might answer this now, like https://github.com/manuelblancovalentin/DynamicTable#readme – rokejulianlockhart Jun 09 '23 at 14:23

4 Answers4

4

Update: The colprint routine is now available in the applib Python library hosted in GitHub.

Here's the complete program for those of you interested:

# This function was written by Alex Martelli
# http://stackoverflow.com/questions/1396820/
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
Sridhar Ratnakumar
  • 81,433
  • 63
  • 146
  • 187
  • Could you please provide a little usage example? What should a "table" object look like? If I call it with a table like this:`table = [["HEAD1", "HEAD2", "HEAD3"],["text1", "text2", "text3"]]`, I get an error `TypeError: can only concatenate list (not "tuple") to list` – Wolkenarchitekt Jan 11 '12 at 09:29
  • @ifischer - can you tried the updated revision of the above code at GitHub https://github.com/ActiveState/applib/blob/master/applib/textui.py#L213 - and let me know if it works (so that I may update the answer here)? – Sridhar Ratnakumar Jan 11 '12 at 20:09
  • 1
    Yes, it works, no TypeError anymore when called with the table from my previous comment – Wolkenarchitekt Jan 12 '12 at 08:54
2

Well, aptitude uses cwidget to format the columns in the text-only display. You could call into cwidget writing a python extension for it, but I don't think it is worth the trouble... You can use your preferred method of getting the actual horizontal size in chars and calculate yourself.

nosklo
  • 217,122
  • 57
  • 293
  • 297
2

First, use ioctl to get the size of the 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()

Then use a function like this to make columns:

pad = lambda s, n=20: "%s%s" % (s,' '*(n-len(s)))

Put those together and you've got resizing columns for the console!

Steven Kryskalla
  • 14,179
  • 2
  • 40
  • 42
2

I don't think there's a general, cross-platform way to "get the width of the terminal" -- most definitely NOT "look at the COLUMNS environment variable" (see my comment on the question). On Linux and Mac OS X (and I expect all modern Unix versions),

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

returns the number of columns; but I don't know if wcurses supports this in Windows.

Once you do have (from os.environ['COLUMNS'] if you insist, or via curses, or from an oracle, or defaulted to 80, or any other way you like) the desired output width, the rest is quite feasible. It's finnicky work, with many chances for off-by-one kinds of errors, and very vulnerable to a lot of detailed specs that you don't make entirely clear, such as: which column gets cut to avoid wrapping -- it it always the last one, or...? How come you're showing 3 columns in the sample output when according to your question only two are passed in...? what is supposed to happen if not all rows have the same number of columns? must all entries in table be strings? and many, many other mysteries of this ilk.

So, taking somewhat-arbitrary guesses for all the specs that you don't express, one approach might be something like...:

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')
martineau
  • 119,623
  • 25
  • 170
  • 301
Alex Martelli
  • 854,459
  • 170
  • 1,222
  • 1,395
  • As for the assumptions, generally speaking ... the output will be similar to 'aptitude search' in that 1) only the last column (which is the only column with the longest string in a row) is to be trimmed, 2) there are typically 2-4 columns only, but the last column ("description") is expected to take at least half the terminal width. 3) all rows contain equal number of columns, 4) all entries are strings only – Sridhar Ratnakumar Sep 09 '09 at 16:46
  • You may want to **format** the code which is appearing without line breaks and indentation. – Sridhar Ratnakumar Sep 09 '09 at 16:47
  • @Sridhar, tx for the heads-up on formatting (I fixed by editing the response as soon as I saw your comment 2 hrs ago). My code should meet all the specs you now give, btw -- they were roughly my guesses; try it out. – Alex Martelli Sep 10 '09 at 04:09
  • Very nice! I missed this comment until today when I was about to finally start writing a 'library' (with classes and whatnot) to do this. Hah! There is still a tendency in me to over-complicate things. – Sridhar Ratnakumar Sep 18 '09 at 21:25
  • Alex, I further modified your code (below) to use `print` instead of `sys.stdout` so it will be unicode-friendly. for reference, see the comments in: http://stackoverflow.com/questions/1473577/writing-unicode-strings-via-sys-stdout-in-python/1473764#1473764 – Sridhar Ratnakumar Sep 24 '09 at 20:18
  • Could you please provide a little usage example? What should a "table" object look like? If I call it with a table like this:`table = [["HEAD1", "HEAD2", "HEAD3"],["text1", "text2", "text3"]]`, I get an error `TypeError: can only concatenate list (not "tuple") to list` – Wolkenarchitekt Jan 11 '12 at 09:30