From: Martin Mares Date: Fri, 30 May 2014 11:53:22 +0000 (+0200) Subject: Imported tableprinter module X-Git-Tag: v6.0~13^2~11 X-Git-Url: http://mj.ucw.cz/gitweb/?a=commitdiff_plain;h=1ac31d623afa6873c1cbe691bf8429c03d289ac2;p=libucw.git Imported tableprinter module Original version was developed in GigaMail repository by Robert Kessl, but we agreed that it should be moved to LibUCW and maintained there. Further cleanup can be expected. --- diff --git a/ucw/Makefile b/ucw/Makefile index 860a647a..527e2beb 100644 --- a/ucw/Makefile +++ b/ucw/Makefile @@ -37,7 +37,8 @@ LIBUCW_MODS= \ resource trans res-fd res-mem res-subpool res-mempool res-eltpool \ daemon daemon-ctrl \ signames \ - opt opt-help opt-conf + opt opt-help opt-conf \ + table LIBUCW_MAIN_INCLUDES= \ lib.h log.h tbf.h threads.h time.h \ @@ -67,7 +68,8 @@ LIBUCW_MAIN_INCLUDES= \ daemon.h \ signames.h \ sighandler.h \ - opt.h + opt.h \ + table.h ifdef CONFIG_UCW_THREADS # Some modules require threading @@ -130,6 +132,7 @@ endif $(o)/ucw/ipaccess-test: $(o)/ucw/ipaccess-test.o $(LIBUCW) $(o)/ucw/trie-test: $(o)/ucw/trie-test.o $(LIBUCW) $(o)/ucw/opt-test: $(o)/ucw/opt-test.o $(LIBUCW) +$(o)/ucw/table-test: $(o)/ucw/table-test.o $(LIBUCW) TESTS+=$(addprefix $(o)/ucw/,regex.test unicode.test hash-test.test mempool.test stkstring.test \ slists.test bbuf.test kmp-test.test getopt.test ff-unicode.test eltpool.test \ @@ -137,7 +140,8 @@ TESTS+=$(addprefix $(o)/ucw/,regex.test unicode.test hash-test.test mempool.test fb-file.test fb-socket.test fb-grow.test fb-pool.test fb-atomic.test fb-limfd.test fb-temp.test \ fb-mem.test fb-buffer.test fb-mmap.test fb-multi.test fb-null.test \ redblack-test.test url.test strtonum-test.test \ - gary.test time.test crc.test signames.test md5.test bitops.test opt.test) + gary.test time.test crc.test signames.test md5.test bitops.test opt.test \ + table.test table-test.test) $(o)/ucw/varint.test: $(o)/ucw/varint-t $(o)/ucw/regex.test: $(o)/ucw/regex-t @@ -169,6 +173,8 @@ $(o)/ucw/crc.test: $(o)/ucw/crc-t $(o)/ucw/signames.test: $(o)/ucw/signames-t $(o)/ucw/md5.test: $(o)/ucw/md5-t $(o)/ucw/opt.test: $(o)/ucw/opt-test +$(o)/ucw/table.test: $(o)/ucw/table-t +$(o)/ucw/table-test.test: $(o)/ucw/table-test ifdef CONFIG_UCW_THREADS TESTS+=$(addprefix $(o)/ucw/,asio.test) diff --git a/ucw/table-test.c b/ucw/table-test.c new file mode 100644 index 00000000..ee019541 --- /dev/null +++ b/ucw/table-test.c @@ -0,0 +1,181 @@ +/* + * Unit tests of table printer + * + * (c) 2014 Robert Kessl + */ + +#include +#include +#include +#include + +enum test_table_cols { + test_col0_str, test_col1_int, test_col2_uint, test_col3_bool, test_col4_double +}; + +static uint test_column_order[] = { test_col3_bool, test_col4_double, test_col2_uint,test_col1_int, test_col0_str }; + +static struct table test_tbl = { + TBL_COLUMNS { + TBL_COL_STR(test, col0_str, 20), + TBL_COL_INT(test, col1_int, 8), + TBL_COL_UINT(test, col2_uint, 9), + TBL_COL_BOOL(test, col3_bool, 9), + TBL_COL_DOUBLE(test, col4_double, 11, 2), + TBL_COL_END + }, + TBL_COL_ORDER(test_column_order), + TBL_OUTPUT_HUMAN_READABLE, + TBL_COL_DELIMITER("\t"), +}; + +enum test_default_order_cols { + test_default_order_col0_int, test_default_order_col1_int, test_default_order_col2_int +}; + +static struct table test_default_order_tbl = { + TBL_COLUMNS { + TBL_COL_INT(test_default_order, col0_int, 8), + TBL_COL_INT(test_default_order, col1_int, 9), + TBL_COL_INT(test_default_order, col2_int, 9), + TBL_COL_END + }, + TBL_OUTPUT_HUMAN_READABLE, + TBL_COL_DELIMITER("\t"), +}; + +static void do_default_order_test(struct fastbuf *out) +{ + table_init(&test_default_order_tbl, out); + table_start(&test_default_order_tbl); + + table_set_int(&test_default_order_tbl, test_default_order_col0_int, 0); + table_set_int(&test_default_order_tbl, test_default_order_col1_int, 1); + table_set_int(&test_default_order_tbl, test_default_order_col2_int, 2); + table_end_row(&test_default_order_tbl); + + table_set_int(&test_default_order_tbl, test_default_order_col0_int, 10); + table_set_int(&test_default_order_tbl, test_default_order_col1_int, 11); + table_set_int(&test_default_order_tbl, test_default_order_col2_int, 12); + table_end_row(&test_default_order_tbl); + + table_end(&test_default_order_tbl); + table_cleanup(&test_default_order_tbl); +} + +/** + * tests: table_set_nt, table_set_uint, table_set_bool, table_set_double, table_set_printf + **/ +static void do_print1(struct table *test_tbl) +{ + table_set_str(test_tbl, test_col0_str, "sdsdf"); + table_append_str(test_tbl, "aaaaa"); + table_set_int(test_tbl, test_col1_int, -10); + table_set_int(test_tbl, test_col1_int, 10000); + table_set_uint(test_tbl, test_col2_uint, 10); + table_set_printf(test_tbl, test_col2_uint, "XXX-%u", 22222); + table_set_bool(test_tbl, test_col3_bool, 1); + table_set_double(test_tbl, test_col4_double, 1.5); + table_end_row(test_tbl); + + table_set_str(test_tbl, test_col0_str, "test"); + table_append_str(test_tbl, "bbbbb"); + table_set_int(test_tbl, test_col1_int, -100); + table_set_uint(test_tbl, test_col2_uint, 100); + table_set_bool(test_tbl, test_col3_bool, 0); + table_set_double(test_tbl, test_col4_double, 1.5); + table_end_row(test_tbl); +} + +static char **cli_table_opts; +static int test_default_column_order; +static int test_invalid_option; +static int test_invalid_order; + +static struct opt_section table_printer_opts = { + OPT_ITEMS { + OPT_HELP("Options:"), + OPT_STRING_MULTIPLE('T', "table", cli_table_opts, OPT_REQUIRED_VALUE, "\tSets options for the table."), + OPT_BOOL('d', 0, test_default_column_order, 0, "\tRun the test that uses the default column order."), + OPT_BOOL('i', 0, test_invalid_option, 0, "\tTest the output for invalid option."), + OPT_BOOL('n', 0, test_invalid_order, 0, "\tTest the output for invalid names of columns for the 'cols' option."), + OPT_END + } +}; + +static void process_command_line_opts(char *argv[], struct table *tbl) +{ + GARY_INIT(cli_table_opts, 0); + + opt_parse(&table_printer_opts, argv+1); + + for(uint i = 0; i < GARY_SIZE(cli_table_opts); i++) { + const char *rv = table_set_option(tbl, cli_table_opts[i]); + ASSERT_MSG(rv == NULL, "Tableprinter option parser returned error: '%s'.", rv); + } + + GARY_FREE(cli_table_opts); +} + +static int user_defined_option(struct table *tbl UNUSED, const char *key, const char *value) +{ + if(value == NULL && strcmp(key, "novaluekey") == 0) { + printf("setting key: %s; value: (null)\n", key); + return 0; + } + if(value != NULL && strcmp(value, "value") == 0 && + key != NULL && strcmp(key, "valuekey") == 0) { + printf("setting key: %s; value: %s\n", key, value); + return 0; + } + return 1; +} + +static void test_option_parser(struct table *tbl) +{ + tbl->callbacks->process_option = user_defined_option; + const char *rv = table_set_option(tbl, "invalid:option"); + if(rv) printf("Tableprinter option parser returned error: \"%s\".\n", rv); + + rv = table_set_option(tbl, "invalid"); + if(rv) printf("Tableprinter option parser returned error: \"%s\".\n", rv); + + rv = table_set_option(tbl, "novaluekey"); + if(rv) printf("Tableprinter option parser returned error: \"%s\".\n", rv); + + rv = table_set_option(tbl, "valuekey:value"); + if(rv) printf("Tableprinter option parser returned error: \"%s\".\n", rv); +} + +int main(int argc UNUSED, char **argv) +{ + struct fastbuf *out; + out = bfdopen_shared(1, 4096); + + table_init(&test_tbl, out); + + process_command_line_opts(argv, &test_tbl); + + if(test_invalid_order == 1) { + const char *rv = table_set_option(&test_tbl, "cols:test_col0_str,test_col1_int,xxx"); + if(rv) printf("Tableprinter option parser returned: '%s'.\n", rv); + return 0; + } else if(test_default_column_order == 1) { + do_default_order_test(out); + bclose(out); + return 0; + } else if(test_invalid_option == 1) { + test_option_parser(&test_tbl); + bclose(out); + return 0; + } + + table_start(&test_tbl); + do_print1(&test_tbl); + table_end(&test_tbl); + table_cleanup(&test_tbl); + + bclose(out); + + return 0; +} diff --git a/ucw/table-test.t b/ucw/table-test.t new file mode 100644 index 00000000..f2871a45 --- /dev/null +++ b/ucw/table-test.t @@ -0,0 +1,88 @@ +Run: ../obj/ucw/table-test -T cols:col3_bool +Out < + */ + +#include +#include +#include +#include +#include + +#include + +/** + * Forward defintions of table callbacks. Should these routines be static/private in the + * table.c ? What about the return value? Is it better to have void instead of int? + **/ +static int table_oneline_human_readable(struct table *tbl); +static int table_start_human_readable(struct table *tbl); +static int table_end_human_readable(struct table *tbl); + +static int table_oneline_machine_readable(struct table *tbl); +static int table_start_machine_readable(struct table *tbl); +static int table_end_machine_readable(struct table *tbl); + +struct table_output_callbacks table_fmt_human_readable = { + .row_output_func = table_oneline_human_readable, + .table_start_callback = table_start_human_readable, + .table_end_callback = table_end_human_readable, +}; + +struct table_output_callbacks table_fmt_machine_readable = { + .row_output_func = table_oneline_machine_readable, + .table_start_callback = table_start_machine_readable, + .table_end_callback = table_end_machine_readable, +}; + +void table_init(struct table *tbl, struct fastbuf *out) +{ + tbl->out = out; + + int col_count = 0; // count the number of columns in the struct table + + for(;;) { + if(tbl->columns[col_count].name == NULL && + tbl->columns[col_count].fmt == NULL && + tbl->columns[col_count].width == 0 && + tbl->columns[col_count].type == COL_TYPE_LAST) + break; + ASSERT(tbl->columns[col_count].name != NULL); + ASSERT(tbl->columns[col_count].type == COL_TYPE_ANY || tbl->columns[col_count].fmt != NULL); + ASSERT(tbl->columns[col_count].width != 0); + ASSERT(tbl->columns[col_count].type < COL_TYPE_LAST); + col_count++; + } + tbl->pool = mp_new(4096); + + tbl->column_count = col_count; + + if(tbl->callbacks->row_output_func == NULL && tbl->callbacks->table_start_callback == NULL && tbl->callbacks->table_end_callback == NULL) { + tbl->callbacks = &table_fmt_human_readable; + } + + tbl->print_header = 1; // by default, print header +} + +void table_cleanup(struct table *tbl) +{ + mp_delete(tbl->pool); + memset(tbl, 0, sizeof(struct table)); +} + +// TODO: test default column order +static void table_make_default_column_order(struct table *tbl) +{ + int *col_order_int = mp_alloc_zero(tbl->pool, sizeof(int) * tbl->column_count); + for(int i = 0; i < tbl->column_count; i++) { + col_order_int[i] = i; + } + table_col_order(tbl, col_order_int, tbl->column_count); +} + +void table_start(struct table *tbl) +{ + tbl->last_printed_col = -1; + tbl->row_printing_started = 0; + + tbl->col_str_ptrs = mp_alloc_zero(tbl->pool, sizeof(char *) * tbl->column_count); + + if(tbl->column_order == NULL) table_make_default_column_order(tbl); + + if(tbl->callbacks->table_start_callback != NULL) tbl->callbacks->table_start_callback(tbl); + if(tbl->cols_to_output == 0) { + die("Table should output at least one column."); + } + + mp_save(tbl->pool, &tbl->pool_state); + + ASSERT_MSG(tbl->col_delimiter, "In-between column delimiter not specified."); + ASSERT_MSG(tbl->append_delimiter, "Append delimiter not specified."); +} + +void table_end(struct table *tbl) +{ + tbl->last_printed_col = -1; + tbl->row_printing_started = 0; + tbl->print_header = 1; + + mp_restore(tbl->pool, &tbl->pool_state); + + if(tbl->callbacks->table_end_callback) tbl->callbacks->table_end_callback(tbl); +} + +static void table_write_header(struct table *tbl) +{ + uint col_idx = tbl->column_order[0]; + bprintf(tbl->out, "%*s", tbl->columns[col_idx].width, tbl->columns[col_idx].name); + + for(uint i = 1; i < tbl->cols_to_output; i++) { + col_idx = tbl->column_order[i]; + bputs(tbl->out, tbl->col_delimiter); + bprintf(tbl->out, "%*s", tbl->columns[col_idx].width, tbl->columns[col_idx].name); + } + + bputc(tbl->out, '\n'); +} + +int table_get_col_idx(struct table *tbl, const char *col_name) +{ + for(int i = 0; i < tbl->column_count; i++) { + if(strcmp(tbl->columns[i].name, col_name) == 0) return i; + } + return -1; +} + +const char * table_get_col_list(struct table *tbl) +{ + if(tbl->column_count == 0) return NULL; + + char *tmp = mp_printf(tbl->pool, "%s", tbl->columns[0].name); + + for(int i = 1; i < tbl->column_count; i++) { + mp_printf_append(tbl->pool, tmp, ",%s", tbl->columns[i].name); + } + + return tmp; +} + +void table_col_order(struct table *tbl, int *col_order, int cols_to_output) +{ + for(int i = 0; i < cols_to_output; i++) { + ASSERT_MSG(col_order[i] >= 0 && col_order[i] < tbl->column_count, "Column %d does not exists (column number should be between 0 and %d)", col_order[i], tbl->column_count); + } + + tbl->column_order = col_order; + tbl->cols_to_output = cols_to_output; +} + +/** + * TODO: ERROR! this function deliberately causes memory leak. the + * problem is that when table_col_order_by_name is called multiple-times, + * the mp_save adds all the resulting column orders on the memory pool. + * The memory leak is small, but it is present. + **/ +int table_col_order_by_name(struct table *tbl, const char *col_order_str) +{ + int col_order_len = strlen(col_order_str); + + char *tmp_col_order = stk_strdup(col_order_str); + + int col_count = 1; + for(int i = 0; i < col_order_len; i++) { + if(col_order_str[i] == ',') { + col_count++; + } + } + + struct mempool_state mp_tmp_state; + mp_save(tbl->pool, &mp_tmp_state); + + int *col_order_int = mp_alloc_zero(tbl->pool, sizeof(int) * col_count); + int curr_col_order_int = 0; + const char *name_start = tmp_col_order; + while(name_start) { + char *next = strchr(name_start, ','); + if(next) { + *next++ = 0; + } + + int idx = table_get_col_idx(tbl, name_start); + col_order_int[curr_col_order_int] = idx; + if(idx == -1) { + //ASSERT_MSG(idx != -1, "Table column with name '%s' does not exists.", name_start); + mp_restore(tbl->pool, &mp_tmp_state); + return -1; + } + curr_col_order_int++; + + name_start = next; + } + + tbl->column_order = col_order_int; + tbl->cols_to_output = curr_col_order_int; + return 0; +} + +void table_set_printf(struct table *tbl, int col, const char *fmt, ...) +{ + ASSERT_MSG(col < tbl->column_count && col >= 0, "Table column %d does not exists.", col); + tbl->last_printed_col = col; + tbl->row_printing_started = 1; + va_list args; + va_start(args, fmt); + tbl->col_str_ptrs[col] = mp_vprintf(tbl->pool, fmt, args); + va_end(args); +} + +static const char *table_set_col_default_fmts[] = { + [COL_TYPE_STR] = "%s", + [COL_TYPE_INT] = "%d", + [COL_TYPE_INTMAX] = "%jd", + [COL_TYPE_UINT] = "%u", + [COL_TYPE_UINTMAX] = "%ju", + [COL_TYPE_BOOL] = "%d", + [COL_TYPE_DOUBLE] = "%.2lf", + [COL_TYPE_ANY] = NULL, + [COL_TYPE_LAST] = NULL +}; + +#define TABLE_SET_COL(_name_, _type_, _typeconst_) void table_set_##_name_(struct table *tbl, int col, _type_ val) \ + {\ + const char *fmt = tbl->columns[col].fmt;\ + if(tbl->columns[col].type == COL_TYPE_ANY) {\ + fmt = table_set_col_default_fmts[_typeconst_];\ + }\ + table_set_##_name_##_fmt(tbl, col, fmt, val);\ + } + +#define TABLE_SET_COL_STR(_name_, _type_, _typeconst_) void table_set_##_name_##_name(struct table *tbl, const char *col_name, _type_ val) \ + {\ + int col = table_get_col_idx(tbl, col_name);\ + table_set_##_name_(tbl, col, val);\ + } + +#define TABLE_SET_COL_FMT(_name_, _type_, _typeconst_) void table_set_##_name_##_fmt(struct table *tbl, int col, const char *fmt, _type_ val)\ + {\ + ASSERT_MSG(col < tbl->column_count && col >= 0, "Table column %d does not exists.", col);\ + ASSERT(tbl->columns[col].type == COL_TYPE_ANY || _typeconst_ == tbl->columns[col].type);\ + ASSERT(fmt != NULL);\ + tbl->last_printed_col = col;\ + tbl->row_printing_started = 1;\ + tbl->col_str_ptrs[col] = mp_printf(tbl->pool, fmt, val);\ + } + +#define TABLE_SET(_name_, _type_, _typeconst_) TABLE_SET_COL(_name_, _type_, _typeconst_);\ + TABLE_SET_COL_STR(_name_, _type_, _typeconst_);\ + TABLE_SET_COL_FMT(_name_, _type_, _typeconst_); + +TABLE_SET(int, int, COL_TYPE_INT) +TABLE_SET(uint, uint, COL_TYPE_UINT) +TABLE_SET(double, double, COL_TYPE_DOUBLE) +TABLE_SET(str, const char *, COL_TYPE_STR) +TABLE_SET(intmax, intmax_t, COL_TYPE_INTMAX) +TABLE_SET(uintmax, uintmax_t, COL_TYPE_UINTMAX) +#undef TABLE_SET_COL_FMT +#undef TABLE_SET_COL_STR +#undef TABLE_SET_COL +#undef TABLE_SET + +void table_set_bool(struct table *tbl, int col, uint val) +{ + table_set_bool_fmt(tbl, col, tbl->columns[col].fmt, val); +} + +void table_set_bool_name(struct table *tbl, const char *col_name, uint val) +{ + int col = table_get_col_idx(tbl, col_name); + table_set_bool(tbl, col, val); +} + +void table_set_bool_fmt(struct table *tbl, int col, const char *fmt, uint val) +{ + ASSERT_MSG(col < tbl->column_count && col >= 0, "Table column %d does not exists.", col); + ASSERT(COL_TYPE_BOOL == tbl->columns[col].type); + + tbl->last_printed_col = col; + tbl->row_printing_started = 1; + tbl->col_str_ptrs[col] = mp_printf(tbl->pool, fmt, val ? "true" : "false"); +} + +#define TABLE_APPEND(_name_, _type_, _typeconst_) void table_append_##_name_(struct table *tbl, _type_ val) \ + {\ + ASSERT(tbl->last_printed_col != -1 || tbl->row_printing_started != 0);\ + ASSERT(_typeconst_ == tbl->columns[tbl->last_printed_col].type);\ + int col = tbl->last_printed_col;\ + mp_printf_append(tbl->pool, tbl->col_str_ptrs[col], "%s", tbl->append_delimiter);\ + tbl->col_str_ptrs[col] = mp_printf_append(tbl->pool, tbl->col_str_ptrs[col], tbl->columns[col].fmt, val);\ + } + +TABLE_APPEND(int, int, COL_TYPE_INT) +TABLE_APPEND(uint, uint, COL_TYPE_UINT) +TABLE_APPEND(double, double, COL_TYPE_DOUBLE) +TABLE_APPEND(str, const char *, COL_TYPE_STR) +TABLE_APPEND(intmax, intmax_t, COL_TYPE_INTMAX) +TABLE_APPEND(uintmax, uintmax_t, COL_TYPE_UINTMAX) +#undef TABLE_APPEND + +void table_append_bool(struct table *tbl, int val) +{ + ASSERT(tbl->last_printed_col != -1 || tbl->row_printing_started != 0); + ASSERT(COL_TYPE_BOOL == tbl->columns[tbl->last_printed_col].type); + + int col = tbl->last_printed_col; + + mp_printf_append(tbl->pool, tbl->col_str_ptrs[col], "%s", tbl->append_delimiter); + + tbl->col_str_ptrs[col] = mp_printf_append(tbl->pool, tbl->col_str_ptrs[col], tbl->columns[col].fmt, val ? "true" : "false"); +} + +void table_append_printf(struct table *tbl, const char *fmt, ...) +{ + ASSERT(tbl->last_printed_col != -1 || tbl->row_printing_started != 0); + int col = tbl->last_printed_col; + + va_list args; + va_start(args, fmt); + + mp_printf_append(tbl->pool, tbl->col_str_ptrs[col], "%s", tbl->append_delimiter); + tbl->col_str_ptrs[col] = mp_vprintf_append(tbl->pool, tbl->col_str_ptrs[col], fmt, args); + + va_end(args); +} + +void table_end_row(struct table *tbl) +{ + if(tbl->callbacks->row_output_func) { + tbl->callbacks->row_output_func(tbl); + } else { + die("Tableprinter: invalid parameter, struct table does not have filled row_output_func"); + } + memset(tbl->col_str_ptrs, 0, sizeof(char *) * tbl->column_count); + mp_restore(tbl->pool, &tbl->pool_state); + tbl->last_printed_col = -1; + tbl->row_printing_started = 0; +} + +/* + * construction of string in mempool using fastbuf + * + */ +struct fastbuf *table_col_fbstart(struct table *tbl, int col) +{ + fbpool_init(&tbl->fb_col_out); + fbpool_start(&tbl->fb_col_out, tbl->pool, 1); + tbl->col_out = col; + return &tbl->fb_col_out.fb; +} + +void table_col_fbend(struct table *tbl) +{ + tbl->col_str_ptrs[tbl->col_out] = fbpool_end(&tbl->fb_col_out); + tbl->col_out = -1; +} + +void table_set_output_callbacks(struct table *tbl, struct table_output_callbacks *callbacks) +{ + tbl->callbacks = callbacks; +} + +// Row output routines +static int table_oneline_human_readable(struct table *tbl) +{ + uint col = tbl->column_order[0]; + int col_width = tbl->columns[col].width; + bprintf(tbl->out, "%*s", col_width, tbl->col_str_ptrs[col]); + for(uint i = 1; i < tbl->cols_to_output; i++) { + col = tbl->column_order[i]; + col_width = tbl->columns[col].width; + bputs(tbl->out, tbl->col_delimiter); + bprintf(tbl->out, "%*s", col_width, tbl->col_str_ptrs[col]); + } + + bputc(tbl->out, '\n'); + return 0; +} + +static int table_oneline_machine_readable(struct table *tbl) +{ + uint col = tbl->column_order[0]; + bputs(tbl->out, tbl->col_str_ptrs[col]); + for(uint i = 1; i < tbl->cols_to_output; i++) { + col = tbl->column_order[i]; + bputs(tbl->out, tbl->col_delimiter); + bputs(tbl->out, tbl->col_str_ptrs[col]); + } + + bputc(tbl->out, '\n'); + return 0; +} + +static int table_start_human_readable(struct table *tbl) +{ + if(tbl->col_delimiter == NULL) { + tbl->col_delimiter = " "; + } + + if(tbl->append_delimiter == NULL) { + tbl->append_delimiter = ","; + } + + if(tbl->print_header != 0) { + tbl->print_header = 0; + table_write_header(tbl); + } + return 0; +} + +static int table_end_human_readable(struct table *tbl UNUSED) +{ + return 0; +} + +static int table_start_machine_readable(struct table *tbl) +{ + if(tbl->col_delimiter == NULL) { + tbl->col_delimiter = ";"; + } + + if(tbl->append_delimiter == NULL) { + tbl->append_delimiter = ","; + } + + if(tbl->print_header != 0) { + tbl->print_header = 0; + + uint col_idx = tbl->column_order[0]; + bputs(tbl->out, tbl->columns[col_idx].name); + for(uint i = 1; i < tbl->cols_to_output; i++) { + col_idx = tbl->column_order[i]; + bputs(tbl->out, tbl->col_delimiter); + bputs(tbl->out, tbl->columns[col_idx].name); + } + bputc(tbl->out, '\n'); + } + return 0; +} + +static int table_end_machine_readable(struct table *tbl UNUSED) +{ + return 0; +} + +static int get_colon(char *str) +{ + int l = strlen(str); + for(int i = 0; i < l; i++) { + if(str[i] == ':') return i; + } + return -1; +} + +static const char *table_set_option2(struct table *tbl, const char *key, const char *value) +{ + if(value == NULL || (value != NULL && strlen(value) == 0)) { + if(strcmp(key, "noheader") == 0) { + tbl->print_header = 0; + return NULL; + } else { + int rv = 1; + if(tbl->callbacks && tbl->callbacks->process_option) rv = tbl->callbacks->process_option(tbl, key, value); + if(rv) { + return mp_printf(tbl->pool, "Tableprinter: invalid option: '%s'.", key); + } + } + } else { + if(strcmp(key, "header") == 0) { + // FIXME: Check syntax of value. + //tbl->print_header = strtol(value, NULL, 10); //atoi(value); + //if(errno != 0) tbl->print_header + if(value[1] != 0) + return mp_printf(tbl->pool, "Tableprinter: invalid option: '%s' has invalid value: '%s'.", key, value); + uint tmp = value[0] - '0'; + if(tmp > 1) + return mp_printf(tbl->pool, "Tableprinter: invalid option: '%s' has invalid value: '%s'.", key, value); + tbl->print_header = tmp; + return NULL; + } else if(strcmp(key, "cols") == 0) { + // FIXME: We should not exit/abort on errors caused from command line. + if(table_col_order_by_name(tbl, value) != 0) { + const char *tmp = table_get_col_list(tbl); + return mp_printf(tbl->pool, "Invalid tableprinter column list: possible column names are %s.", tmp); + } + return NULL; + } else if(strcmp(key, "fmt") == 0) { + if(strcmp(value, "human") == 0) table_set_output_callbacks(tbl, &table_fmt_human_readable); + else if(strcmp(value, "machine") == 0) table_set_output_callbacks(tbl, &table_fmt_machine_readable); + else { + return "Tableprinter: invalid argument to output-type option."; + } + return NULL; + } else if(strcmp(key, "col-delim") == 0) { + char * d = mp_printf(tbl->pool, "%s", value); + tbl->col_delimiter = d; + return NULL; + } else { + int rv = 1; + if(tbl->callbacks && tbl->callbacks->process_option) rv = tbl->callbacks->process_option(tbl, key, value); + if(rv) { + return mp_printf(tbl->pool, "Tableprinter: invalid option: '%s:%s'.", key, value); + } + } + } + return NULL; +} + +const char *table_set_option(struct table *tbl, const char *opt) +{ + char *opt_dup = stk_strdup(opt); + int colidx = get_colon(opt_dup); + if(colidx > 0) opt_dup[colidx] = 0; + char *key = opt_dup; + char *value = NULL; + if(colidx > 0) value = opt_dup + colidx + 1; + return table_set_option2(tbl, key, value); +} + +const char *table_set_gary_options(struct table *tbl, char **gary_table_opts) +{ + for (uint i = 0; i < GARY_SIZE(gary_table_opts); i++) { + const char *rv = table_set_option(tbl, gary_table_opts[i]); + if (rv != NULL) { + return rv; + } + } + return NULL; +} + +#ifdef TEST + +#include + +enum test_table_cols { + test_col0_str, test_col1_int, test_col2_uint, test_col3_bool, test_col4_double +}; + +static uint test_column_order[] = {test_col3_bool, test_col4_double, test_col2_uint,test_col1_int, test_col0_str}; + +static struct table test_tbl = { + TBL_COLUMNS { + TBL_COL_STR(test, col0_str, 20), + TBL_COL_INT(test, col1_int, 8), + TBL_COL_UINT(test, col2_uint, 9), + TBL_COL_BOOL(test, col3_bool, 9), + TBL_COL_DOUBLE(test, col4_double, 11, 2), + TBL_COL_END + }, + TBL_COL_ORDER(test_column_order), + TBL_OUTPUT_HUMAN_READABLE, + TBL_COL_DELIMITER("\t"), + TBL_APPEND_DELIMITER(",") +}; + +/** + * tests: table_set_nt, table_set_uint, table_set_bool, table_set_double, table_set_printf + **/ +static void do_print1(struct table *test_tbl) +{ + table_set_str(test_tbl, test_col0_str, "sdsdf"); + table_append_str(test_tbl, "aaaaa"); + table_set_int(test_tbl, test_col1_int, -10); + table_set_int(test_tbl, test_col1_int, 10000); + table_set_uint(test_tbl, test_col2_uint, 10); + table_set_printf(test_tbl, test_col2_uint, "XXX-%u", 22222); + table_set_bool(test_tbl, test_col3_bool, 1); + table_set_double(test_tbl, test_col4_double, 1.5); + table_set_printf(test_tbl, test_col4_double, "AAA"); + table_end_row(test_tbl); + + table_set_str(test_tbl, test_col0_str, "test"); + table_append_str(test_tbl, "bbbbb"); + table_set_int(test_tbl, test_col1_int, -100); + table_set_uint(test_tbl, test_col2_uint, 100); + table_set_bool(test_tbl, test_col3_bool, 0); + table_set_printf(test_tbl, test_col4_double, "%.2lf", 1.5); + table_end_row(test_tbl); +} + +static void test_simple1(struct fastbuf *out) +{ + table_init(&test_tbl, out); + // print table with header + table_col_order_by_name(&test_tbl, "col3_bool"); + table_start(&test_tbl); + do_print1(&test_tbl); + table_end(&test_tbl); + + // print the same table as in the previous case without header + table_col_order_by_name(&test_tbl, "col0_str,col2_uint,col1_int,col3_bool"); + table_start(&test_tbl); + do_print1(&test_tbl); + table_end(&test_tbl); + + // this also tests whether there is need to call table_col_order_by_name after table_end was called + test_tbl.print_header = 0; + table_start(&test_tbl); + do_print1(&test_tbl); + table_end(&test_tbl); + + table_col_order_by_name(&test_tbl, "col3_bool"); + table_start(&test_tbl); + do_print1(&test_tbl); + table_end(&test_tbl); + + table_col_order_by_name(&test_tbl, "col3_bool,col0_str"); + table_start(&test_tbl); + do_print1(&test_tbl); + table_end(&test_tbl); + + table_col_order_by_name(&test_tbl, "col0_str,col3_bool,col2_uint"); + table_start(&test_tbl); + do_print1(&test_tbl); + table_end(&test_tbl); + + table_col_order_by_name(&test_tbl, "col0_str,col3_bool,col2_uint,col0_str,col3_bool,col2_uint,col0_str,col3_bool,col2_uint"); + table_start(&test_tbl); + do_print1(&test_tbl); + table_end(&test_tbl); + + table_col_order_by_name(&test_tbl, "col0_str,col1_int,col2_uint,col3_bool,col4_double"); + table_start(&test_tbl); + do_print1(&test_tbl); + table_end(&test_tbl); + + table_cleanup(&test_tbl); +} + +enum test_any_table_cols { + test_any_col0_int, test_any_col1_any +}; + +static uint test_any_column_order[] = { test_any_col0_int, test_any_col1_any }; + +static struct table test_any_tbl = { + TBL_COLUMNS { + TBL_COL_INT(test_any, col0_int, 8), + TBL_COL_ANY(test_any, col1_any, 9), + TBL_COL_END + }, + TBL_COL_ORDER(test_any_column_order), + TBL_OUTPUT_HUMAN_READABLE, + TBL_COL_DELIMITER("\t"), + TBL_APPEND_DELIMITER(",") +}; + +static void test_any_type(struct fastbuf *out) +{ + table_init(&test_any_tbl, out); + table_start(&test_any_tbl); + + table_set_int(&test_any_tbl, test_any_col0_int, -10); + table_set_int(&test_any_tbl, test_any_col1_any, 10000); + table_end_row(&test_any_tbl); + + table_set_int(&test_any_tbl, test_any_col0_int, -10); + table_set_double(&test_any_tbl, test_any_col1_any, 1.4); + table_end_row(&test_any_tbl); + + table_set_printf(&test_any_tbl, test_any_col0_int, "%d", 10); + table_append_printf(&test_any_tbl, "%d", 20); + table_append_printf(&test_any_tbl, "%d", 30); + table_set_double(&test_any_tbl, test_any_col1_any, 1.4); + table_append_printf(&test_any_tbl, "%.2lf", 1.5); + table_append_printf(&test_any_tbl, "%.2lf", 1.6); + table_end_row(&test_any_tbl); + + table_end(&test_any_tbl); + table_cleanup(&test_any_tbl); +} + +int main(int argc UNUSED, char **argv UNUSED) +{ + struct fastbuf *out; + out = bfdopen_shared(1, 4096); + + test_simple1(out); + + test_any_type(out); + + bclose(out); + return 0; +} + +#endif diff --git a/ucw/table.h b/ucw/table.h new file mode 100644 index 00000000..0a2b4dbe --- /dev/null +++ b/ucw/table.h @@ -0,0 +1,296 @@ +/* + * UCW Library -- Table printer + * + * (c) 2014 Robert Kessl + */ + +#ifndef _TABLEPRINTER_H +#define _TABLEPRINTER_H + +#include +#include + +enum column_type { + COL_TYPE_STR, COL_TYPE_INT, COL_TYPE_INTMAX, COL_TYPE_UINT, COL_TYPE_UINTMAX, COL_TYPE_BOOL, COL_TYPE_DOUBLE, COL_TYPE_ANY, COL_TYPE_LAST +}; + +#define TBL_COL_STR(_enum_prefix, _name, _width) [_enum_prefix##_##_name] = { .name = #_name, .width = _width, .fmt = "%s", .type = COL_TYPE_STR } +#define TBL_COL_INT(_enum_prefix, _name, _width) [_enum_prefix##_##_name] = { .name = #_name, .width = _width, .fmt = "%d", .type = COL_TYPE_INT } +#define TBL_COL_UINT(_enum_prefix, _name, _width) [_enum_prefix##_##_name] = { .name = #_name, .width = _width, .fmt = "%u", .type = COL_TYPE_UINT } +#define TBL_COL_INTMAX(_enum_prefix, _name, _width) [_enum_prefix##_##_name] = { .name = #_name, .width = _width, .fmt = "%jd", .type = COL_TYPE_INTMAX } +#define TBL_COL_UINTMAX(_enum_prefix, _name, _width) [_enum_prefix##_##_name] = { .name = #_name, .width = _width, .fmt = "%ju", .type = COL_TYPE_UINTMAX } +#define TBL_COL_HEXUINT(_enum_prefix, _name, _width) [_enum_prefix##_##_name] = { .name = #_name, .width = _width, .fmt = "0x%x", .type = COL_TYPE_UINT } +#define TBL_COL_DOUBLE(_enum_prefix, _name, _width, _prec) [_enum_prefix##_##_name] = { .name = #_name, .width = _width, .fmt = "%." #_prec "lf", .type = COL_TYPE_DOUBLE } +#define TBL_COL_BOOL(_enum_prefix, _name, _width) [_enum_prefix##_##_name] = { .name = #_name, .width = _width, .fmt = "%s", .type = COL_TYPE_BOOL } +#define TBL_COL_ANY(_enum_prefix, _name, _width) [_enum_prefix##_##_name] = { .name = #_name, .width = _width, .fmt = 0, .type = COL_TYPE_ANY } + +#define TBL_COL_STR_FMT(_enum_prefix, _name, _width, _fmt) [_enum_prefix##_##_name] = { .name = #_name, .width = _width, .fmt = _fmt, .type = COL_TYPE_STR } +#define TBL_COL_INT_FMT(_enum_prefix, _name, _width, _fmt) [_enum_prefix##_##_name] = { .name = #_name, .width = _width, .fmt = _fmt, .type = COL_TYPE_INT } +#define TBL_COL_UINT_FMT(_enum_prefix, _name, _width, _fmt) [_enum_prefix##_##_name] = { .name = #_name, .width = _width, .fmt = _fmt, .type = COL_TYPE_UINT } +#define TBL_COL_INTMAX_FMT(_enum_prefix, _name, _width, _fmt) [_enum_prefix##_##_name] = { .name = #_name, .width = _width, .fmt = _fmt, .type = COL_TYPE_INTMAX } +#define TBL_COL_UINTMAX_FMT(_enum_prefix, _name, _width, _fmt) [_enum_prefix##_##_name] = { .name = #_name, .width = _width, .fmt = _fmt, .type = COL_TYPE_UINTMAX } +#define TBL_COL_HEXUINT_FMT(_enum_prefix, _name, _width, _fmt) [_enum_prefix##_##_name] = { .name = #_name, .width = _width, .fmt = _fmt, .type = COL_TYPE_UINT } +#define TBL_COL_BOOL_FMT(_enum_prefix, _name, _width, _fmt) [_enum_prefix##_##_name] = { .name = #_name, .width = _width, .fmt = _fmt, .type = COL_TYPE_BOOL } + +#define TBL_COL_END { .name = 0, .width = 0, .fmt = 0, .type = COL_TYPE_LAST } + +#define TBL_COLUMNS .columns = (struct table_column []) +#define TBL_COL_ORDER(order) .column_order = (int *) order, .cols_to_output = ARRAY_SIZE(order) +#define TBL_COL_DELIMITER(_delimiter_) .col_delimiter = _delimiter_ +#define TBL_APPEND_DELIMITER(_delimiter_) .append_delimiter = _delimiter_ + +#define TBL_OUTPUT_HUMAN_READABLE .callbacks = &table_fmt_human_readable +#define TBL_OUTPUT_MACHINE_READABLE .callbacks = &table_fmt_machine_readable + +/*** + * [[ Usage ]] + * The table works as follows: + * The table can be used after table_init is called. Then at the beginning of each printing, the + * table_start function must be called. After printing, the table_end must be called. The + * table_start MUST be paired with table_end. Inbetween table_start/table_end the user can set the + * cells of one row and one row is finished and printed using table_end_of_row. The pairs + * table_start/table_end can be used multiple-times for one table. The table is deallocated using + * table_cleanup. After table_cleanup is called it is not possible to further use the struct table. + * The struct table must be reinitialized. + * + * Default behaviour of the table_set_col_* is replacement of already set data. To append, the user + * must use table_append_* + * + * To summarize: + * 1) @table_init is called; + * 2) @table_start is called following by table_set_xxx functions and @table_end. + * table_start/table_end forms 1-level parenthesis structure. Some of the table + * settings can be changed only between table_init and @table_start or after table_end + * is called (but before next table_start. + * 3) the table is deallocated using @table_cleanup. After the cleanup + * is done, the struct table is unusable and must be initialized. + * + * + * An example of the procedure is following sequence of calls: + * table_init + * + * table_start + * table_end + * table_start + * table_end + * + * table_cleanup + * + * The tableprinter supports user-specified callback for each row and table-print (i.e., a callback + * that is called in table_end). + * + * The table is initialized by defining a table struct using the following macros: + * o TBL_START_COLUMNS indicates start of definition of columns + * o TBL_COL_XXX macros specify the column types with some default formatting the column is specified using a column + * name (which should be C identifier) and a prefix. the column name is the a string with the column + * name. The prefix is used for discriminating between columns from different tables. The column index + * should be taken from an enum. The enum identifier is prefix concatenated with the column name identifier. + * o TBL_COL_XXX_F macros specify column types with user supplied formatting + * o TBL_COL_END indicates end of column definitions + * o TBL_COL_ORDER specify the column order + * o TBL_COL_DELIMITER specify the in-between cell delimiter + * + * The table cells have strict type control, with the exception of type TBL_COL_ANY. In the case of + * TBL_COL_ANY, the type is not tested and an arbitrary value can be printed into the cell. + * It is also possible to print string to an arbitrary cell. + * + * Features: + * * user supplied callback functions can be used for modifying the output format. + * + * Non-tested features: + * * computing statistics of columns via the table_start_callback/table_end_callback. + * TODO: is it better to have callback for each cell with the original value supplied by the caller of the table_set_* functions? + * TODO: + * * unsupported: (dynamic) alignment of cells which is computed in table_end + * + * TODO: table_set_col_fmt: this functin takes the format string and the value. But I'm not able to + * test whether the format string and the type match !!! + * + * TODO: Return value of the parser should be a string allocated on the mempool of the table. But: + * is the return value really necessary? The error should be show to the user on the terminal + * (std. out). + * TODO: all macros prefix TBL_ should be changed to TABLE_ ? + * TODO: how to print column which is aligned to the left flag for alignment: 1) left; 2) right; + * 3) decimal point alignment; 4) arbitrary separator alignment + ***/ + +struct table; + +/** Specification of a single table column */ +struct table_column { + const char *name; // [*] name of the column displayed in the header + int width; // [*] width of the column (in characters). Negative number indicates alignment to left. SHOULD BE RATHER INDICATED BY A FLAG? + const char *fmt; // [*] default format of each cell in the column + enum column_type type; // type of the cells in the column. +}; + +struct table_output_callbacks { + int (*row_output_func)(struct table *tbl); // [*] function that outputs one row + int (*table_start_callback)(struct table *tbl); // [*] table_start callback + int (*table_end_callback)(struct table *tbl); // [*] table_end callback + // FIXME: Int -> void? + int (*process_option)(struct table *tbl, const char *key, const char *value); + // FIXME: Shouldn't it be possible to return also a custom error string? For example in an optionally writeable `const char **' argument. +}; + +/** The definition of a table. Contains column definitions plus internal data. */ +struct table { + struct table_column *columns; // [*] columns definition + int column_count; // [*] number of columns of the table + struct mempool *pool; // memory pool used for storing all the data. At the beggining are the data needed for printing + // the whole table (delimited by table_start, table_end) + struct mempool_state pool_state; // state of the pool AFTER the table is initialized. The state is used for + // deallocation of the strings used for printing single table row + + char **col_str_ptrs; // used to store the position of the row in the memory pool + + uint *column_order; // [*] order of the columns in the print-out of the table. + uint cols_to_output; // [*] number of columns that are printed. + const char *col_delimiter; // [*] delimiter that is placed between the columns + const char *append_delimiter; // [*] character used for delimiting the values in a single cell + uint print_header; // [*] 0 indicates that the header should not be printed + + struct fastbuf *out; // fastbuffer that is used for outputing the table rows + int last_printed_col; // index of the last column which was set. -1 indicates start of row. Used for example for + // appending to the last column. + int row_printing_started; // this can be considered as a duplicity of last_printed_col (it is -1 if the row printing + // did not start), but it is probably better to store the flag separately + + struct fbpool fb_col_out; // used for printing using the fast buffers(into the mempool), @see table_col_fbstart() + int col_out; // index of the column that is currently printed using fb_col_out + + struct table_output_callbacks *callbacks; + void *data; // [*] user-managed data. the data can be used for example in row_output_func, table_start_callback, table_end_callback +}; + + +/** + * table_init serves for initialization of the table. The @tbl parameter should have set the columns member of + * the table structure. The @out parameter is supplied by the caller and can be deallocated after table_deinit + * is called. + **/ +void table_init(struct table *tbl, struct fastbuf *out); +void table_cleanup(struct table *tbl); + +/** + * table_start is called before the cells of the table are set. After the table_start is called, the user can + * call the table_set_* functions. The table_end_of_row function can be called after the table_start is called + * (but before the table_end is called) + **/ +void table_start(struct table *tbl); + +/** + * This function must be called after all the rows of the current table are printed. The table_set_* + * functions can be called in between table_start and table_end calls. + **/ +void table_end(struct table *tbl); + +/** + * Sets the order in which the columns are printed. The @col_order parameter is used until the table_end or + * table_cleanup is called. The table stores the pointer only and the memory pointed to by @col_order is + * allocated and deallocated by the caller. + **/ +void table_col_order(struct table *tbl, int *col_order, int col_order_size); + +/** + * Sets the order in which the columns are printed. The specification is a string with comma delimited column + * names. + **/ +int table_col_order_by_name(struct table *tbl, const char *col_order); + +/** + * Called when all the cells have filled values. The function the prints a table row into the output stream. + * The table row has newline at the end. + **/ +void table_end_row(struct table *tbl); + +/** + * Prints a string that is printf-like formated into a particular column. This function does not check the + * type of the column, i.e., it can be used to print double into an int column + **/ +void table_set_printf(struct table *tbl, int col, const char *fmt, ...) FORMAT_CHECK(printf, 3, 4); + +/** + * Appends a string that is printf-like formated to the last printed column. This function does not check the + * type of the column, i.e., it can be used to print double into an int column + **/ +void table_append_printf(struct table *tbl, const char *fmt, ...) FORMAT_CHECK(printf, 2, 3); + +/** + * Find the index of a column with name @col_name and returns it. Returns -1 if the column was not found. + **/ +int table_get_col_idx(struct table *tbl, const char *col_name); + +/** + * Returns comma-separated list of column names + **/ +const char * table_get_col_list(struct table *tbl); + +/** + * Opens a fastbuf stream that can be used for creating a cell content. The @sz parameter is the initial size + * allocated on the memory pool. + **/ +struct fastbuf *table_col_fbstart(struct table *tbl, int col); +// FIXME: test table_col_fbstart/table_col_fbend + +/** + * Closes the stream that is used for printing of the last column + **/ +void table_col_fbend(struct table *tbl); + +/** + * Sets the callbacks in @tbl. The callbacks are stored the arg @callbacks. + **/ +void table_set_output_callbacks(struct table *tbl, struct table_output_callbacks *callbacks); + + +/** + * Process the table one option and sets the values in @tbl according to the command-line parameters. + * The option has the following format: a) ":"; b) "" (currently not used). + * + * Possible key-value pairs: + * header:[0|1] - 1 indicates that the header should be printed, 0 otherwise + * noheader - equivalent to header:0 + * cols: - comma-separated list of columns that will be printed (in the order specified on the cmd-line) + * fmt:[human|machine|...] - output type + * col-delim: - column delimiter + * + * Returns NULL on success or an error string otherwise. + **/ +const char *table_set_option(struct table *tbl, const char *opt); +const char *table_set_gary_options(struct table *tbl, char **gary_table_opts); + +extern struct table_output_callbacks table_fmt_human_readable; +extern struct table_output_callbacks table_fmt_machine_readable; + +#define TABLE_SET_COL_PROTO(_name_, _type_) void table_set_##_name_(struct table *tbl, int col, _type_ val);\ + void table_set_##_name_##_name(struct table *tbl, const char *col_name, _type_ val);\ + void table_set_##_name_##_fmt(struct table *tbl, int col, const char *fmt, _type_ val) FORMAT_CHECK(printf, 3, 0); + +// table_set__fmt has one disadvantage: it is not possible to +// check whether fmt contains format that contains formatting that is +// compatible with _type_ + +TABLE_SET_COL_PROTO(int, int); +TABLE_SET_COL_PROTO(uint, uint); +TABLE_SET_COL_PROTO(double, double); +TABLE_SET_COL_PROTO(str, const char *); +TABLE_SET_COL_PROTO(intmax, intmax_t); +TABLE_SET_COL_PROTO(uintmax, uintmax_t); + +void table_set_bool(struct table *tbl, int col, uint val); +void table_set_bool_name(struct table *tbl, const char *col_name, uint val); +void table_set_bool_fmt(struct table *tbl, int col, const char *fmt, uint val); +#undef TABLE_SET_COL_PROTO + +#define TABLE_APPEND_PROTO(_name_, _type_) void table_append_##_name_(struct table *tbl, _type_ val) +TABLE_APPEND_PROTO(int, int); +TABLE_APPEND_PROTO(uint, uint); +TABLE_APPEND_PROTO(double, double); +TABLE_APPEND_PROTO(str, const char *); +TABLE_APPEND_PROTO(intmax, intmax_t); +TABLE_APPEND_PROTO(uintmax, uintmax_t); +void table_append_bool(struct table *tbl, int val); +#undef TABLE_APPEND_PROTO + +#endif diff --git a/ucw/table.t b/ucw/table.t new file mode 100644 index 00000000..d5dc82a4 --- /dev/null +++ b/ucw/table.t @@ -0,0 +1,30 @@ +Run: ../obj/ucw/table-t +Out <