blob: ae8d2da1cc90a7bf51659e66a3fb419af820c580 [file] [log] [blame]
/*
* table.c - functions handling the data at the table level
*
* Copyright (C) 2010-2014 Karel Zak <kzak@redhat.com>
* Copyright (C) 2016 Igor Gnatenko <i.gnatenko.brain@gmail.com>
*
* 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 <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <termios.h>
#include <ctype.h>
#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);
switch (cl->json_type) {
case SCOLS_JSON_STRING:
if (!*data)
fputs("null", tb->out);
else
fputs_quoted_json(data, tb->out);
break;
case SCOLS_JSON_NUMBER:
if (!*data)
fputs("null", tb->out);
else
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, no_header = 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);
} else
no_header = 1;
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;
/* Column without header and data, set minimal size to zero (default is 1) */
if (cl->width_max == 0 && no_header && cl->width_min == 1 && cl->width <= 1)
cl->width = cl->width_min = 0;
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