1   
   2   
   3   
   4   
   5   
   6   
   7   
   8   
   9   
  10   
  11   
  12   
  13   
  14   
  15   
  16   
  17   
  18   
  19   
  20  """This module implements a text based render. 
  21   
  22  A renderer is used by plugins to produce formatted output. 
  23  """ 
  24   
  25  try: 
  26      import curses 
  27      curses.setupterm() 
  28  except Exception:   
  29      curses = None 
  30   
  31  import re 
  32  import os 
  33  import subprocess 
  34  import sys 
  35  import tempfile 
  36  import textwrap 
  37   
  38  from rekall import config 
  39  from rekall_lib import registry 
  40  from rekall_lib import utils 
  41   
  42  from rekall.ui import renderer as renderer_module 
  43   
  44   
  45  config.DeclareOption( 
  46      "--pager", default=os.environ.get("PAGER"), group="Interface", 
  47      help="The pager to use when output is larger than a screen full.") 
  48   
  49  config.DeclareOption( 
  50      "--paging_limit", default=None, group="Interface", type="IntParser", 
  51      help="The number of output lines before we invoke the pager.") 
  52   
  53  config.DeclareOption( 
  54      "--colors", default="auto", type="Choices", 
  55      choices=["auto", "yes", "no"], 
  56      group="Interface", help="Color control. If set to auto only output " 
  57      "colors when connected to a terminal.") 
  58   
  59   
  60  HIGHLIGHT_SCHEME = dict( 
  61      important=(u"WHITE", u"RED"), 
  62      good=(u"GREEN", None), 
  63      neutral=(None, None)) 
  64   
  65   
  66  StyleEnum = utils.AttributeDict( 
  67      address="address", 
  68      value="value", 
  69      compact="compact", 
  70      typed="typed",   
  71      full="full", 
  72      cow="cow") 
  73   
  74   
  75   
  76   
  77  FORMAT_SPECIFIER_RE = re.compile(r""" 
  78  (?P<fill>[^{}<>=^#bcdeEfFgGnLorsxX0-9])?  # The fill parameter. This can not be 
  79                                            # a format string or it is ambiguous. 
  80  (?P<align>[<>=^])?     # The alignment. 
  81  (?P<sign>[+\- ])?      # Sign extension. 
  82  (?P<hash>\#)?          # Hash means to preceed the whole thing with 0x. 
  83  (?P<zerofill>0)?       # Should numbers be zero filled. 
  84  (?P<width>\d+)?        # The minimum width. 
  85  (?P<comma>,)? 
  86  (?P<precision>.\d+)?   # Precision 
  87  (?P<type>[bcdeEfFgGnorsxXL%])?  # The format string (Not all are supported). 
  88  """, re.X) 
 117   
 120      """A wrapper around a pager. 
 121   
 122      The pager can be specified by the session. (eg. 
 123      session.SetParameter("pager", 'less') or in an PAGER environment var. 
 124      """ 
 125       
 126      encoding = "utf8" 
 127   
 129          if session == None: 
 130              raise RuntimeError("Session must be set") 
 131   
 132          self.session = session 
 133   
 134           
 135           
 136          self.pager_command = (session.GetParameter("pager") or 
 137                                os.environ.get("PAGER")) 
 138   
 139          if self.pager_command in [None, "-"]: 
 140              raise AttributeError("Pager command must be specified") 
 141   
 142          self.encoding = session.GetParameter("encoding", "UTF-8") 
 143          self.fd = None 
 144          self.paging_limit = self.session.GetParameter("paging_limit") 
 145          self.data = "" 
 146   
 147           
 148           
 149           
 150           
 151           
 152          self.term_fd = term_fd or sys.stdout 
 153          if not self.term_fd.isatty(): 
 154              raise AttributeError("Pager can only work on a tty.") 
 155   
 156          self.colorizer = Colorizer( 
 157              self.term_fd, 
 158              color=self.session.GetParameter("colors"), 
 159              session=session 
 160          ) 
  161   
 164   
 166           
 167          try: 
 168              if self.fd: 
 169                  self.fd.close() 
 170                  os.unlink(self.fd.name) 
 171          except OSError: 
 172              pass 
  173   
 175          if self.fd is not None: 
 176              return self.fd 
 177   
 178           
 179          self.fd = tempfile.NamedTemporaryFile(prefix="rekall", delete=False) 
 180   
 181          return self.fd 
  182   
 184           
 185          data = utils.SmartUnicode(data).encode(self.encoding, "replace") 
 186          if sys.platform == "win32": 
 187              data = data.replace("\n", "\r\n") 
 188   
 189          if self.fd is not None: 
 190               
 191               
 192              self.fd.write(data) 
 193   
 194           
 195          elif self.paging_limit is None: 
 196              self.term_fd.write(data) 
 197              self.term_fd.flush() 
 198   
 199           
 200           
 201          elif len(self.data.splitlines()) < self.paging_limit: 
 202              self.term_fd.write(data) 
 203              self.term_fd.flush() 
 204              self.data += data 
 205   
 206           
 207          else: 
 208              self.term_fd.write( 
 209                  self.colorizer.Render( 
 210                      "Please wait while the rest is paged...", 
 211                      foreground="YELLOW") + "\r\n") 
 212              self.term_fd.flush() 
 213   
 214              fd = self.GetTempFile() 
 215              fd.write(self.data + data) 
  216   
 218          return self.term_fd.isatty() 
  219   
 221          """Wait for the pager to be exited.""" 
 222          if self.fd is None: 
 223              return 
 224   
 225          try: 
 226              self.fd.flush() 
 227          except ValueError: 
 228              pass 
 229   
 230          try: 
 231              args = dict(filename=self.fd.name) 
 232               
 233               
 234              if "%" in self.pager_command: 
 235                  pager_command = self.pager_command % args 
 236              else: 
 237                  pager_command = self.pager_command + " %s" % self.fd.name 
 238   
 239               
 240               
 241              self.fd.close() 
 242   
 243              subprocess.call(pager_command, shell=True) 
 244   
 245           
 246          except KeyboardInterrupt: 
 247              pass 
 248   
 249          finally: 
 250              try: 
 251                   
 252                  os.unlink(self.fd.name) 
 253              except Exception: 
 254                  pass 
   255   
 258      """An object which makes its target colorful.""" 
 259   
 260      COLORS = u"BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE" 
 261      COLOR_MAP = dict([(x, i) for i, x in enumerate(COLORS.split())]) 
 262   
 263      terminal_capable = False 
 264   
 265 -    def __init__(self, stream, color="auto", session=None): 
  266          """Initialize a colorizer. 
 267   
 268          Args: 
 269            stream: The stream to write to. 
 270   
 271            color: If "no" we suppress using colors, even if the output stream 
 272               can support them. 
 273          """ 
 274          if session == None: 
 275              raise RuntimeError("Session must be set") 
 276   
 277          self.session = session 
 278          self.logging = self.session.logging.getChild("colorizer") 
 279   
 280          if stream is None: 
 281              stream = sys.stdout 
 282   
 283           
 284          if curses is None or color == "no": 
 285              self.terminal_capable = False 
 286   
 287          elif color == "yes": 
 288              self.terminal_capable = True 
 289   
 290          elif color == "auto": 
 291              try: 
 292                  if curses and stream.isatty(): 
 293                      curses.setupterm() 
 294                      self.terminal_capable = True 
 295              except AttributeError: 
 296                  pass 
  297   
 298 -    def tparm(self, capabilities, *args): 
  299          """A simplified version of tigetstr without terminal delays.""" 
 300          for capability in capabilities: 
 301              term_string = curses.tigetstr(capability) 
 302              if term_string is not None: 
 303                  term_string = re.sub(r"\$\<[^>]+>", "", term_string) 
 304                  break 
 305   
 306          try: 
 307              return curses.tparm(term_string, *args) 
 308          except Exception as e: 
 309              self.logging.debug("Unable to set tparm: %s" % e) 
 310              return "" 
  311   
 312 -    def Render(self, target, foreground=None, background=None): 
  313          """Decorate the string with the ansii escapes for the color.""" 
 314          if (not self.terminal_capable or 
 315                  foreground not in self.COLOR_MAP or 
 316                  background not in self.COLOR_MAP): 
 317              return utils.SmartUnicode(target) 
 318   
 319          escape_seq = "" 
 320          if background: 
 321              escape_seq += self.tparm( 
 322                  ["setab", "setb"], self.COLOR_MAP[background]) 
 323   
 324          if foreground: 
 325              escape_seq += self.tparm( 
 326                  ["setaf", "setf"], self.COLOR_MAP[foreground]) 
 327   
 328          return (escape_seq + utils.SmartUnicode(target) + 
 329                  self.tparm(["sgr0"])) 
   330   
 331   
 332 -class TextObjectRenderer(renderer_module.ObjectRenderer): 
  333      """Baseclass for all TextRenderer object renderers.""" 
 334   
 335       
 336      renders_type = "object" 
 337      renderers = ["TextRenderer", "WideTextRenderer", "TestRenderer"] 
 338   
 339      __metaclass__ = registry.MetaclassRegistry 
 340      DEFAULT_STYLE = "full" 
 341   
 342      @utils.safe_property 
 343 -    def address_size(self): 
  344          address_size = 14 
 345   
 346           
 347           
 348           
 349          if (self.session.HasParameter("profile_obj") and 
 350                  self.session.profile.metadata("arch") == "I386"): 
 351              address_size = 10 
 352   
 353          return address_size 
  354   
 356          result = "%x" % address 
 357          padding = options.get("padding", " ") 
 358          if padding == "0": 
 359              return ("0x" + "0" * max(0, self.address_size - 2 - len(result)) + 
 360                      result) 
 361   
 362          return padding * max( 
 363              0, self.address_size - 2 - len(result)) + "0x" + result 
  364   
 367          """This should be overloaded to return the header Cell. 
 368   
 369          Note that typically the same ObjectRenderer instance will be used to 
 370          render all Cells in the same column. 
 371   
 372          Args: 
 373            name: The name of the Column. 
 374            options: The options of the column (i.e. the dict which defines the 
 375              column). 
 376   
 377          Return: 
 378            A Cell instance containing the formatted Column header. 
 379          """ 
 380          header_cell = Cell(unicode(name), width=options.get("width", None)) 
 381   
 382          if style == "address" and header_cell.width < self.address_size: 
 383              header_cell.rewrap(width=self.address_size, align="c") 
 384   
 385          self.header_width = max(header_cell.width, len(unicode(name))) 
 386          header_cell.rewrap(align="c", width=self.header_width) 
 387   
 388           
 389          header_cell.append_line("-" * self.header_width) 
 390   
 391          return header_cell 
  392   
 393 -    def render_typed(self, target, **options): 
  394          return Cell(repr(target), **options) 
  395   
 396 -    def render_full(self, target, **options): 
  397          return Cell(utils.SmartUnicode(target), **options) 
  398   
 399 -    def render_address(self, target, width=None, **options): 
  400          if target is None: 
 401              return Cell(width=width) 
 402   
 403          return Cell( 
 404              self.format_address(int(target), **options), 
 405              width=width) 
  406   
 407 -    def render_compact(self, *args, **kwargs): 
  408          return self.render_full(*args, **kwargs) 
  409   
 410 -    def render_value(self, *args, **kwargs): 
  411          return self.render_full(*args, **kwargs) 
  412   
 413 -    def render_row(self, target, style=None, **options): 
  414          """Render the target suitably. 
 415   
 416          The default implementation calls a render_STYLE method based on the 
 417          style keyword arg. 
 418   
 419          Args: 
 420            target: The object to be rendered. 
 421   
 422            style: A value from StyleEnum, specifying how the object should 
 423                be renderered. 
 424   
 425            options: A dict containing rendering options. The options are created 
 426              from the column options, overriden by the row options and finally 
 427              the cell options.  It is ok for an instance to ignore some or all of 
 428              the options. Some options only make sense in certain Renderer 
 429              contexts. 
 430   
 431          Returns: 
 432            A Cell instance containing the rendering of target. 
 433          """ 
 434          if not style: 
 435              style = self.DEFAULT_STYLE 
 436   
 437          method = getattr(self, "render_%s" % style, None) 
 438          if not callable(method): 
 439              raise NotImplementedError( 
 440                  "%s doesn't know how to render style %s." % ( 
 441                      type(self).__name__, style)) 
 442   
 443          cell = method(target, **options) 
 444          if not isinstance(cell, BaseCell): 
 445              raise RuntimeError("Invalid cell renderer.") 
 446   
 447          return cell 
  448   
 449 -    def render_cow(self, *_, **__): 
  450          """Renders Bessy the cow.""" 
 451          cow = ( 
 452              "                                |############          \n" 
 453              "                                |#####  #####          \n" 
 454              "                                |##        ##          \n" 
 455              "              _                 |#####  #####          \n" 
 456              "             / \\_               |############          \n" 
 457              "            /    \\              |                      \n" 
 458              "           /\\/\\  /\\  _          |       /;    ;\\       \n" 
 459              "          /    \\/  \\/ \\         |   __  \\____//        \n" 
 460              "        /\\  .-   `. \\  \\        |  /{_\\_/   `'\\____    \n" 
 461              "       /  `-.__ ^   /\\  \\       |  \\___ (o)  (o)   }   \n" 
 462              "      / _____________________________/          :--'   \n" 
 463              "    ,-,'`@@@@@@@@       @@@@@@         \\_    `__\\      \n" 
 464              "   ;:(  @@@@@@@@@        @@@             \\___(o'o)     \n" 
 465              "   :: )  @@@@          @@@@@@        ,'@@(  `===='     \n" 
 466              "   :: : @@@@@:          @@@@         `@@@:             \n" 
 467              "   :: \\  @@@@@:       @@@@@@@)    (  '@@@'             \n" 
 468              "   :; /\\      /      @@@@@@@@@\\   :@@@@@)              \n" 
 469              "   ::/  )    {_----------------:  :~`,~~;              \n" 
 470              "  ;; `; :   )                  :  / `; ;               \n" 
 471              " ;;;  : :   ;                  :  ;  ; :               \n" 
 472              " `'`  / :  :                   :  :  : :               \n" 
 473              "     )_ \\__;                   :_ ;  \\_\\               \n" 
 474              "     :__\\  \\                   \\  \\  :  \\              \n" 
 475              "         `^'                    `^'  `-^-'             \n") 
 476   
 477          cell = Cell(value=cow, 
 478                      highlights=[(33, 45, u"RED", u"RED"), 
 479                                  (88, 93, u"RED", u"RED"), 
 480                                  (93, 95, u"WHITE", u"WHITE"), 
 481                                  (95, 100, u"RED", u"RED"), 
 482                                  (143, 145, u"RED", u"RED"), 
 483                                  (145, 153, u"WHITE", u"WHITE"), 
 484                                  (153, 155, u"RED", u"RED"), 
 485                                  (198, 203, u"RED", u"RED"), 
 486                                  (203, 205, u"WHITE", u"WHITE"), 
 487                                  (205, 210, u"RED", u"RED"), 
 488                                  (253, 265, u"RED", u"RED")]) 
 489          return cell 
   490   
 493      renders_type = "AttributedString" 
 494   
 496          raise NotImplementedError("This doesn't make any sense.") 
  497   
 501   
  504   
 507      """This renders a Cell object into a Cell object. 
 508   
 509      i.e. it is just a passthrough object renderer for Cell objects. This is 
 510      useful for rendering nested tables. 
 511      """ 
 512      renders_type = "Cell" 
 513   
  516   
 519      """A Cell represents a single entry in a table. 
 520   
 521      Cells always have a fixed number of characters in width and may have 
 522      arbitrary number of characters (lines) for a height. 
 523   
 524      The TextTable consists of an array of Cells: 
 525   
 526      Cell Cell Cell Cell  <----- Headers. 
 527      Cell Cell Cell Cell  <----- Table rows. 
 528   
 529      The ObjectRenderer is responsible for turning an arbitrary object into a 
 530      Cell object. 
 531      """ 
 532   
 533      _width = None 
 534      _height = None 
 535      _align = None 
 536      _lines = None 
 537   
 538       
 539       
 540       
 541      width_explicit = False 
 542   
 543       
 544       
 545      mode = "stretch" 
 546   
 547      __abstract = True 
 548   
 549 -    def __init__(self, align="l", width=None, **_): 
  554   
 556          return iter(self.lines) 
  557   
 560   
 561      @utils.safe_property 
 567   
 568      @utils.safe_property 
 571   
 572      @utils.safe_property 
 575   
 576      @utils.safe_property 
 579   
 584   
 586          raise NotImplementedError("Subclasses must override.") 
  587   
 588 -    def rewrap(self, width=None, align="l", mode="stretch"): 
   599   
 602      """Joins child cells sideways (left to right). 
 603   
 604      This is not a replacement for table output! Joined cells are for use when 
 605      an object renderer needs to display a subtable, or when one needs to pass 
 606      on wrapping information onto the table, and string concatenation in the 
 607      Cell class is insufficient. 
 608      """ 
 609   
 636   
 638          self._height = 0 
 639          self._lines = [] 
 640   
 641           
 642           
 643          contents_width = 0 
 644          for cell in self.cells: 
 645              contents_width += cell.width + len(self.tablesep) 
 646   
 647          contents_width = max(0, contents_width - len(self.tablesep)) 
 648   
 649          if self.width_explicit or self.width is None: 
 650              self._width = max(self.width, contents_width) 
 651          else: 
 652              self._width = self.width 
 653   
 654          adjustment = self._width - contents_width 
 655   
 656           
 657          if adjustment and self.mode == "stretch" and self.cells: 
 658              align = self.align 
 659   
 660              if align == "l": 
 661                  child_cell = self.cells[-1] 
 662                  child_cell.rewrap(width=adjustment + child_cell.width) 
 663              elif align == "r": 
 664                  child_cell = self.cells[0] 
 665                  child_cell.rewrap(width=adjustment + child_cell.width) 
 666              elif align == "c": 
 667                  self.cells[-1].rewrap( 
 668                      width=(adjustment / 2) + self.cells[-1].width + 
 669                      adjustment % 2) 
 670                  self.cells[0].rewrap( 
 671                      width=(adjustment / 2) + self.cells[0].width) 
 672              else: 
 673                  raise ValueError( 
 674                      "Invalid alignment %s for JoinedCell." % align) 
 675   
 676           
 677          for cell in self.cells: 
 678              self._height = max(self.height, cell.height) 
 679   
 680          for line_no in xrange(self.height): 
 681              parts = [] 
 682              for cell in self.cells: 
 683                  try: 
 684                      parts.append(cell.lines[line_no]) 
 685                  except IndexError: 
 686                      parts.append(" " * cell.width) 
 687   
 688              line = self.tablesep.join(parts) 
 689              if self.mode == "margin": 
 690                  if self.align == "l": 
 691                      line += adjustment * " " 
 692                  elif self.align == "r": 
 693                      line = adjustment * " " + line 
 694                  elif self.align == "c": 
 695                      p, r = divmod(adjustment, 2) 
 696                      line = " " * p + line + " " * (p + r) 
 697              self._lines.append(line) 
  698   
 700          return "<JoinedCell align=%s, width=%s, cells=%s>" % ( 
 701              repr(self.align), repr(self.width), repr(self.cells)) 
   702   
 705      """Vertically stack child cells on top of each other. 
 706   
 707      This is not a replacement for table output! Stacked cells should be used 
 708      when one needs to display multiple lines in a single cell, and the text 
 709      paragraph logic in the Cell class is insufficient. (E.g. rendering faux 
 710      graphics, such as QR codes and heatmaps.) 
 711   
 712      Arguments: 
 713      table_align: If True (default) will align child cells as columns. 
 714                   NOTE: With this option, child cells must all be JoinedCell 
 715                   instanes and have exactly the same number of children each. 
 716      """ 
 717   
 728   
 729      @utils.safe_property 
 735   
 736      @utils.safe_property 
 738          if not self.table_align: 
 739              raise AttributeError( 
 740                  "Only works for StackedCells with table_align set to True.") 
 741          first_row = self.cells[0] 
 742          if not isinstance(first_row, JoinedCell): 
 743              raise AttributeError( 
 744                  ("With table_align is set to True, first cell must be a " 
 745                   "JoinedCell")) 
 746          return len(self.cells[0].cells) 
  747   
 749          target_width = 0 
 750          if self.width_explicit: 
 751              target_width = self._width 
 752   
 753          self._width = 0 
 754          self._height = 0 
 755          self._lines = [] 
 756   
 757          column_widths = [] 
 758          if self.table_align: 
 759              for row in self.cells: 
 760                  if len(row.cells) > len(column_widths): 
 761                      column_widths.extend([0] * ( 
 762                          len(row.cells) - len(column_widths))) 
 763   
 764                  for column, cell in enumerate(row.cells): 
 765                      w = column_widths[column] 
 766                      if cell.width > w: 
 767                          column_widths[column] = cell.width 
 768   
 769          lines = [] 
 770          for cell in self.cells: 
 771              for column, width in enumerate(column_widths): 
 772                  try: 
 773                      cell.cells[column].rewrap(width=width) 
 774                  except IndexError: 
 775                       
 776                       
 777                      break 
 778   
 779              if target_width: 
 780                   
 781                  cell.rewrap(align="l", width=target_width, mode="margin") 
 782              else: 
 783                  cell.dirty()   
 784   
 785              lines.extend(cell.lines) 
 786              self._height += cell.height 
 787              self._width = max(self._width, cell.width) 
 788   
 789          self._lines = lines 
  790   
 792          return "<StackedCell align=%s, _width=%s, cells=%s>" % ( 
 793              repr(self.align), repr(self._width), repr(self.cells)) 
   794   
 795   
 796 -class Cell(BaseCell): 
  797      """A cell for text, knows how to wrap, preserve paragraphs and colorize.""" 
 798      _lines = None 
 799   
 800 -    def __init__(self, value="", highlights=None, colorizer=None, 
 801                   padding=0, **kwargs): 
  802          super(Cell, self).__init__(**kwargs) 
 803          self.paragraphs = value.splitlines() 
 804          self.colorizer = colorizer 
 805          self.highlights = highlights or [] 
 806          self.padding = padding or 0 
 807   
 808          if not self._width: 
 809              if self.paragraphs: 
 810                  self._width = max([len(x) for x in self.paragraphs]) 
 811              else: 
 812                  self._width = 1 
 813   
 814          self._width += self.padding 
  815   
 817          adjust = self.width - len(line) - self.padding 
 818   
 819          if self.align == "l": 
 820              return " " * self.padding + line + " " * adjust, 0 
 821          elif self.align == "r": 
 822              return " " * adjust + line + " " * self.padding, adjust 
 823          elif self.align == "c": 
 824              radjust, r = divmod(adjust, 2) 
 825              ladjust = radjust 
 826              radjust += r 
 827   
 828              padding, r = divmod(self.padding, 2) 
 829              radjust += padding 
 830              ladjust += padding 
 831              ladjust += r 
 832   
 833              lpad = " " * ladjust 
 834              rpad = " " * radjust 
 835              return lpad + line + rpad, 0 
 836          else: 
 837              raise ValueError("Invalid cell alignment: %s." % self.align) 
  838   
 840          if not self.colorizer.terminal_capable: 
 841              return line 
 842   
 843          if last_highlight: 
 844              line = last_highlight + line 
 845   
 846          limit = offset + len(line) 
 847          adjust = 0 
 848   
 849          for rule in self.highlights: 
 850              start = rule.get("start") 
 851              end = rule.get("end") 
 852              fg = rule.get("fg") 
 853              bg = rule.get("bg") 
 854              bold = rule.get("bold") 
 855   
 856              if offset <= start <= limit + adjust: 
 857                  escape_seq = "" 
 858                  if fg is not None: 
 859                      if isinstance(fg, basestring): 
 860                          fg = self.colorizer.COLOR_MAP[fg] 
 861   
 862                      escape_seq += self.colorizer.tparm( 
 863                          ["setaf", "setf"], fg) 
 864   
 865                  if bg is not None: 
 866                      if isinstance(bg, basestring): 
 867                          bg = self.colorizer.COLOR_MAP[bg] 
 868   
 869                      escape_seq += self.colorizer.tparm( 
 870                          ["setab", "setb"], bg) 
 871   
 872                  if bold: 
 873                      escape_seq += self.colorizer.tparm(["bold"]) 
 874   
 875                  insert_at = start - offset + adjust 
 876                  line = line[:insert_at] + escape_seq + line[insert_at:] 
 877   
 878                  adjust += len(escape_seq) 
 879                  last_highlight = escape_seq 
 880   
 881              if offset <= end <= limit + adjust: 
 882                  escape_seq = self.colorizer.tparm(["sgr0"]) 
 883   
 884                  insert_at = end - offset + adjust 
 885                  line = line[:insert_at] + escape_seq + line[insert_at:] 
 886   
 887                  adjust += len(escape_seq) 
 888                  last_highlight = None 
 889   
 890           
 891           
 892           
 893          if last_highlight: 
 894              line += self.colorizer.tparm(["sgr0"]) 
 895          return line, last_highlight 
  896   
 898          self._lines = [] 
 899          last_highlight = None 
 900   
 901          normalized_highlights = [] 
 902          for highlight in self.highlights: 
 903              if isinstance(highlight, dict): 
 904                  normalized_highlights.append(highlight) 
 905              else: 
 906                  normalized_highlights.append(dict( 
 907                      start=highlight[0], end=highlight[1], 
 908                      fg=highlight[2], bg=highlight[3])) 
 909   
 910          self.highlights = sorted(normalized_highlights, 
 911                                   key=lambda x: x["start"]) 
 912   
 913          offset = 0 
 914          for paragraph in self.paragraphs: 
 915              for line in textwrap.wrap(paragraph, self.width): 
 916                  line, adjust = self.justify_line(line) 
 917                  offset += adjust 
 918   
 919                  if self.colorizer and self.colorizer.terminal_capable: 
 920                      line, last_highlight = self.highlight_line( 
 921                          line=line, offset=offset, last_highlight=last_highlight) 
 922   
 923                  self._lines.append(line) 
 924   
 925              offset += len(paragraph) 
  926   
 929   
 930 -    def rewrap(self, width=None, align=None, **_): 
  943   
 945          self.paragraphs.append(line) 
 946          self.dirty() 
  947   
 948      @utils.safe_property 
 950          """The number of chars this Cell takes in height.""" 
 951          return len(self.lines) 
  952   
 954          if not self.paragraphs: 
 955              contents = "None" 
 956          elif len(self.paragraphs) == 1: 
 957              contents = repr(self.paragraphs[0]) 
 958          else: 
 959              contents = repr("%s..." % self.paragraphs[0]) 
 960   
 961          return "<Cell value=%s, align=%s, width=%s>" % ( 
 962              contents, repr(self.align), repr(self.width)) 
   963   
 964   
 965 -class TextColumn(object): 
  966      """Implementation for text (mostly CLI) tables.""" 
 967   
 968       
 969      object_renderer = None 
 970   
 971 -    def __init__(self, table=None, renderer=None, session=None, type=None, 
 972                   formatstring=None, **options): 
  973          if session is None: 
 974              raise RuntimeError("A session must be provided.") 
 975   
 976          self.session = session 
 977          self.table = table 
 978          self.renderer = renderer 
 979          self.header_width = 0 
 980   
 981           
 982           
 983           
 984          self.options = ParseFormatSpec(formatstring) if formatstring else {} 
 985           
 986          self.options.update(options) 
 987   
 988           
 989           
 990           
 991           
 992          if type: 
 993              self.object_renderer = self.renderer.get_object_renderer( 
 994                  type=type, target_renderer="TextRenderer", **options) 
  995   
 996 -    def render_header(self): 
  997          """Renders the cell header. 
 998   
 999          Returns a Cell instance for this column header.""" 
