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