/* * table.c - functions handling the data at the table level * * Copyright (C) 2010-2014 Karel Zak * Copyright (C) 2016 Igor Gnatenko * * This file may be redistributed under the terms of the * GNU Lesser General Public License. */ /** * SECTION: table_print * @title: Table print * @short_description: output functions * * Table output API. */ #include #include #include #include #include #include "mbsalign.h" #include "carefulputc.h" #include "smartcolsP.h" #define colsep(tb) ((tb)->colsep ? (tb)->colsep : " ") #define linesep(tb) ((tb)->linesep ? (tb)->linesep : "\n") /* Fallback for symbols * * Note that by default library define all the symbols, but in case user does * not define all symbols or if we extended the symbols struct then we need * fallback to be more robust and backwardly compatible. */ #define titlepadding_symbol(tb) ((tb)->symbols->title_padding ? (tb)->symbols->title_padding : " ") #define branch_symbol(tb) ((tb)->symbols->branch ? (tb)->symbols->branch : "|-") #define vertical_symbol(tb) ((tb)->symbols->vert ? (tb)->symbols->vert : "| ") #define right_symbol(tb) ((tb)->symbols->right ? (tb)->symbols->right : "`-") #define cellpadding_symbol(tb) ((tb)->padding_debug ? "." : \ ((tb)->symbols->cell_padding ? (tb)->symbols->cell_padding: " ")) #define want_repeat_header(tb) (!(tb)->header_repeat || (tb)->header_next <= (tb)->termlines_used) /* This is private struct to work with output data */ struct libscols_buffer { char *begin; /* begin of the buffer */ char *cur; /* current end of the buffer */ char *encdata; /* encoded buffer mbs_safe_encode() */ size_t bufsz; /* size of the buffer */ size_t art_idx; /* begin of the tree ascii art or zero */ }; static struct libscols_buffer *new_buffer(size_t sz) { struct libscols_buffer *buf = malloc(sz + sizeof(struct libscols_buffer)); if (!buf) return NULL; buf->cur = buf->begin = ((char *) buf) + sizeof(struct libscols_buffer); buf->encdata = NULL; buf->bufsz = sz; DBG(BUFF, ul_debugobj(buf, "alloc (size=%zu)", sz)); return buf; } static void free_buffer(struct libscols_buffer *buf) { if (!buf) return; DBG(BUFF, ul_debugobj(buf, "dealloc")); free(buf->encdata); free(buf); } static int buffer_reset_data(struct libscols_buffer *buf) { if (!buf) return -EINVAL; /*DBG(BUFF, ul_debugobj(buf, "reset data"));*/ buf->begin[0] = '\0'; buf->cur = buf->begin; buf->art_idx = 0; return 0; } static int buffer_append_data(struct libscols_buffer *buf, const char *str) { size_t maxsz, sz; if (!buf) return -EINVAL; if (!str || !*str) return 0; sz = strlen(str); maxsz = buf->bufsz - (buf->cur - buf->begin); if (maxsz <= sz) return -EINVAL; memcpy(buf->cur, str, sz + 1); buf->cur += sz; return 0; } static int buffer_set_data(struct libscols_buffer *buf, const char *str) { int rc = buffer_reset_data(buf); return rc ? rc : buffer_append_data(buf, str); } /* save the current buffer position to art_idx */ static void buffer_set_art_index(struct libscols_buffer *buf) { if (buf) { buf->art_idx = buf->cur - buf->begin; /*DBG(BUFF, ul_debugobj(buf, "art index: %zu", buf->art_idx));*/ } } static char *buffer_get_data(struct libscols_buffer *buf) { return buf ? buf->begin : NULL; } /* encode data by mbs_safe_encode() to avoid control and non-printable chars */ static char *buffer_get_safe_data(struct libscols_table *tb, struct libscols_buffer *buf, size_t *cells, const char *safechars) { char *data = buffer_get_data(buf); char *res = NULL; if (!data) goto nothing; if (!buf->encdata) { buf->encdata = malloc(mbs_safe_encode_size(buf->bufsz) + 1); if (!buf->encdata) goto nothing; } if (tb->no_encode) { *cells = mbs_safe_width(data); strcpy(buf->encdata, data); res = buf->encdata; } else { res = mbs_safe_encode_to_buffer(data, cells, buf->encdata, safechars); } if (!res || !*cells || *cells == (size_t) -1) goto nothing; return res; nothing: *cells = 0; return NULL; } /* returns size in bytes of the ascii art (according to art_idx) in safe encoding */ static size_t buffer_get_safe_art_size(struct libscols_buffer *buf) { char *data = buffer_get_data(buf); size_t bytes = 0; if (!data || !buf->art_idx) return 0; mbs_safe_nwidth(data, buf->art_idx, &bytes); return bytes; } /* returns pointer to the end of used data */ static int line_ascii_art_to_buffer(struct libscols_table *tb, struct libscols_line *ln, struct libscols_buffer *buf) { const char *art; int rc; assert(ln); assert(buf); if (!ln->parent) return 0; rc = line_ascii_art_to_buffer(tb, ln->parent, buf); if (rc) return rc; if (list_entry_is_last(&ln->ln_children, &ln->parent->ln_branch)) art = " "; else art = vertical_symbol(tb); return buffer_append_data(buf, art); } static int is_last_column(struct libscols_column *cl) { int rc = list_entry_is_last(&cl->cl_columns, &cl->table->tb_columns); struct libscols_column *next; if (rc) return 1; next = list_entry(cl->cl_columns.next, struct libscols_column, cl_columns); if (next && scols_column_is_hidden(next) && is_last_column(next)) return 1; return 0; } static int has_pending_data(struct libscols_table *tb) { struct libscols_column *cl; struct libscols_iter itr; scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (scols_table_next_column(tb, &itr, &cl) == 0) { if (scols_column_is_hidden(cl)) continue; if (cl->pending_data) return 1; } return 0; } /* print padding or ASCII-art instead of data of @cl */ static void print_empty_cell(struct libscols_table *tb, struct libscols_column *cl, struct libscols_line *ln, /* optional */ size_t bufsz) { size_t len_pad = 0; /* in screen cells as opposed to bytes */ /* generate tree ASCII-art rather than padding */ if (ln && scols_column_is_tree(cl)) { if (!ln->parent) { /* only print symbols->vert if followed by child */ if (!list_empty(&ln->ln_branch)) { fputs(vertical_symbol(tb), tb->out); len_pad = mbs_safe_width(vertical_symbol(tb)); } } else { /* use the same draw function as though we were intending to draw an L-shape */ struct libscols_buffer *art = new_buffer(bufsz); char *data; if (art) { /* whatever the rc, len_pad will be sensible */ line_ascii_art_to_buffer(tb, ln, art); if (!list_empty(&ln->ln_branch) && has_pending_data(tb)) buffer_append_data(art, vertical_symbol(tb)); data = buffer_get_safe_data(tb, art, &len_pad, NULL); if (data && len_pad) fputs(data, tb->out); free_buffer(art); } } } if (is_last_column(cl)) return; /* fill rest of cell with space */ for(; len_pad < cl->width; ++len_pad) fputs(cellpadding_symbol(tb), tb->out); fputs(colsep(tb), tb->out); } static const char *get_cell_color(struct libscols_table *tb, struct libscols_column *cl, struct libscols_line *ln, /* optional */ struct libscols_cell *ce) /* optional */ { const char *color = NULL; if (tb && tb->colors_wanted) { if (ce) color = ce->color; if (ln && !color) color = ln->color; if (!color) color = cl->color; } return color; } /* Fill the start of a line with padding (or with tree ascii-art). * * This is necessary after a long non-truncated column, as this requires the * next column to be printed on the next line. For example (see 'DDD'): * * aaa bbb ccc ddd eee * AAA BBB CCCCCCC * DDD EEE * ^^^^^^^^^^^^ * new line padding */ static void print_newline_padding(struct libscols_table *tb, struct libscols_column *cl, struct libscols_line *ln, /* optional */ size_t bufsz) { size_t i; assert(tb); assert(cl); fputs(linesep(tb), tb->out); /* line break */ tb->termlines_used++; /* fill cells after line break */ for (i = 0; i <= (size_t) cl->seqnum; i++) print_empty_cell(tb, scols_table_get_column(tb, i), ln, bufsz); } /* * Pending data * * The first line in the multi-line cells (columns with SCOLS_FL_WRAP flag) is * printed as usually and output is truncated to match column width. * * The rest of the long text is printed on next extra line(s). The extra lines * don't exist in the table (not represented by libscols_line). The data for * the extra lines are stored in libscols_column->pending_data_buf and the * function print_line() adds extra lines until the buffer is not empty in all * columns. */ /* set data that will be printed by extra lines */ static int set_pending_data(struct libscols_column *cl, const char *data, size_t sz) { char *p = NULL; if (data && *data) { DBG(COL, ul_debugobj(cl, "setting pending data")); assert(sz); p = strdup(data); if (!p) return -ENOMEM; } free(cl->pending_data_buf); cl->pending_data_buf = p; cl->pending_data_sz = sz; cl->pending_data = cl->pending_data_buf; return 0; } /* the next extra line has been printed, move pending data cursor */ static int step_pending_data(struct libscols_column *cl, size_t bytes) { DBG(COL, ul_debugobj(cl, "step pending data %zu -= %zu", cl->pending_data_sz, bytes)); if (bytes >= cl->pending_data_sz) return set_pending_data(cl, NULL, 0); cl->pending_data += bytes; cl->pending_data_sz -= bytes; return 0; } /* print next pending data for the column @cl */ static int print_pending_data( struct libscols_table *tb, struct libscols_column *cl, struct libscols_line *ln, /* optional */ struct libscols_cell *ce) { const char *color = get_cell_color(tb, cl, ln, ce); size_t width = cl->width, bytes; size_t len = width, i; char *data; char *nextchunk = NULL; if (!cl->pending_data) return 0; if (!width) return -EINVAL; DBG(COL, ul_debugobj(cl, "printing pending data")); data = strdup(cl->pending_data); if (!data) goto err; if (scols_column_is_customwrap(cl) && (nextchunk = cl->wrap_nextchunk(cl, data, cl->wrapfunc_data))) { bytes = nextchunk - data; len = mbs_safe_nwidth(data, bytes, NULL); } else bytes = mbs_truncate(data, &len); if (bytes == (size_t) -1) goto err; if (bytes) step_pending_data(cl, bytes); if (color) fputs(color, tb->out); fputs(data, tb->out); if (color) fputs(UL_COLOR_RESET, tb->out); free(data); if (is_last_column(cl)) return 0; for (i = len; i < width; i++) fputs(cellpadding_symbol(tb), tb->out); /* padding */ fputs(colsep(tb), tb->out); /* columns separator */ return 0; err: free(data); return -errno; } static int print_data(struct libscols_table *tb, struct libscols_column *cl, struct libscols_line *ln, /* optional */ struct libscols_cell *ce, /* optional */ struct libscols_buffer *buf) { size_t len = 0, i, width, bytes; const char *color = NULL; char *data, *nextchunk; int is_last; assert(tb); assert(cl); data = buffer_get_data(buf); if (!data) data = ""; is_last = is_last_column(cl); switch (tb->format) { case SCOLS_FMT_RAW: fputs_nonblank(data, tb->out); if (!is_last) fputs(colsep(tb), tb->out); return 0; case SCOLS_FMT_EXPORT: fprintf(tb->out, "%s=", scols_cell_get_data(&cl->header)); fputs_quoted(data, tb->out); if (!is_last) fputs(colsep(tb), tb->out); return 0; case SCOLS_FMT_JSON: fputs_quoted_json_lower(scols_cell_get_data(&cl->header), tb->out); fputs(":", tb->out); if (!*data) fputs("null", tb->out); else switch (cl->json_type) { case SCOLS_JSON_STRING: fputs_quoted_json(data, tb->out); break; case SCOLS_JSON_NUMBER: fputs(data, tb->out); break; case SCOLS_JSON_BOOLEAN: fputs(!*data ? "false" : *data == '0' ? "false" : *data == 'N' || *data == 'n' ? "false" : "true", tb->out); break; } if (!is_last) fputs(", ", tb->out); return 0; case SCOLS_FMT_HUMAN: break; /* continue below */ } color = get_cell_color(tb, cl, ln, ce); /* Encode. Note that 'len' and 'width' are number of cells, not bytes. */ data = buffer_get_safe_data(tb, buf, &len, scols_column_get_safechars(cl)); if (!data) data = ""; bytes = strlen(data); width = cl->width; /* custom multi-line cell based */ if (*data && scols_column_is_customwrap(cl) && (nextchunk = cl->wrap_nextchunk(cl, data, cl->wrapfunc_data))) { set_pending_data(cl, nextchunk, bytes - (nextchunk - data)); bytes = nextchunk - data; len = mbs_safe_nwidth(data, bytes, NULL); } if (is_last && len < width && !scols_table_is_maxout(tb) && !scols_column_is_right(cl)) width = len; /* truncate data */ if (len > width && scols_column_is_trunc(cl)) { len = width; bytes = mbs_truncate(data, &len); /* updates 'len' */ } /* standard multi-line cell */ if (len > width && scols_column_is_wrap(cl) && !scols_column_is_customwrap(cl)) { set_pending_data(cl, data, bytes); len = width; bytes = mbs_truncate(data, &len); if (bytes != (size_t) -1 && bytes > 0) step_pending_data(cl, bytes); } if (bytes == (size_t) -1) { bytes = len = 0; data = NULL; } if (data) { if (scols_column_is_right(cl)) { if (color) fputs(color, tb->out); for (i = len; i < width; i++) fputs(cellpadding_symbol(tb), tb->out); fputs(data, tb->out); if (color) fputs(UL_COLOR_RESET, tb->out); len = width; } else if (color) { char *p = data; size_t art = buffer_get_safe_art_size(buf); /* we don't want to colorize tree ascii art */ if (scols_column_is_tree(cl) && art && art < bytes) { fwrite(p, 1, art, tb->out); p += art; } fputs(color, tb->out); fputs(p, tb->out); fputs(UL_COLOR_RESET, tb->out); } else fputs(data, tb->out); } for (i = len; i < width; i++) fputs(cellpadding_symbol(tb), tb->out); /* padding */ if (is_last) return 0; if (len > width && !scols_column_is_trunc(cl)) print_newline_padding(tb, cl, ln, buf->bufsz); /* next column starts on next line */ else fputs(colsep(tb), tb->out); /* columns separator */ return 0; } static int cell_to_buffer(struct libscols_table *tb, struct libscols_line *ln, struct libscols_column *cl, struct libscols_buffer *buf) { const char *data; struct libscols_cell *ce; int rc = 0; assert(tb); assert(ln); assert(cl); assert(buf); assert(cl->seqnum <= tb->ncols); buffer_reset_data(buf); ce = scols_line_get_cell(ln, cl->seqnum); data = ce ? scols_cell_get_data(ce) : NULL; if (!data) return 0; if (!scols_column_is_tree(cl)) return buffer_set_data(buf, data); /* * Tree stuff */ if (ln->parent && !scols_table_is_json(tb)) { rc = line_ascii_art_to_buffer(tb, ln->parent, buf); if (!rc && list_entry_is_last(&ln->ln_children, &ln->parent->ln_branch)) rc = buffer_append_data(buf, right_symbol(tb)); else if (!rc) rc = buffer_append_data(buf, branch_symbol(tb)); if (!rc) buffer_set_art_index(buf); } if (!rc) rc = buffer_append_data(buf, data); return rc; } static void fput_indent(struct libscols_table *tb) { int i; for (i = 0; i <= tb->indent; i++) fputs(" ", tb->out); } static void fput_table_open(struct libscols_table *tb) { tb->indent = 0; if (scols_table_is_json(tb)) { fputc('{', tb->out); fputs(linesep(tb), tb->out); fput_indent(tb); fputs_quoted(tb->name, tb->out); fputs(": [", tb->out); fputs(linesep(tb), tb->out); tb->indent++; tb->indent_last_sep = 1; } } static void fput_table_close(struct libscols_table *tb) { tb->indent--; if (scols_table_is_json(tb)) { fput_indent(tb); fputc(']', tb->out); tb->indent--; fputs(linesep(tb), tb->out); fputc('}', tb->out); tb->indent_last_sep = 1; } } static void fput_children_open(struct libscols_table *tb) { if (scols_table_is_json(tb)) { fputc(',', tb->out); fputs(linesep(tb), tb->out); fput_indent(tb); fputs("\"children\": [", tb->out); } /* between parent and child is separator */ fputs(linesep(tb), tb->out); tb->indent_last_sep = 1; tb->indent++; tb->termlines_used++; } static void fput_children_close(struct libscols_table *tb) { tb->indent--; if (scols_table_is_json(tb)) { fput_indent(tb); fputc(']', tb->out); fputs(linesep(tb), tb->out); tb->indent_last_sep = 1; } } static void fput_line_open(struct libscols_table *tb) { if (scols_table_is_json(tb)) { fput_indent(tb); fputc('{', tb->out); tb->indent_last_sep = 0; } tb->indent++; } static void fput_line_close(struct libscols_table *tb, int last, int last_in_table) { tb->indent--; if (scols_table_is_json(tb)) { if (tb->indent_last_sep) fput_indent(tb); fputs(last ? "}" : "},", tb->out); if (!tb->no_linesep) fputs(linesep(tb), tb->out); } else if (tb->no_linesep == 0 && last_in_table == 0) { fputs(linesep(tb), tb->out); tb->termlines_used++; } tb->indent_last_sep = 1; } /* * Prints data. Data can be printed in more formats (raw, NAME=xxx pairs), and * control and non-printable characters can be encoded in the \x?? encoding. */ static int print_line(struct libscols_table *tb, struct libscols_line *ln, struct libscols_buffer *buf) { int rc = 0, pending = 0; struct libscols_column *cl; struct libscols_iter itr; assert(ln); DBG(TAB, ul_debugobj(tb, "printing line")); /* regular line */ scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (rc == 0 && scols_table_next_column(tb, &itr, &cl) == 0) { if (scols_column_is_hidden(cl)) continue; rc = cell_to_buffer(tb, ln, cl, buf); if (rc == 0) rc = print_data(tb, cl, ln, scols_line_get_cell(ln, cl->seqnum), buf); if (rc == 0 && cl->pending_data) pending = 1; } /* extra lines of the multi-line cells */ while (rc == 0 && pending) { pending = 0; fputs(linesep(tb), tb->out); tb->termlines_used++; scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (rc == 0 && scols_table_next_column(tb, &itr, &cl) == 0) { if (scols_column_is_hidden(cl)) continue; if (cl->pending_data) { rc = print_pending_data(tb, cl, ln, scols_line_get_cell(ln, cl->seqnum)); if (rc == 0 && cl->pending_data) pending = 1; } else print_empty_cell(tb, cl, ln, buf->bufsz); } } return 0; } static int print_title(struct libscols_table *tb) { int rc, color = 0; mbs_align_t align; size_t width, len = 0, bufsz, titlesz; char *title = NULL, *buf = NULL; assert(tb); if (!tb->title.data) return 0; DBG(TAB, ul_debugobj(tb, "printing title")); /* encode data */ if (tb->no_encode) { len = bufsz = strlen(tb->title.data) + 1; buf = strdup(tb->title.data); if (!buf) { rc = -ENOMEM; goto done; } } else { bufsz = mbs_safe_encode_size(strlen(tb->title.data)) + 1; if (bufsz == 1) { DBG(TAB, ul_debugobj(tb, "title is empty string -- ignore")); return 0; } buf = malloc(bufsz); if (!buf) { rc = -ENOMEM; goto done; } if (!mbs_safe_encode_to_buffer(tb->title.data, &len, buf, NULL) || !len || len == (size_t) -1) { rc = -EINVAL; goto done; } } /* truncate and align */ width = tb->is_term ? tb->termwidth : 80; titlesz = width + bufsz; title = malloc(titlesz); if (!title) { rc = -EINVAL; goto done; } switch (scols_cell_get_alignment(&tb->title)) { case SCOLS_CELL_FL_RIGHT: align = MBS_ALIGN_RIGHT; break; case SCOLS_CELL_FL_CENTER: align = MBS_ALIGN_CENTER; break; case SCOLS_CELL_FL_LEFT: default: align = MBS_ALIGN_LEFT; /* * Don't print extra blank chars after the title if on left * (that's same as we use for the last column in the table). */ if (len < width && !scols_table_is_maxout(tb) && isblank(*titlepadding_symbol(tb))) width = len; break; } /* copy from buf to title and align to width with title_padding */ rc = mbsalign_with_padding(buf, title, titlesz, &width, align, 0, (int) *titlepadding_symbol(tb)); if (rc == -1) { rc = -EINVAL; goto done; } if (tb->colors_wanted && tb->title.color) color = 1; if (color) fputs(tb->title.color, tb->out); fputs(title, tb->out); if (color) fputs(UL_COLOR_RESET, tb->out); fputc('\n', tb->out); rc = 0; done: free(buf); free(title); DBG(TAB, ul_debugobj(tb, "printing title done [rc=%d]", rc)); return rc; } static int print_header(struct libscols_table *tb, struct libscols_buffer *buf) { int rc = 0; struct libscols_column *cl; struct libscols_iter itr; assert(tb); if ((tb->header_printed == 1 && tb->header_repeat == 0) || scols_table_is_noheadings(tb) || scols_table_is_export(tb) || scols_table_is_json(tb) || list_empty(&tb->tb_lines)) return 0; DBG(TAB, ul_debugobj(tb, "printing header")); /* set the width according to the size of the data */ scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (rc == 0 && scols_table_next_column(tb, &itr, &cl) == 0) { if (scols_column_is_hidden(cl)) continue; rc = buffer_set_data(buf, scols_cell_get_data(&cl->header)); if (!rc) rc = print_data(tb, cl, NULL, &cl->header, buf); } if (rc == 0) { fputs(linesep(tb), tb->out); tb->termlines_used++; } tb->header_printed = 1; tb->header_next = tb->termlines_used + tb->termheight; if (tb->header_repeat) DBG(TAB, ul_debugobj(tb, "\tnext header: %zu [current=%zu]", tb->header_next, tb->termlines_used)); return rc; } static int print_range( struct libscols_table *tb, struct libscols_buffer *buf, struct libscols_iter *itr, struct libscols_line *end) { int rc = 0; struct libscols_line *ln; assert(tb); DBG(TAB, ul_debugobj(tb, "printing range")); while (rc == 0 && scols_table_next_line(tb, itr, &ln) == 0) { int last = scols_iter_is_last(itr); fput_line_open(tb); rc = print_line(tb, ln, buf); fput_line_close(tb, last, last); if (end && ln == end) break; if (!last && want_repeat_header(tb)) print_header(tb, buf); } return rc; } static int print_table(struct libscols_table *tb, struct libscols_buffer *buf) { struct libscols_iter itr; scols_reset_iter(&itr, SCOLS_ITER_FORWARD); return print_range(tb, buf, &itr, NULL); } static int print_tree_line(struct libscols_table *tb, struct libscols_line *ln, struct libscols_buffer *buf, int last, int last_in_table) { int rc; /* print the line */ fput_line_open(tb); rc = print_line(tb, ln, buf); if (rc) goto done; /* print children */ if (!list_empty(&ln->ln_branch)) { struct list_head *p; fput_children_open(tb); /* print all children */ list_for_each(p, &ln->ln_branch) { struct libscols_line *chld = list_entry(p, struct libscols_line, ln_children); int last_child = p->next == &ln->ln_branch; rc = print_tree_line(tb, chld, buf, last_child, last_in_table && last_child); if (rc) goto done; } fput_children_close(tb); } if (list_empty(&ln->ln_branch) || scols_table_is_json(tb)) fput_line_close(tb, last, last_in_table); done: return rc; } static int print_tree(struct libscols_table *tb, struct libscols_buffer *buf) { int rc = 0; struct libscols_line *ln, *last = NULL; struct libscols_iter itr; assert(tb); DBG(TAB, ul_debugobj(tb, "printing tree")); scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (scols_table_next_line(tb, &itr, &ln) == 0) if (!last || !ln->parent) last = ln; scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (rc == 0 && scols_table_next_line(tb, &itr, &ln) == 0) { if (ln->parent) continue; rc = print_tree_line(tb, ln, buf, ln == last, ln == last); } return rc; } static void dbg_column(struct libscols_table *tb, struct libscols_column *cl) { if (scols_column_is_hidden(cl)) { DBG(COL, ul_debugobj(cl, "%s (hidden) ignored", cl->header.data)); return; } DBG(COL, ul_debugobj(cl, "%15s seq=%zu, width=%zd, " "hint=%d, avg=%zu, max=%zu, min=%zu, " "extreme=%s %s", cl->header.data, cl->seqnum, cl->width, cl->width_hint > 1 ? (int) cl->width_hint : (int) (cl->width_hint * tb->termwidth), cl->width_avg, cl->width_max, cl->width_min, cl->is_extreme ? "yes" : "not", cl->flags & SCOLS_FL_TRUNC ? "trunc" : "")); } static void dbg_columns(struct libscols_table *tb) { struct libscols_iter itr; struct libscols_column *cl; scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (scols_table_next_column(tb, &itr, &cl) == 0) dbg_column(tb, cl); } /* * This function counts column width. * * For the SCOLS_FL_NOEXTREMES columns it is possible to call this function * two times. The first pass counts the width and average width. If the column * contains fields that are too large (a width greater than 2 * average) then * the column is marked as "extreme". In the second pass all extreme fields * are ignored and the column width is counted from non-extreme fields only. */ static int count_column_width(struct libscols_table *tb, struct libscols_column *cl, struct libscols_buffer *buf) { struct libscols_line *ln; struct libscols_iter itr; int count = 0, rc = 0; size_t sum = 0; assert(tb); assert(cl); cl->width = 0; if (!cl->width_min) { if (cl->width_hint < 1 && scols_table_is_maxout(tb) && tb->is_term) { cl->width_min = (size_t) (cl->width_hint * tb->termwidth); if (cl->width_min && !is_last_column(cl)) cl->width_min--; } if (scols_cell_get_data(&cl->header)) { size_t len = mbs_safe_width(scols_cell_get_data(&cl->header)); cl->width_min = max(cl->width_min, len); } if (!cl->width_min) cl->width_min = 1; } scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (scols_table_next_line(tb, &itr, &ln) == 0) { size_t len; char *data; rc = cell_to_buffer(tb, ln, cl, buf); if (rc) goto done; data = buffer_get_data(buf); if (!data) len = 0; else if (scols_column_is_customwrap(cl)) len = cl->wrap_chunksize(cl, data, cl->wrapfunc_data); else len = mbs_safe_width(data); if (len == (size_t) -1) /* ignore broken multibyte strings */ len = 0; cl->width_max = max(len, cl->width_max); if (cl->is_extreme && len > cl->width_avg * 2) continue; else if (scols_column_is_noextremes(cl)) { sum += len; count++; } cl->width = max(len, cl->width); if (scols_column_is_tree(cl)) { size_t treewidth = buffer_get_safe_art_size(buf); cl->width_treeart = max(cl->width_treeart, treewidth); } } if (count && cl->width_avg == 0) { cl->width_avg = sum / count; if (cl->width_max > cl->width_avg * 2) cl->is_extreme = 1; } /* enlarge to minimal width */ if (cl->width < cl->width_min && !scols_column_is_strict_width(cl)) cl->width = cl->width_min; /* use absolute size for large columns */ else if (cl->width_hint >= 1 && cl->width < (size_t) cl->width_hint && cl->width_min < (size_t) cl->width_hint) cl->width = (size_t) cl->width_hint; done: ON_DBG(COL, dbg_column(tb, cl)); return rc; } /* * This is core of the scols_* voodoo... */ static int recount_widths(struct libscols_table *tb, struct libscols_buffer *buf) { struct libscols_column *cl; struct libscols_iter itr; size_t width = 0, width_min = 0; /* output width */ int stage, rc = 0; int extremes = 0; size_t colsepsz; DBG(TAB, ul_debugobj(tb, "recounting widths (termwidth=%zu)", tb->termwidth)); colsepsz = mbs_safe_width(colsep(tb)); /* set basic columns width */ scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (scols_table_next_column(tb, &itr, &cl) == 0) { int is_last; if (scols_column_is_hidden(cl)) continue; rc = count_column_width(tb, cl, buf); if (rc) goto done; is_last = is_last_column(cl); width += cl->width + (is_last ? 0 : colsepsz); /* separator for non-last column */ width_min += cl->width_min + (is_last ? 0 : colsepsz); extremes += cl->is_extreme; } if (!tb->is_term) { DBG(TAB, ul_debugobj(tb, " non-terminal output")); goto done; } /* be paranoid */ if (width_min > tb->termwidth && scols_table_is_maxout(tb)) { DBG(TAB, ul_debugobj(tb, " min width larger than terminal! [width=%zu, term=%zu]", width_min, tb->termwidth)); scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (width_min > tb->termwidth && scols_table_next_column(tb, &itr, &cl) == 0) { if (scols_column_is_hidden(cl)) continue; width_min--; cl->width_min--; } DBG(TAB, ul_debugobj(tb, " min width reduced to %zu", width_min)); } /* reduce columns with extreme fields */ if (width > tb->termwidth && extremes) { DBG(TAB, ul_debugobj(tb, " reduce width (extreme columns)")); scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (scols_table_next_column(tb, &itr, &cl) == 0) { size_t org_width; if (!cl->is_extreme || scols_column_is_hidden(cl)) continue; org_width = cl->width; rc = count_column_width(tb, cl, buf); if (rc) goto done; if (org_width > cl->width) width -= org_width - cl->width; else extremes--; /* hmm... nothing reduced */ } } if (width < tb->termwidth) { if (extremes) { DBG(TAB, ul_debugobj(tb, " enlarge width (extreme columns)")); /* enlarge the first extreme column */ scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (scols_table_next_column(tb, &itr, &cl) == 0) { size_t add; if (!cl->is_extreme || scols_column_is_hidden(cl)) continue; /* this column is too large, ignore? if (cl->width_max - cl->width > (tb->termwidth - width)) continue; */ add = tb->termwidth - width; if (add && cl->width + add > cl->width_max) add = cl->width_max - cl->width; cl->width += add; width += add; if (width == tb->termwidth) break; } } if (width < tb->termwidth && scols_table_is_maxout(tb)) { DBG(TAB, ul_debugobj(tb, " enlarge width (max-out)")); /* try enlarging all columns */ while (width < tb->termwidth) { scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (scols_table_next_column(tb, &itr, &cl) == 0) { if (scols_column_is_hidden(cl)) continue; cl->width++; width++; if (width == tb->termwidth) break; } } } else if (width < tb->termwidth) { /* enlarge the last column */ struct libscols_column *col = list_entry( tb->tb_columns.prev, struct libscols_column, cl_columns); DBG(TAB, ul_debugobj(tb, " enlarge width (last column)")); if (!scols_column_is_right(col) && tb->termwidth - width > 0) { col->width += tb->termwidth - width; width = tb->termwidth; } } } /* bad, we have to reduce output width, this is done in three stages: * * 1) trunc relative with trunc flag if the column width is greater than * expected column width (it means "width_hint * terminal_width"). * * 2) trunc all with trunc flag * * 3) trunc relative without trunc flag * * Note that SCOLS_FL_WRAP (if no custom wrap function is specified) is * interpreted as SCOLS_FL_TRUNC. */ for (stage = 1; width > tb->termwidth && stage <= 3; ) { size_t org_width = width; DBG(TAB, ul_debugobj(tb, " reduce width - #%d stage (current=%zu, wanted=%zu)", stage, width, tb->termwidth)); scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (scols_table_next_column(tb, &itr, &cl) == 0) { int trunc_flag = 0; DBG(TAB, ul_debugobj(cl, " checking %s (width=%zu, treeart=%zu)", cl->header.data, cl->width, cl->width_treeart)); if (scols_column_is_hidden(cl)) continue; if (width <= tb->termwidth) break; /* never truncate if already minimal width */ if (cl->width == cl->width_min) continue; /* never truncate the tree */ if (scols_column_is_tree(cl) && width <= cl->width_treeart) continue; /* nothing to truncate */ if (cl->width == 0 || width == 0) continue; trunc_flag = scols_column_is_trunc(cl) || (scols_column_is_wrap(cl) && !scols_column_is_customwrap(cl)); switch (stage) { /* #1 stage - trunc relative with TRUNC flag */ case 1: if (!trunc_flag) /* ignore: missing flag */ break; if (cl->width_hint <= 0 || cl->width_hint >= 1) /* ignore: no relative */ break; if (cl->width < (size_t) (cl->width_hint * tb->termwidth)) /* ignore: smaller than expected width */ break; DBG(TAB, ul_debugobj(tb, " reducing (relative with flag)")); cl->width--; width--; break; /* #2 stage - trunc all with TRUNC flag */ case 2: if (!trunc_flag) /* ignore: missing flag */ break; DBG(TAB, ul_debugobj(tb, " reducing (all with flag)")); cl->width--; width--; break; /* #3 stage - trunc relative without flag */ case 3: if (cl->width_hint <= 0 || cl->width_hint >= 1) /* ignore: no relative */ break; DBG(TAB, ul_debugobj(tb, " reducing (relative without flag)")); cl->width--; width--; break; } /* hide zero width columns */ if (cl->width == 0) cl->flags |= SCOLS_FL_HIDDEN; } /* the current stage is without effect, go to the next */ if (org_width == width) stage++; } /* ignore last column(s) or force last column to be truncated if * nowrap mode enabled */ if (tb->no_wrap && width > tb->termwidth) { scols_reset_iter(&itr, SCOLS_ITER_BACKWARD); while (scols_table_next_column(tb, &itr, &cl) == 0) { if (scols_column_is_hidden(cl)) continue; if (width <= tb->termwidth) break; if (width - cl->width < tb->termwidth) { size_t r = width - tb->termwidth; cl->flags |= SCOLS_FL_TRUNC; cl->width -= r; width -= r; } else { cl->flags |= SCOLS_FL_HIDDEN; width -= cl->width + colsepsz; } } } done: DBG(TAB, ul_debugobj(tb, " final width: %zu (rc=%d)", width, rc)); ON_DBG(TAB, dbg_columns(tb)); return rc; } static size_t strlen_line(struct libscols_line *ln) { size_t i, sz = 0; assert(ln); for (i = 0; i < ln->ncells; i++) { struct libscols_cell *ce = scols_line_get_cell(ln, i); const char *data = ce ? scols_cell_get_data(ce) : NULL; sz += data ? strlen(data) : 0; } return sz; } static void cleanup_printing(struct libscols_table *tb, struct libscols_buffer *buf) { if (!tb) return; free_buffer(buf); if (tb->priv_symbols) { scols_table_set_symbols(tb, NULL); tb->priv_symbols = 0; } } static int initialize_printing(struct libscols_table *tb, struct libscols_buffer **buf) { size_t bufsz, extra_bufsz = 0; struct libscols_line *ln; struct libscols_iter itr; int rc; DBG(TAB, ul_debugobj(tb, "initialize printing")); *buf = NULL; if (!tb->symbols) { rc = scols_table_set_default_symbols(tb); if (rc) goto err; tb->priv_symbols = 1; } else tb->priv_symbols = 0; if (tb->format == SCOLS_FMT_HUMAN) tb->is_term = tb->termforce == SCOLS_TERMFORCE_NEVER ? 0 : tb->termforce == SCOLS_TERMFORCE_ALWAYS ? 1 : isatty(STDOUT_FILENO); if (tb->is_term) { size_t width = (size_t) scols_table_get_termwidth(tb); if (tb->termreduce > 0 && tb->termreduce < width) { width -= tb->termreduce; scols_table_set_termwidth(tb, width); } bufsz = width; } else bufsz = BUFSIZ; if (!tb->is_term || tb->format != SCOLS_FMT_HUMAN || scols_table_is_tree(tb)) tb->header_repeat = 0; /* * Estimate extra space necessary for tree, JSON or another output * decoration. */ if (scols_table_is_tree(tb)) extra_bufsz += tb->nlines * strlen(vertical_symbol(tb)); switch (tb->format) { case SCOLS_FMT_RAW: extra_bufsz += tb->ncols; /* separator between columns */ break; case SCOLS_FMT_JSON: if (tb->format == SCOLS_FMT_JSON) extra_bufsz += tb->nlines * 3; /* indention */ /* fallthrough */ case SCOLS_FMT_EXPORT: { struct libscols_column *cl; scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (scols_table_next_column(tb, &itr, &cl) == 0) { if (scols_column_is_hidden(cl)) continue; extra_bufsz += strlen(scols_cell_get_data(&cl->header)); /* data */ extra_bufsz += 2; /* separators */ } break; } case SCOLS_FMT_HUMAN: break; } /* * Enlarge buffer if necessary, the buffer should be large enough to * store line data and tree ascii art (or another decoration). */ scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (scols_table_next_line(tb, &itr, &ln) == 0) { size_t sz; sz = strlen_line(ln) + extra_bufsz; if (sz > bufsz) bufsz = sz; } *buf = new_buffer(bufsz + 1); /* data + space for \0 */ if (!*buf) { rc = -ENOMEM; goto err; } if (tb->format == SCOLS_FMT_HUMAN) { rc = recount_widths(tb, *buf); if (rc != 0) goto err; } return 0; err: cleanup_printing(tb, *buf); return rc; } /** * scola_table_print_range: * @tb: table * @start: first printed line or NULL to print from the begin of the table * @end: last printed line or NULL to print all from start. * * If the start is the first line in the table than prints table header too. * The header is printed only once. This does not work for trees. * * Returns: 0, a negative value in case of an error. */ int scols_table_print_range( struct libscols_table *tb, struct libscols_line *start, struct libscols_line *end) { struct libscols_buffer *buf = NULL; struct libscols_iter itr; int rc; if (scols_table_is_tree(tb)) return -EINVAL; DBG(TAB, ul_debugobj(tb, "printing range from API")); rc = initialize_printing(tb, &buf); if (rc) return rc; if (start) { itr.direction = SCOLS_ITER_FORWARD; itr.head = &tb->tb_lines; itr.p = &start->ln_lines; } else scols_reset_iter(&itr, SCOLS_ITER_FORWARD); if (!start || itr.p == tb->tb_lines.next) { rc = print_header(tb, buf); if (rc) goto done; } rc = print_range(tb, buf, &itr, end); done: cleanup_printing(tb, buf); return rc; } /** * scols_table_print_range_to_string: * @tb: table * @start: first printed line or NULL to print from the beginning of the table * @end: last printed line or NULL to print all from start. * @data: pointer to the beginning of a memory area to print to * * The same as scols_table_print_range(), but prints to @data instead of * stream. * * Returns: 0, a negative value in case of an error. */ #ifdef HAVE_OPEN_MEMSTREAM int scols_table_print_range_to_string( struct libscols_table *tb, struct libscols_line *start, struct libscols_line *end, char **data) { FILE *stream, *old_stream; size_t sz; int rc; if (!tb) return -EINVAL; DBG(TAB, ul_debugobj(tb, "printing range to string")); /* create a stream for output */ stream = open_memstream(data, &sz); if (!stream) return -ENOMEM; old_stream = scols_table_get_stream(tb); scols_table_set_stream(tb, stream); rc = scols_table_print_range(tb, start, end); fclose(stream); scols_table_set_stream(tb, old_stream); return rc; } #else int scols_table_print_range_to_string( struct libscols_table *tb __attribute__((__unused__)), struct libscols_line *start __attribute__((__unused__)), struct libscols_line *end __attribute__((__unused__)), char **data __attribute__((__unused__))) { return -ENOSYS; } #endif static int __scols_print_table(struct libscols_table *tb, int *is_empty) { int rc = 0; struct libscols_buffer *buf = NULL; if (!tb) return -EINVAL; DBG(TAB, ul_debugobj(tb, "printing")); if (is_empty) *is_empty = 0; if (list_empty(&tb->tb_columns)) { DBG(TAB, ul_debugobj(tb, "error -- no columns")); return -EINVAL; } if (list_empty(&tb->tb_lines)) { DBG(TAB, ul_debugobj(tb, "ignore -- no lines")); if (is_empty) *is_empty = 1; return 0; } tb->header_printed = 0; rc = initialize_printing(tb, &buf); if (rc) return rc; fput_table_open(tb); if (tb->format == SCOLS_FMT_HUMAN) print_title(tb); rc = print_header(tb, buf); if (rc) goto done; if (scols_table_is_tree(tb)) rc = print_tree(tb, buf); else rc = print_table(tb, buf); fput_table_close(tb); done: cleanup_printing(tb, buf); return rc; } /** * scols_print_table: * @tb: table * * Prints the table to the output stream and terminate by \n. * * Returns: 0, a negative value in case of an error. */ int scols_print_table(struct libscols_table *tb) { int empty = 0; int rc = __scols_print_table(tb, &empty); if (rc == 0 && !empty) fputc('\n', tb->out); return rc; } /** * scols_print_table_to_string: * @tb: table * @data: pointer to the beginning of a memory area to print to * * Prints the table to @data. * * Returns: 0, a negative value in case of an error. */ #ifdef HAVE_OPEN_MEMSTREAM int scols_print_table_to_string(struct libscols_table *tb, char **data) { FILE *stream, *old_stream; size_t sz; int rc; if (!tb) return -EINVAL; DBG(TAB, ul_debugobj(tb, "printing to string")); /* create a stream for output */ stream = open_memstream(data, &sz); if (!stream) return -ENOMEM; old_stream = scols_table_get_stream(tb); scols_table_set_stream(tb, stream); rc = __scols_print_table(tb, NULL); fclose(stream); scols_table_set_stream(tb, old_stream); return rc; } #else int scols_print_table_to_string( struct libscols_table *tb __attribute__((__unused__)), char **data __attribute__((__unused__))) { return -ENOSYS; } #endif