1000           
1001          if self.object_renderer: 
1002              header = self.object_renderer.render_header(**self.options) 
1003          else: 
1004               
1005              object_renderer = TextObjectRenderer(self.renderer, self.session) 
1006              header = object_renderer.render_header(**self.options) 
1007   
1008          self.header_width = header.width 
1009   
1010          return header 
 1011   
1012 -    def render_row(self, target, **options): 
 1013          """Renders the current row for the target.""" 
1014           
1015           
1016          merged_opts = self.options.copy() 
1017          merged_opts.update(options) 
1018   
1019          if merged_opts.get("nowrap"): 
1020              merged_opts.pop("width", None) 
1021   
1022          if self.object_renderer is not None: 
1023              object_renderer = self.object_renderer 
1024          else: 
1025              object_renderer = self.table.renderer.get_object_renderer( 
1026                  target=target, type=merged_opts.get("type"), 
1027                  target_renderer="TextRenderer", **options) 
1028   
1029          if target is None: 
1030              result = Cell(width=merged_opts.get("width")) 
1031          else: 
1032              result = object_renderer.render_row(target, **merged_opts) 
1033              if result is None: 
1034                  return 
1035   
1036              result.colorizer = self.renderer.colorizer 
1037   
1038           
1039          if merged_opts.get("nowrap"): 
1040              return result 
1041   
1042          if "width" in self.options or self.header_width > result.width: 
1043               
1044               
1045               
1046              result.rewrap(width=self.header_width, 
1047                            align=merged_opts.get("align", result.align)) 
1048   
1049          return result 
 1050   
