May-27-2017, 08:00 AM
here is a "first working" case of my class to tightly columnize output. as a command/program it will columnize STDIN to STDOUT. this is just a first time running. more work to do. posted before completion to let you see what i have been wasting my time on.
#!/usr/bin/env python3 # -*- coding: utf-8 -*- from __future__ import division, print_function, unicode_literals """ file columnize.py purpose class to convert a long stream of short lines to multiple columns in one or more pages passed to caller or printed to a given file, where columns are aligned vertically and can vary in width and number of columns per page (more can fit when they are narrower) email 10054452614123394844460370234029112340408691 The intent is that this command works correctly under both Python 2 and Python 3. Please report failures or code improvement to the author. """ __license__ = """ Copyright © 2017, by Phil D. Howard - all other rights reserved Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA, OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE, OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. The author may be contacted by decoding the number 10054452614123394844460370234029112340408691 (provu igi la numeron al duuma) """ import fcntl, os, struct, sys, termios import printobject print_object = printobject.print_object if sys.version_info.major<3: ints = (int,long,) strs = (str,unicode,) def sorteditems(d): return sorted(d.iteritems()) BrokenPipeError = IOError else: ints = (int,) strs = (str,bytes,bytearray,) def sorteditems(d): return sorted(d.items()) def filemap(f,m): """map various values to various file types""" if f is None: return None elif f is False: return sys.stderr elif f is True: return sys.stdout elif isinstance(f,int): return os.fdopen(f,m) if sys.version_info.major < 3: if isinstance(f,(str,unicode)): return open(f,m) else: if isinstance(f,str): return open(f,m) if isinstance(f,list): return [filemap(a,m) for a in f] elif isinstance(f,tuple): return tuple([filemap(a,m) for a in f]) elif isinstance(f,set): return set([filemap(a,m) for a in f]) elif isinstance(f,dict): if sys.version_info.major < 3: return {k:filemap(v,m) for k,v in f.iteritems()} else: return {k:filemap(v,m) for k,v in f.items()} else: return f class colpage: """class to columnize a sequence of lines into columns and pages""" def __init__(self,*args,**opts): """Customize a colpage instance.""" self._init() opts = self.set_opts(**opts) msg = '' if len(args)>0: for n,a in enumerate(args): print('unused argument',n,'=',repr(a),file=sys.stderr) msg += str(len(args))+' unused argument'+\ ('' if len(args)<2 else 's') if len(opts)>0: for k,v in sorteditems(opts): print('unknown option',repr(k)+'='+repr(v),file=sys.stderr) if len(msg) > 0: msg += ' and ' msg += str(len(opts))+' unknown option'+\ ('' if len(opts)<2 else 's') if len(msg) > 0: raise ValueError(msg) return def _init(self): """Initialize variables to initial values.""" self._columns = [] self._closed = False self._pages = [] self.bottom = None self.call = None self.file = sys.stdout self.gutter = 1 self.height1 = 25 self.heightx = 24 self.left = None self.prefix1 = '' self.prefixx = '' self.right = None self.sep = ' ' self.suffix1 = '' self.suffixx = '' self.top = None self.width = 72 return def _is_enough_for_a_page(self): """Return True if enough data is available to make a full page.""" width = - self.gutter for col in self._columns: width += self.gutter width += col[0] if width >= self.width: return True return False def _process_page(self): """Make a page and output it, popping columns used to make it.""" # collect the columns to make the page with cols = [] width = - self.gutter while self._columns: width += self.gutter width += self._columns[0][0] if width >= self.width: break cols.append(self._columns.pop(0)) # form the collected columns into a page page = [] sep = (' ' if self.sep is None else self.sep)*self.gutter for y in range(self.height1): y1 = y+1 line = '' for x,col in enumerate(cols): if x: # no gutter before the first column line = line+sep cell = '' if y1 >= len(col) else col[y1] line = line+cell+' '*(col[0]-len(cell)) page.append(line) page[0] = self.prefix1 + page[0] page[-1] = page[-1] + self.suffix1 self.prefix1 = self.prefixx self.suffix1 = self.suffixx self.height1 = self.heightx if self.file is None: self._pages.append(page[:]) return try: for line in page: print(line,file=self.file) if 'flush' in dir(self.file): self.file.flush() except BrokenPipeError: return return def close(self): """Called when output is all done.""" if self._closed: raise IOError('close after close') self._closed = True while self._columns: self._process_page() return def get_properties_dict(self): """Get all options/properties as a dictionary.""" return dict(self.get_properties_list) get_properties = get_properties_dict get_options = get_properties get_props = get_properties get_opts = get_properties def get_properties_list(self): """Get all options/properties as a list or tuple of items.""" return ( ('bottom', self.bottom), ('call', self.call), ('file', self.file), ('gutter', self.gutter), ('height1', self.height1), ('heightx', self.heightx), ('left', self.left), ('prefix1', self.prefix1), ('prefixx', self.prefixx), ('right', self.right), ('sep', self.sep), ('suffix1', self.suffix1), ('suffixx', self.suffixx), ('top', self.top), ('width', self.width), ) def get_property_names(self): """Get all options/properties as a list or tuple of names.""" return [i[0] for i in self.get_properties_list()] def get_page(self): """Get the next ready formatted page. """\ """This works only when no output file is set.""" if self.file is not None: if self.file is sys.stdout: raise IOError('output is already set to STDOUT') elif self.file is sys.stderr: raise IOError('output is already set to STDERR') raise IOError('output is already set to a file') if self.pages: return self._pages.pop(0) return None get = get_page def next_column(self,**opts): """Output that follows goes to the next column.""" if self._closed: raise IOError('next_column after close') self._columns.append([0]) if self._is_enough_for_a_page(): self._process_page() return def next_page(self,**opts): """Output that follows goes to the next page.""" if self._closed: raise IOError('next_page after close') self._process_page() return def print(self,*args,**opts): """Print a line to a columnized stream.""" # no args: close # one arg: # None : close # False : next column # True : next page # other : str() results # multiple args: str() each and join if self.height1 < 2: raise ValueError('self.height1 < 2') if self.width < 2: raise ValueError('self.width < 2') if opts: self.set_opts(opts) if len(args) < 1: return self.close() if args[0] is None: return self.close() if args[0] is True: return self.next_page() if args[0] is False: return self.next_column() line = '' n = 0 for arg in args: if n: line = line + self.sep*self.gutter n += 1 line = line + str(arg) # we now have a line to add to a column # if there are no columns, start the first one if len(self._columns) < 1: self._columns = [[0]] # if the current column is full, start a new one if len(self._columns[-1])-1 >= self.height1: self._columns.append([0]) # append the line to the current column self._columns[-1].append(line) # update width if self._columns[-1][0] < len(line): self._columns[-1][0] = len(line) # check if page is full if self._is_enough_for_a_page(): self._process_page() return def set_properties(self,*args,**opts): """Set options/properties and return a dict of unrecognized ones.""" names = self.get_property_names() for n in args: opts[n[0]]=n[1] for n in names: if n in opts: v = opts.pop(n) if v != None: setattr(self,n,v) return opts set_options = set_properties set_props = set_properties set_opts = set_properties def main(args): """main""" if len(args) == 3: gutter = 1 width = int(args[1]) height1 = int(args[2]) heightx = height1 elif len(args) == 4: gutter = int(args[1]) width = int(args[2]) height1 = int(args[3]) heightx = height1 elif len(args) == 5: gutter = int(args[1]) width = int(args[2]) height1 = int(args[3]) heightx = int(args[4]) else: print('need 2 tp 4 arguments: [gutter] width [height1] heightx', file=sys.stderr) return 1 c = colpage( gutter=gutter, width=width, height1=height1, heightx=heightx, prefix1=chr(12)+'<prefix1>', prefixx=chr(12)+'<prefixx>', suffix1='<suffix1>', suffixx='<suffixx>', file=sys.stdout ) c.prefix1 = '' c.prefixx = '' c.suffix1 = '' c.suffixx = '' for line in sys.stdin: c.print( line.rstrip() ) c.close() del c return 0 if __name__ == '__main__': try: result=main(sys.argv) sys.stdout.flush() except BrokenPipeError: result=99 except KeyboardInterrupt: print('') result=98 if result is 0 or result is None or result is True: exit(0) if result is 1 or result is False: exit(1) if isinstance(result,str): print(result,file=sys.stderr) exit(2) try: exit(int(result)) except ValueError: print(str(result),file=sys.stderr) exit(3) except TypeError: exit(4) # EOFcompared to other tools it has the ability to tightly fit as many columns as it can per page. i plan to add the ability to add headings, footings, and side pillars on each page. the page width and height is given in character units, as well as the minimum gutter space between columns.