1""" 2Display output in a table format 3================================= 4 5.. versionadded:: 2017.7.0 6 7The ``table`` outputter displays a sequence of rows as table. 8 9Example output: 10 11.. code-block:: text 12 13 edge01.bjm01: 14 ---------- 15 comment: 16 ---------- 17 out: 18 ---------- 19 ______________________________________________________________________________ 20 | Active | Interface | Last Move | Mac | Moves | Static | Vlan | 21 ______________________________________________________________________________ 22 | True | ae1.900 | 0.0 | 40:A6:77:5A:50:01 | 0 | False | 111 | 23 ______________________________________________________________________________ 24 | True | ae1.111 | 0.0 | 64:16:8D:32:26:58 | 0 | False | 111 | 25 ______________________________________________________________________________ 26 | True | ae1.111 | 0.0 | 8C:60:4F:73:2D:57 | 0 | False | 111 | 27 ______________________________________________________________________________ 28 | True | ae1.111 | 0.0 | 8C:60:4F:73:2D:7C | 0 | False | 111 | 29 ______________________________________________________________________________ 30 | True | ae1.222 | 0.0 | 8C:60:4F:73:2D:57 | 0 | False | 222 | 31 ______________________________________________________________________________ 32 | True | ae1.222 | 0.0 | F4:0F:1B:76:9D:97 | 0 | False | 222 | 33 ______________________________________________________________________________ 34 result: 35 ---------- 36 37 38CLI Example: 39 40.. code-block:: bash 41 42 salt '*' foo.bar --out=table 43""" 44 45 46import operator 47from functools import reduce # pylint: disable=redefined-builtin 48 49import salt.output 50import salt.utils.color 51import salt.utils.data 52 53__virtualname__ = "table" 54 55 56def __virtual__(): 57 return __virtualname__ 58 59 60class TableDisplay: 61 """ 62 Manage the table display content. 63 """ 64 65 _JUSTIFY_MAP = { 66 "center": str.center, 67 "right": str.rjust, 68 "left": str.ljust, 69 } 70 71 def __init__( 72 self, 73 has_header=True, # if header will be displayed 74 row_delimiter="-", # row delimiter char 75 delim=" | ", # column delimiter 76 justify="center", # text justify 77 separate_rows=True, # display the line separating two consecutive rows 78 prefix="| ", # character to display at the beginning of the row 79 suffix=" |", # character to display at the end of the row 80 width=50, # column max width 81 wrapfunc=None, 82 ): # function wrapper 83 self.__dict__.update( 84 salt.utils.color.get_colors( 85 __opts__.get("color"), __opts__.get("color_theme") 86 ) 87 ) 88 self.strip_colors = __opts__.get("strip_colors", True) 89 90 self.has_header = has_header 91 self.row_delimiter = row_delimiter 92 self.delim = delim 93 self.justify = justify 94 self.separate_rows = separate_rows 95 self.prefix = prefix 96 self.suffix = suffix 97 self.width = width 98 99 if not (wrapfunc and callable(wrapfunc)): 100 self.wrapfunc = self.wrap_onspace 101 else: 102 self.wrapfunc = wrapfunc 103 104 def ustring(self, indent, color, msg, prefix="", suffix="", endc=None): 105 """Build the unicode string to be displayed.""" 106 if endc is None: 107 endc = self.ENDC # pylint: disable=no-member 108 109 indent *= " " 110 fmt = "{0}{1}{2}{3}{4}{5}" 111 112 try: 113 return fmt.format(indent, color, prefix, msg, endc, suffix) 114 except UnicodeDecodeError: 115 return fmt.format( 116 indent, color, prefix, salt.utils.data.decode(msg), endc, suffix 117 ) 118 119 def wrap_onspace(self, text): 120 121 """ 122 When the text inside the column is longer then the width, will split by space and continue on the next line.""" 123 124 def _truncate(line, word): 125 return "{line}{part}{word}".format( 126 line=line, 127 part=" \n"[ 128 ( 129 len(line[line.rfind("\n") + 1 :]) + len(word.split("\n", 1)[0]) 130 >= self.width 131 ) 132 ], 133 word=word, 134 ) 135 136 return reduce(_truncate, text.split(" ")) 137 138 def prepare_rows(self, rows, indent, has_header): 139 140 """Prepare rows content to be displayed.""" 141 142 out = [] 143 144 def row_wrapper(row): 145 new_rows = [self.wrapfunc(item).split("\n") for item in row] 146 rows = [] 147 for item in map(lambda *args: args, *new_rows): 148 if isinstance(item, (tuple, list)): 149 rows.append([substr or "" for substr in item]) 150 else: 151 rows.append([item]) 152 return rows 153 154 logical_rows = [row_wrapper(row) for row in rows] 155 156 columns = map(lambda *args: args, *reduce(operator.add, logical_rows)) 157 158 max_widths = [max([len(str(item)) for item in column]) for column in columns] 159 row_separator = self.row_delimiter * ( 160 len(self.prefix) 161 + len(self.suffix) 162 + sum(max_widths) 163 + len(self.delim) * (len(max_widths) - 1) 164 ) 165 166 justify = self._JUSTIFY_MAP[self.justify.lower()] 167 168 if self.separate_rows: 169 out.append( 170 self.ustring( 171 indent, self.LIGHT_GRAY, row_separator # pylint: disable=no-member 172 ) 173 ) 174 for physical_rows in logical_rows: 175 for row in physical_rows: 176 line = ( 177 self.prefix 178 + self.delim.join( 179 [ 180 justify(str(item), width) 181 for (item, width) in zip(row, max_widths) 182 ] 183 ) 184 + self.suffix 185 ) 186 out.append( 187 self.ustring(indent, self.WHITE, line) # pylint: disable=no-member 188 ) 189 if self.separate_rows or has_header: 190 out.append( 191 self.ustring( 192 indent, 193 self.LIGHT_GRAY, # pylint: disable=no-member 194 row_separator, 195 ) 196 ) 197 has_header = False 198 return out 199 200 def display_rows(self, rows, labels, indent): 201 202 """Prepares row content and displays.""" 203 204 out = [] 205 206 if not rows: 207 return out 208 209 first_row_type = type(rows[0]) 210 # all rows must have the same datatype 211 consistent = True 212 for row in rows[1:]: 213 if type(row) != first_row_type: 214 consistent = False 215 216 if not consistent: 217 return out 218 219 if isinstance(labels, dict): 220 labels_temp = [] 221 for key in sorted(labels): 222 labels_temp.append(labels[key]) 223 labels = labels_temp 224 225 if first_row_type is dict: # and all the others 226 temp_rows = [] 227 if not labels: 228 labels = [ 229 str(label).replace("_", " ").title() for label in sorted(rows[0]) 230 ] 231 for row in rows: 232 temp_row = [] 233 for key in sorted(row): 234 temp_row.append(str(row[key])) 235 temp_rows.append(temp_row) 236 rows = temp_rows 237 elif isinstance(rows[0], str): 238 rows = [ 239 [row] for row in rows 240 ] # encapsulate each row in a single-element list 241 242 labels_and_rows = [labels] + rows if labels else rows 243 has_header = self.has_header and labels 244 245 return self.prepare_rows(labels_and_rows, indent + 4, has_header) 246 247 def display(self, ret, indent, out, rows_key=None, labels_key=None): 248 249 """Display table(s).""" 250 251 rows = [] 252 labels = None 253 254 if isinstance(ret, dict): 255 if not rows_key or (rows_key and rows_key in list(ret.keys())): 256 # either not looking for a specific key 257 # either looking and found in the current root 258 for key in sorted(ret): 259 if rows_key and key != rows_key: 260 continue # if searching specifics, ignore anything else 261 val = ret[key] 262 if not rows_key: 263 out.append( 264 self.ustring( 265 indent, 266 self.DARK_GRAY, # pylint: disable=no-member 267 key, 268 suffix=":", 269 ) 270 ) 271 out.append( 272 self.ustring( 273 indent, 274 self.DARK_GRAY, # pylint: disable=no-member 275 "----------", 276 ) 277 ) 278 if isinstance(val, (list, tuple)): 279 rows = val 280 if labels_key: 281 # at the same depth 282 labels = ret.get(labels_key) # if any 283 out.extend(self.display_rows(rows, labels, indent)) 284 else: 285 self.display( 286 val, 287 indent + 4, 288 out, 289 rows_key=rows_key, 290 labels_key=labels_key, 291 ) 292 elif rows_key: 293 # dig deeper 294 for key in sorted(ret): 295 val = ret[key] 296 self.display( 297 val, indent, out, rows_key=rows_key, labels_key=labels_key 298 ) # same indent 299 elif isinstance(ret, (list, tuple)): 300 if not rows_key: 301 rows = ret 302 out.extend(self.display_rows(rows, labels, indent)) 303 304 return out 305 306 307def output(ret, **kwargs): 308 """ 309 Display the output as table. 310 311 Args: 312 313 * nested_indent: integer, specify the left alignment. 314 * has_header: boolean specifying if header should be displayed. Default: True. 315 * row_delimiter: character to separate rows. Default: ``_``. 316 * delim: character to separate columns. Default: ``" | "``. 317 * justify: text alignment. Default: ``center``. 318 * separate_rows: boolean specifying if row separator will be displayed between consecutive rows. Default: True. 319 * prefix: character at the beginning of the row. Default: ``"| "``. 320 * suffix: character at the end of the row. Default: ``" |"``. 321 * width: column max width. Default: ``50``. 322 * rows_key: display the rows under a specific key. 323 * labels_key: use the labels under a certain key. Otherwise will try to use the dictionary keys (if any). 324 * title: display title when only one table is selected (using the ``rows_key`` argument). 325 """ 326 327 # to facilitate re-use 328 if "opts" in kwargs: 329 global __opts__ # pylint: disable=W0601 330 __opts__ = kwargs.pop("opts") 331 332 # Prefer kwargs before opts 333 base_indent = kwargs.get("nested_indent", 0) or __opts__.get( 334 "out.table.nested_indent", 0 335 ) 336 rows_key = kwargs.get("rows_key") or __opts__.get("out.table.rows_key") 337 labels_key = kwargs.get("labels_key") or __opts__.get("out.table.labels_key") 338 title = kwargs.get("title") or __opts__.get("out.table.title") 339 340 class_kvargs = {} 341 argks = ( 342 "has_header", 343 "row_delimiter", 344 "delim", 345 "justify", 346 "separate_rows", 347 "prefix", 348 "suffix", 349 "width", 350 ) 351 352 for argk in argks: 353 argv = kwargs.get(argk) or __opts__.get("out.table.{key}".format(key=argk)) 354 if argv is not None: 355 class_kvargs[argk] = argv 356 357 table = TableDisplay(**class_kvargs) 358 359 out = [] 360 if title and rows_key: 361 out.append( 362 table.ustring( 363 base_indent, 364 title, 365 table.WHITE, # pylint: disable=no-member 366 suffix="\n", 367 ) 368 ) 369 370 return "\n".join( 371 table.display( 372 salt.utils.data.decode(ret), 373 base_indent, 374 out, 375 rows_key=rows_key, 376 labels_key=labels_key, 377 ) 378 ) 379