1051      @utils.safe_property 
1053          return self.options.get("name") or self.options.get("cname", "") 
  1054   
1055   
1056 -class TextTable(renderer_module.BaseTable): 
 1057      """A table is a collection of columns. 
1058   
1059      This table formats all its cells using proportional text font. 
1060      """ 
1061   
1062      column_class = TextColumn 
1063      deferred_rows = None 
1064   
1065 -    def __init__(self, auto_widths=False, **options): 
 1066          super(TextTable, self).__init__(**options) 
1067   
1068           
1069          self.options.setdefault("tablesep", self.renderer.tablesep) 
1070   
1071           
1072          self.columns = [] 
1073   
1074          for column_specs in self.column_specs: 
1075              column = self.column_class(session=self.session, table=self, 
1076                                         renderer=self.renderer, **column_specs) 
1077              self.columns.append(column) 
1078   
1079           
1080          self.auto_widths = auto_widths 
1081   
1082           
1083          if auto_widths: 
1084              self.deferred_rows = [] 
 1085   
1086 -    def write_row(self, *cells, **kwargs): 
 1087          """Writes a row of the table. 
1088   
1089          Args: 
1090            cells: A list of cell contents. Each cell content is a list of lines 
1091              in the cell. 
1092          """ 
1093          highlight = kwargs.pop("highlight", None) 
1094          foreground, background = HIGHLIGHT_SCHEME.get( 
1095              highlight, (None, None)) 
1096   
1097           
1098          for line in JoinedCell(tablesep=self.options.get("tablesep"), *cells): 
1099              self.renderer.write( 
1100                  self.renderer.colorizer.Render( 
1101                      line, foreground=foreground, background=background) + "\n") 
 1102   
1104          """Returns a Cell formed by joining all the column headers.""" 
1105           
1106          result = [] 
1107          for c in self.columns: 
1108              merged_opts = c.options.copy() 
1109              merged_opts.update(options) 
1110              if not merged_opts.get("hidden"): 
1111                  result.append(c.render_header()) 
1112   
1113          return JoinedCell( 
1114              *result, tablesep=self.options.get("tablesep", " ")) 
 1115   
1116 -    def get_row(self, *row, **options): 
 1117          """Format the row into a single Cell spanning all output columns. 
1118   
1119          Args: 
1120            *row: A list of objects to render in the same order as columns are 
1121               defined. 
1122   
1123          Returns: 
1124            A single Cell object spanning the entire row. 
1125          """ 
1126          result = [] 
1127          for c, x in zip(self.columns, row): 
1128              merged_opts = c.options.copy() 
1129              merged_opts.update(options) 
1130              if not merged_opts.get("hidden"): 
1131                  result.append(c.render_row(x, **options) or Cell("")) 
1132   
1133          return JoinedCell( 
1134              *result, tablesep=self.options.get("tablesep", " ")) 
 1135   
1136 -    def render_row(self, row=None, highlight=None, annotation=False, **options): 
 1137          """Write the row to the output.""" 
1138          if annotation: 
1139              self.renderer.format(*row) 
1140          elif self.deferred_rows is None: 
1141              return self.write_row(self.get_row(*row, **options), 
1142                                    highlight=highlight) 
1143          else: 
1144              self.deferred_rows.append((row, options)) 
 1145   
1147          if self.deferred_rows is not None: 
1148               
1149              if self.auto_widths: 
1150                  total_width = self.renderer.GetColumns() - 10 
1151   
1152                  max_widths = [] 
1153                  for i, column in enumerate(self.columns): 
1154                      length = 1 
1155                      for row in self.deferred_rows: 
1156                          try: 
1157                               
1158                              rendered_lines = column.render_row( 
1159                                  row[0][i], nowrap=1).lines 
1160   
1161   
1162                              if rendered_lines: 
1163                                  rendered_line = rendered_lines[0] 
1164                              else: 
1165                                  rendered_line = "" 
1166   
1167                              length = max(length, len(rendered_line)) 
1168   
1169                          except IndexError: 
1170                              pass 
1171   
1172                      max_widths.append(length) 
1173   
1174                   
1175                   
1176                   
1177                  sum_of_widths = sum(max_widths) 
1178                  for column, max_width in zip(self.columns, max_widths): 
1179                      width = min( 
1180                          max_width * total_width / sum_of_widths, 
1181                          max_width + 1) 
1182   
1183                      width = max(width, len(unicode(column.name))) 
1184                      column.options["width"] = width 
1185   
1186               
1187              if not self.options.get("suppress_headers"): 
1188                  for line in self.render_header(): 
1189                      self.renderer.write(line + "\n") 
1190   
1191              self.session.report_progress("TextRenderer: sorting %(spinner)s") 
1192              for row, options in self.deferred_rows: 
1193                  self.write_row(self.get_row(*row, **options)) 
  1194   
1197      """A wrapper around a file like object which guarantees writes in utf8.""" 
1198   
1199      _isatty = None 
1200   
1201 -    def __init__(self, fd, encoding='utf8'): 
 1204   
1208   
1211   
 1220   
1221   
1222 -class TextRenderer(renderer_module.BaseRenderer): 
 1223      """Renderer for the command line that supports paging, colors and progress. 
1224      """ 
1225      name = "text" 
1226   
1227      tablesep = " " 
1228      paging_limit = None 
1229      progress_fd = None 
1230   
1231      deferred_rows = None 
1232   
1233       
1234      spinner = r"/-\|" 
1235      last_spin = 0 
1236      last_message_len = 0 
1237   
1238      table_class = TextTable 
1239   
1240 -    def __init__(self, tablesep=" ", output=None, mode="a+b", fd=None, 
1241                   **kwargs): 
 1242          super(TextRenderer, self).__init__(**kwargs) 
1243   
1244           
1245          self.output = output 
1246          if self.output: 
1247               
1248               
1249               
1250              fd = open(self.output, mode) 
1251   
1252          if fd == None: 
1253              fd = self.session.fd 
1254   
1255          if fd == None: 
1256              try: 
1257                  fd = Pager(session=self.session) 
1258              except AttributeError: 
1259                  fd = sys.stdout 
1260   
1261           
1262          self.fd = UnicodeWrapper(fd) 
1263          self.tablesep = tablesep 
1264   
1265           
1266          self.data = [] 
1267   
1268           
1269          self.progress_fd = UnicodeWrapper(sys.stdout) 
1270          if not self.progress_fd.isatty(): 
1271              self.progress_fd = None 
1272   
1273          self.colorizer = Colorizer( 
1274              self.fd, 
1275              color=self.session.GetParameter("colors"), 
1276              session=self.session) 
1277          self.logging = self.session.logging.getChild("renderer.text") 
 1278   
1279 -    def section(self, name=None, width=50): 
 1280          if name is None: 
1281              self.write("*" * width + "\n") 
1282          else: 
1283              pad_len = width - len(name) - 2   
1284              padding = "*" * (pad_len / 2)   
1285   
1286              self.write("\n{0} {1} {2}\n".format(padding, name, padding)) 
 1287   
1288 -    def format(self, formatstring, *data): 
 1289          """Parse and interpolate the format string. 
1290   
1291          A format string consists of a string with interpolation markers 
1292          embedded. The syntax for an interpolation marker is 
1293          {pos:opt1=value,opt2=value}, where pos is the position of the data 
1294          element to interpolate, and opt1, opt2 are the options to provide the 
1295          object renderer. 
1296   
1297          For example: 
1298   
1299          renderer.format("Process {0:style=compact}", task) 
1300   
1301          For backwards compatibility we support the following syntaxes: 
1302          {0:#x} equivalent to {0:style=address} 
1303          {1:d} equivalent to {1} 
1304   
1305   
1306          """ 
1307          super(TextRenderer, self).format(formatstring, *data) 
1308   
1309           
1310           
1311          if self.fd is self.progress_fd: 
1312              self.ClearProgress() 
1313   
1314          default_pos = 0 
1315           
1316           
1317          for part in re.split("({.*?})", formatstring): 
1318               
1319              if not part.startswith("{"): 
1320                  self.write(part) 
1321                  continue 
1322   
1323               
1324              options = dict(style="compact") 
1325              position = None 
1326   
1327               
1328               
1329              m = re.match(r"{(\d+):(.+)}", part) 
1330              if m: 
1331                  position = int(m.group(1)) 
1332                  option_string = m.group(2) 
1333   
1334              m = re.match(r"{(\d*)}", part) 
1335              if m: 
1336                  option_string = "" 
1337                  if not m.group(1): 
1338                      position = default_pos 
1339                      default_pos += 1 
1340                  else: 
1341                      position = int(m.group(1)) 
1342   
1343              if position is None: 
1344                  self.logging.error("Unknown format specifier: %s", part) 
1345                  continue 
1346   
1347               
1348               
1349              if option_string in ["#x", "08x", "8x", "addr"]: 
1350                  options["style"] = "address" 
1351                  options["padding"] = "" 
1352   
1353              elif option_string == "addrpad": 
1354                  options["style"] = "address" 
1355                  options["padding"] = "0" 
1356   
1357              elif "=" in option_string: 
1358                  for option_part in option_string.split(","): 
1359                      if "=" in option_part: 
1360                          key, value = option_part.split("=", 1) 
1361                          options[key.strip()] = value.strip() 
1362                      else: 
1363                          options[option_part] = True 
1364              else: 
1365                  options.update(ParseFormatSpec(option_string)) 
1366   
1367               
1368              item = data[position] 
1369   
1370               
1371              obj_renderer = TextObjectRenderer.ForTarget(item, self)( 
1372                  renderer=self, session=self.session) 
1373   
1374              self.write(obj_renderer.render_row(item, **options)) 
 1375   
1376 -    def write(self, data): 
 1378   
1380          super(TextRenderer, self).flush() 
1381          self.data = [] 
1382          self.ClearProgress() 
1383          self.fd.flush() 
 1384   
1386          options["tablesep"] = self.tablesep 
1387          super(TextRenderer, self).table_header(*args, **options) 
1388   
1389           
1390          if (self.table.deferred_rows is not None or 
1391                  self.table.options.get("suppress_headers") or 
1392                  self.table.auto_widths): 
1393              return 
1394   
1395          for line in self.table.render_header(**options): 
1396              self.write(line + "\n") 
 1397   
1398 -    def table_row(self, *args, **kwargs): 
 1399          """Outputs a single row of a table. 
1400   
1401          Text tables support these additional kwargs: 
1402            highlight: Highlights this raw according to the color scheme (e.g. 
1403                       important, good...) 
1404          """ 
1405          super(TextRenderer, self).table_row(*args, **kwargs) 
1406          self.RenderProgress(message=None) 
 1407   
1408 -    def GetColumns(self): 
 1409          if curses: 
1410              return curses.tigetnum('cols') 
1411   
1412          return int(os.environ.get("COLUMNS", 80)) 
 1413   
1414 -    def RenderProgress(self, message=" %(spinner)s", *args, **kwargs): 
 1415          if super(TextRenderer, self).RenderProgress(**kwargs): 
1416              self.last_spin += 1 
1417              if not message: 
1418                  return 
1419   
1420               
1421              if "%(" in message: 
1422                  kwargs["spinner"] = self.spinner[ 
1423                      self.last_spin % len(self.spinner)] 
1424   
1425                  message = message % kwargs 
1426              elif args: 
1427                  format_args = [] 
1428                  for arg in args: 
1429                      if callable(arg): 
1430                          format_args.append(arg()) 
1431                      else: 
1432                          format_args.append(arg) 
1433   
1434                  message = message % tuple(format_args) 
1435   
1436              self.ClearProgress() 
1437   
1438              message = " " + message + "\r" 
1439   
1440               
1441              message = message[:self.GetColumns()] 
1442   
1443              self.last_message_len = len(message) 
1444   
1445              self._RenderProgress(message) 
1446   
1447              return True 
 1448   
1449 -    def _RenderProgress(self, message): 
 1450          """Actually write the progress message. 
1451   
1452          This can be overwritten by renderers to deliver the progress messages 
1453          elsewhere. 
1454          """ 
1455          if self.progress_fd is not None: 
1456              self.progress_fd.write(message) 
1457              self.progress_fd.flush() 
 1458   
1459 -    def ClearProgress(self): 
 1460          """Delete the last progress message.""" 
1461          if self.progress_fd is None: 
1462              return 
1463   
1464           
1465          self.progress_fd.write("\r" + " " * self.last_message_len + "\r") 
1466          self.progress_fd.flush() 
 1467   
1468 -    def open(self, directory=None, filename=None, mode="rb"): 
 1469          if filename is None and directory is None: 
1470              raise IOError("Must provide a filename") 
1471   
1472          if directory is None: 
1473              directory, filename = os.path.split(filename) 
1474   
1475          filename = utils.SmartStr(filename) or "Unknown%s" % self._object_id 
1476   
1477           
1478          filename = re.sub( 
1479              r"[^a-zA-Z0-9_.@{}\[\]\- ]", 
1480              lambda x: "%" + x.group(0).encode("hex"), 
1481              filename) 
1482   
1483          if directory: 
1484              filename = os.path.join(directory, "./", filename) 
1485   
1486          if "w" in mode: 
1487              try: 
1488                  os.makedirs(directory) 
1489              except (OSError, IOError): 
1490                  pass 
1491   
1492          return open(filename, mode) 
  1493   
1496      """A special renderer which makes parsing the output of tables easier.""" 
1497      name = "test" 
1498   
1501   
 1505   
1506   
1507 -class WideTextRenderer(TextRenderer): 
 1508      """A Renderer which explodes tables into wide formatted records.""" 
1509   
1510      name = "wide" 
1511   
1512 -    def __init__(self, **kwargs): 
 1513          super(WideTextRenderer, self).__init__(**kwargs) 
1514   
1515          self.delegate_renderer = TextRenderer(**kwargs) 
 1516   
1517 -    def __enter__(self): 
 1518          self.delegate_renderer.__enter__() 
1519          self.delegate_renderer.table_header([ 
1520              dict(name="Key", width=15), 
1521              dict(name="Value") 
1522          ], suppress_headers=True) 
1523   
1524          return super(WideTextRenderer, self).__enter__() 
 1525   
1526 -    def __exit__(self, exc_type, exc_value, trace): 
 1527          self.delegate_renderer.__exit__(exc_type, exc_value, trace) 
1528          return super(WideTextRenderer, self).__exit__( 
1529              exc_type, exc_value, trace) 
 1530   
1532          options["suppress_headers"] = True 
1533          super(WideTextRenderer, self).table_header(*args, **options) 
 1534   
1535 -    def table_row(self, *row, **options): 
 1536          self.section() 
1537          values = [c.render_row(x) for c, x in zip(self.table.columns, row)] 
1538   
1539          for c, cell in zip(self.table.columns, values): 
1540              column_name = (getattr(c.object_renderer, "name", None) or 
1541                             c.options.get("name")) 
1542   
1543               
1544              if not cell.lines: 
1545                  continue 
1546   
1547              self.delegate_renderer.table_row(column_name, cell, **options) 
  1548   
1551      renders_type = "TreeNode" 
1552   
1553 -    def __init__(self, renderer=None, session=None, **options): 
 1554          self.max_depth = options.pop("max_depth", 10) 
1555          child_spec = options.pop("child", None) 
1556          if child_spec: 
1557              child_type = child_spec.get("type", "object") 
1558              self.child = self.ByName(child_type, renderer)( 
1559                  renderer, session=session, **child_spec) 
1560   
1561              if not self.child: 
1562                  raise AttributeError("Child %s of TreeNode was not found." % 
1563                                       child_type) 
1564          else: 
1565              self.child = None 
1566   
1567          super(TreeNodeObjectRenderer, self).__init__( 
1568              renderer, session=session, **options) 
 1569   
1579   
1580 -    def render_row(self, target, depth=0, child=None, **options): 
  1597   
1600      renders_type = "Divider" 
1601   
1602 -    def __init__(self, renderer=None, session=None, **options): 
 1603          child_spec = options.pop("child", None) 
1604          if child_spec: 
1605              child_type = child_spec.get("type", "object") 
1606              self.child = self.ByName(child_type, renderer)( 
1607                  renderer, session=session, **child_spec) 
1608   
1609              if not self.child: 
1610                  raise AttributeError("Child %s of Divider was not found." % 
1611                                       child_type) 
1612          else: 
1613              self.child = None 
1614   
1615          super(DividerObjectRenderer, self).__init__( 
1616              renderer, session=session, **options) 
 1617   
1619          self.header_width = 0 
1620          return Cell("") 
 1621   
1622 -    def render_row(self, target, child=None, **options): 
  1646