diff --git a/include/lib/util_fmt.h b/include/lib/util_fmt.h new file mode 100644 index 00000000..957768bf --- /dev/null +++ b/include/lib/util_fmt.h @@ -0,0 +1,235 @@ +/* + * util_fmt - Format structured key-value data as JSON, text pairs, or CSV + * + * Copyright IBM Corp. 2024 + * + * s390-tools is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + * + * This module provides helper functions for converting structured key-value + * data into different output formats. + * + * Benefits: + * - Output format can be dynamically configured at run-time + * - Callers do not need to add extra code for each output format + * - Some format-specific requirements such as quoting, indentation, and + * comma-placement are automated + * + * Basic API calling sequence: + * + * util_fmt_init() => Select output format + * util_fmt_obj_start() => Start a new object or list + * util_fmt_pair() => Emit a key-value pair + * util_fmt_obj_end() => End the latest object or list + * util_fmt_exit() => Cleanup + * + * Note: + * - Supported data elements are objects, lists and key-value pairs (mappings) + * - Scalars are only supported as part of a mapping + * - For CSV output and key filtering, mapping keys must be unique - this can + * be achieved either by choosing unique key names or by including object + * names via the FMT_PREFIX flag + * - For CSV output, at least one object or list with the FMT_ROW flag must be + * emitted + * - Common tool-specific meta-information such as API-level, tool version, + * etc. is automatically added to the output + */ + +#ifndef LIB_UTIL_FMT_H +#define LIB_UTIL_FMT_H + +#include +#include + +/* Flag value for default behavior (all flag types). */ +#define FMT_DEFAULT 0 + +/* Names of supported output format types. */ +#define FMT_TYPE_NAMES "json json-seq pairs csv" + +/** + * enum util_fmt_t - Output format types. + * @FMT_JSON: JavaScript Object Notation output data structure + * @FMT_JSONSEQ: Sequence of JSON data structures according to RFC7464 + * @FMT_PAIRS: Textual key=value pairs + * @FMT_CSV: Comma-separated-values output + * + * Use these types with util_fmt_init() to control the output format. + */ +enum util_fmt_t { + FMT_JSON, + FMT_JSONSEQ, + FMT_PAIRS, + FMT_CSV, +}; + +/** + * enum util_fmt_flags_t - Format control flags. + * @FMT_NOPREFIX: (pairs) Remove object hierarchy prefix from keys + * @FMT_KEEPINVAL: (all) Print mappings even if value is marked as invalid + * Values will be replaced with null (JSON) or an empty + * string + * @FMT_QUOTEALL: (all) Add quotes to all mapping values + * @FMT_FILTER: (all) Ignore keys not announced via util_fmt_add_key() + * @FMT_HANDLEINT: (json) Ensure correct JSON closure when interrupted + * @FMT_NOMETA: (all) Do not emit tool meta-data + * @FMT_WARN: (all) Warn about incorrect API usage + * + * Use these flags with util_fmt_init() to control generic aspects. + */ +enum util_fmt_flags_t { + FMT_NOPREFIX = (1 << 0), + FMT_KEEPINVAL = (1 << 1), + FMT_QUOTEALL = (1 << 2), + FMT_FILTER = (1 << 3), + FMT_HANDLEINT = (1 << 4), + FMT_NOMETA = (1 << 5), + FMT_WARN = (1 << 6), +}; + +/** + * enum util_fmt_oflags_t - Object flags. + * @FMT_LIST: (all) Object is a list + * @FMT_ROW: (csv) Start a new CSV row with this object + * @FMT_PREFIX: (all) Include object name in key prefix for CSV headings + * and filter keys + * + * Use these flags with util_fmt_obj_start() to control object related + * aspects. + */ +enum util_fmt_oflags_t { + FMT_LIST = (1 << 0), + FMT_ROW = (1 << 1), + FMT_PREFIX = (1 << 2), +}; + +/** + * enum util_fmt_mflags_t - Mapping flags. + * @FMT_QUOTE: (all) Quote value + * @FMT_INVAL: (all) Mark value as invalid + * @FMT_PERSIST: (csv) Keep value across CSV rows until overwritten + * + * Use these flags with util_fmt_pair() to control mapping related aspects. + */ +enum util_fmt_mflags_t { + FMT_QUOTE = (1 << 0), + FMT_INVAL = (1 << 1), + FMT_PERSIST = (1 << 2), +}; + +/** + * util_fmt_init() - Initialize output formatter. + * @fd : Output file descriptor + * @type : Output format type + * @flags: Formatting parameters + * @api_level: Output format level indicator + * + * Prepare for writing formatted output with the given @type to @fd. Additional + * @flags can be specified to control certain output aspects (see &enum + * util_fmt_flags_t). + * + * @api_level represents an application-specific output format version number: + * this number starts at 1 and must be increased whenever an incompatible format + * change is introduced, e.g. when a non-optional object or mapping is removed + * or used for different data. + */ +void util_fmt_init(FILE *fd, enum util_fmt_t type, unsigned int flags, + int api_level); + +/** + * util_fmt_exit() - Release resources used by output formatter. + * + * Release all resources currently in use by the output formatter. + */ +void util_fmt_exit(void); + +/** + * util_fmt_name_to_type() - Convert format name to type identifier. + * @name: Format name + * @type: Pointer to resulting format type identifier + * + * Search supported output format types for a type with associated @name. If + * found, store resulting type identifier in @type. + * + * Return: %true if type is found, %false otherwise. + */ +bool util_fmt_name_to_type(const char *name, enum util_fmt_t *type); + +/** + * util_fmt_set_indent() - Set indentation parameters. + * @base : Base indentation level to apply to all output lines (default 0) + * @width : Number of indentation characters per intendation level (default 2) + * @ind_char: Indentation characters to use (default space). + */ +void util_fmt_set_indent(unsigned int base, unsigned int width, char ind_char); + +/** + * util_fmt_add_key() - Register expected mapping keys. + * @fmt: Format string to generate key + * + * Register a mapping key before the associated key-value pair is emitted. + * + * Use this function together with format control flag @FMT_FILTER to ignore all + * key-value pairs for which the key has not been registered. This can be + * useful to allow for dynamically configured filtering of output based on + * a static list of emitted mappings. + * + * When creating CSV output, use this function to register all column keys + * in advance to enable a stable column list in case of rows that do not + * provide data for all columns. + */ +void util_fmt_add_key(const char *fmt, ...); + +/** + * util_fmt_obj_start() - Start a new data object. + * @oflags: Flags controlling aspects of this object. + * @fmt : Format string for generating an object name or %NULL. + * + * Use this function to start a new object in output data. Depending on + * @oflags, the new object represents either a normal object or a list. @oflags + * can also be used to indicated that an object corresponds to a new row of + * CSV data. If @fmt is non-%NULL, the resulting name is used in a format + * type specified way: + * + * Pairs: + * - Object names are reflected as dot-separated component in the mapping + * prefix, e.g. 'a.b.key=value' + * - An index is generated for mappings and objects that are part of list, + * e.g. 'a.b[1].key=value' + * JSON: + * - Object names are reflected as key-object mappings, e.g. + * : { } + * - Required commas between objects and mappings are automatically generated + * CSV: + * - Object names and the list type flag have no effect + * - When flag @FMT_ROW is specified, a CSV row will be emitted when + * util_fmt_obj_end() is called for the associated object + */ +void util_fmt_obj_start(unsigned int oflags, const char *fmt, ...); + +/** + * util_fmt_obj_end() - Announce the end of the latest data object started. + * + * Each object started with util_fmt_obj_start() must be ended with an + * associated util_fmt_obj_end() call. + */ +void util_fmt_obj_end(void); + +/** + * util_fmt_pair() - Emit a key-value pair. + * @mflags: Flags controlling this pair. + * @key : Key for this pair, excluding prefix. + * @fmt : Format string used to generated the pair value. + * + * Emit a key-value pair with the specified @key and the value that results + * from format string @fmt. + * + * Notes: + * - For JSON, a mapping can only occur after util_fmt_obj_start() + * - For CSV, each @key must be unique, either by choosing unique key names + * or by including object names as prefix via the use of FMT_PREFIX in + * parent objects + */ +void util_fmt_pair(unsigned int mflags, const char *key, const char *fmt, ...); + +#endif /* LIB_UTIL_FMT_H */ diff --git a/libutil/util_fmt.c b/libutil/util_fmt.c new file mode 100644 index 00000000..4e40fae2 --- /dev/null +++ b/libutil/util_fmt.c @@ -0,0 +1,763 @@ +/* + * util - Utility function library + * + * Format structured data as key-value pairs, JSON, or CSV + * + * Copyright IBM Corp. 2024 + * + * s390-tools is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lib/util_base.h" +#include "lib/util_fmt.h" +#include "lib/util_libc.h" +#include "lib/util_rec.h" +#include "lib/zt_common.h" + +struct obj_t { + char *name; + bool is_list; + bool is_row; + bool is_prefix; + unsigned int index; +}; + +struct key_t { + char *name; + bool persist; +}; + +static struct { + enum util_fmt_t type; + FILE *fd; + int fileno; + /* Format control. */ + bool hide_prefix; + bool hide_inval; + bool quote_all; + bool do_filter; + bool do_warn; + bool hide_meta; + bool handle_int; + int api_level; + const char *nl; + /* JSON specifics. */ + unsigned int ind_base; + unsigned int ind_width; + char ind_char; + bool meta_done; + /* CSV specifics. */ + struct util_rec *csv_rec; + bool csv_hdr; + bool csv_data; + /* State. */ + unsigned int lvl; + struct obj_t *objs; + unsigned int num_objs; + struct key_t *keys; + unsigned int num_keys; + struct sigaction old_int; + struct sigaction old_term; + /* Methods. */ + void (*obj_start)(struct obj_t *parent, struct obj_t *obj); + void (*obj_end)(struct obj_t *parent, struct obj_t *obj); + void (*map)(struct obj_t *parent, unsigned int mflags, const char *key, + const char *val); + void (*term)(void); +} f; + +#define fwarn(fmt, ...) \ + do { if (f.do_warn) warnx(fmt, ##__VA_ARGS__); } while (0) + +/* Map format name to format ID. */ +static const struct { + const char *name; + enum util_fmt_t fmt; +} formats[] = { + { "json", FMT_JSON }, + { "json-seq", FMT_JSONSEQ }, + { "pairs", FMT_PAIRS }, + { "csv", FMT_CSV }, +}; + +/* Signal mask for blocking INT and TERM signals. */ +static sigset_t no_int_mask; + +bool util_fmt_name_to_type(const char *name, enum util_fmt_t *type) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(formats); i++) { + if (strcasecmp(name, formats[i].name) == 0) { + *type = formats[i].fmt; + return true; + } + } + return false; +} + +static void safe_write(const char *str) +{ + size_t done, todo; + ssize_t rc; + + if (f.fileno < 0) + return; + for (done = 0; (todo = strlen(&str[done])) > 0; done += (size_t)rc) { + rc = write(f.fileno, &str[done], todo); + if (rc <= 0) + return; + } +} + +static void _indent(unsigned int off, bool safe) +{ + unsigned int num, i; + + if (f.type == FMT_JSONSEQ) + return; + num = f.ind_base + off; + if (f.type == FMT_JSON && f.lvl > 0) + num += f.lvl - 1; + for (i = 0; i < num * f.ind_width; i++) { + if (!safe) { + fputc(f.ind_char, f.fd); + } else if (f.fileno >= 0) { + if (write(f.fileno, &f.ind_char, 1) <= 0) + return; + } + } +} + +#define indent(x) _indent(x, false) + +static void obj_free(struct obj_t *obj) +{ + free(obj->name); + memset(obj, 0, sizeof(*obj)); +} + +static void disable_int(sigset_t *saved) +{ + if (f.handle_int) + sigprocmask(SIG_BLOCK, &no_int_mask, saved); +} + +static void enable_int(sigset_t *saved) +{ + if (f.handle_int) { + /* Ensure latest updates are flushed to file descriptor. */ + fflush(f.fd); + sigprocmask(SIG_SETMASK, saved, NULL); + } +} + +static void int_handler(int signum) +{ + struct sigaction *old; + + if (f.term) + f.term(); + /* Re-install and call original handler. */ + old = (signum == SIGINT) ? &f.old_int : &f.old_term; + sigaction(signum, old, NULL); + raise(signum); +} + +static void setup_int_handler(void) +{ + struct sigaction act; + + memset(&act, 0, sizeof(act)); + act.sa_handler = &int_handler; + sigaction(SIGINT, &act, &f.old_int); + sigaction(SIGTERM, &act, &f.old_term); +} + +static void remove_int_handler(void) +{ + sigaction(SIGINT, &f.old_int, NULL); + sigaction(SIGTERM, &f.old_term, NULL); +} + +void util_fmt_exit(void) +{ + unsigned int i; + + if (f.handle_int) + remove_int_handler(); + if (f.lvl > 0) + fwarn("%s before remaining %d util_obj_end()", __func__, f.lvl); + for (i = 0; i < f.num_keys; i++) + free(f.keys[i].name); + free(f.keys); + for (i = 0; i < f.num_objs; i++) + obj_free(&f.objs[i]); + free(f.objs); + if (f.type == FMT_CSV) + util_rec_free(f.csv_rec); +} + +void util_fmt_set_indent(unsigned int base, unsigned int width, char ind_char) +{ + f.ind_base = base; + f.ind_width = width; + f.ind_char = ind_char; +} + +static unsigned int to_hex(char *str, int val, unsigned int num_digits) +{ + int digit; + char *c; + + for (c = str + num_digits - 1; c >= str; c--) { + digit = (val & 0xf); + val >>= 4; + *c = (char)((digit >= 10) ? digit - 10 + 'a' : digit + '0'); + } + + return num_digits; +} + +static char get_escape(const char *map, char c) +{ + int i; + + for (i = 0; map[i] && map[i + 1]; i += 2) { + if (map[i] == c) + return map[i + 1]; + } + return 0; +} + +struct quote_params { + const char *double_chars; + const char *esc_map; + char hex_char; + unsigned int hex_digits; + unsigned int max_width_per_char; +}; + +static char *do_quote(const char *str, const struct quote_params *p) +{ + unsigned int from, to; + char *q, esc, c; + + /* Start with worst-case length assuming every char is replaced. */ + q = util_zalloc(strlen(str) * p->max_width_per_char + /* "" nul */ 3); + to = 0; + q[to++] = '"'; + for (from = 0; (c = str[from]); from++) { + if (p->double_chars && strchr(p->double_chars, c)) { + /* Escape characters by doubling them ("" in CSV). */ + q[to++] = c; + q[to++] = c; + } else if (p->esc_map && (esc = get_escape(p->esc_map, c))) { + /* Escape characters with backslash + letter. */ + q[to++] = '\\'; + q[to++] = esc; + } else if (p->hex_char && !isprint(c)) { + /* Escape characters with backslash + hex code. */ + q[to++] = '\\'; + q[to++] = p->hex_char; + to += to_hex(&q[to], c, p->hex_digits); + } else { + q[to++] = c; + } + } + q[to++] = '"'; + + return util_realloc(q, (size_t)to + 1); +} + +static char *csv_quote(const char *str) +{ + static const struct quote_params csv_quote_params = { + .double_chars = "\"", + .esc_map = NULL, + .hex_char = 0, + .hex_digits = 0, + .max_width_per_char = 2 /* " => "" */, + }; + + return do_quote(str, &csv_quote_params); +} + +static void add_key(const char *name, bool persist) +{ + struct key_t key; + char *hdr; + + key.name = util_strdup(name); + key.persist = persist; + util_add_array(&f.keys, &f.num_keys, key); + if (f.type == FMT_CSV) { + hdr = csv_quote(name); + util_rec_def(f.csv_rec, name, UTIL_REC_ALIGN_LEFT, 0, hdr); + free(hdr); + util_rec_set(f.csv_rec, name, "\"\""); + f.csv_hdr = true; + } +} + +static struct key_t *get_key(const char *name) +{ + unsigned int i; + + for (i = 0; i < f.num_keys; i++) { + if (strcmp(name, f.keys[i].name) == 0) + return &f.keys[i]; + } + return NULL; +} + +void util_fmt_add_key(const char *fmt, ...) +{ + va_list args; + char *key; + + va_start(args, fmt); + util_vasprintf(&key, fmt, args); + va_end(args); + + /* Only add unique keys. */ + if (!get_key(key)) + add_key(key, true); + free(key); +} + +static bool update_key(const char *name, bool persist) +{ + struct key_t *key; + bool rc = true; + + key = get_key(name); + if (key) { + key->persist = persist; + } else if (!f.do_filter) { + add_key(name, persist); + } else { + fwarn("util_fmt_pair for key '%s' without util_fmt_add_key()", + name); + rc = false; + } + return rc; +} + +static struct obj_t *curr_obj(int off) +{ + int lvl = (int)f.lvl - 1 + off; + + return lvl < 0 ? NULL : &f.objs[lvl]; +} + +static void _util_fmt_obj_end(void); + +/* + * By s390-tools convention, all tool output must be contained in an extra + * top-level object that includes tool-invocation meta-data. + */ +static void emit_meta_object(void) +{ + unsigned int quoted = FMT_PERSIST | FMT_QUOTE, unquoted = FMT_PERSIST; + char hostname[HOST_NAME_MAX + 1] = { 0 }, date[30]; + struct timeval tv; + struct tm *tm; + + f.meta_done = true; + util_fmt_obj_start(FMT_DEFAULT, NULL); + util_fmt_obj_start(FMT_PREFIX, "meta"); + + /* + * "meta": { + * "api_level": 1, + * "version": "2.32.0", + * "host": "localhost", + * "time_epoch": 1714392976, + * "time": "2024-04-29 14:16:16+0200", + * } + */ + util_fmt_pair(unquoted, "api_level", "%d", f.api_level); + util_fmt_pair(quoted, "version", "%s", RELEASE_STRING); + gethostname(hostname, sizeof(hostname) - 1); + util_fmt_pair(quoted, "host", "%s", hostname); + gettimeofday(&tv, NULL); + util_fmt_pair(unquoted, "time_epoch", "%llu", tv.tv_sec); + tm = localtime(&tv.tv_sec); + if (!strftime(date, sizeof(date), "%F %T%z", tm)) + date[0] = 0; + util_fmt_pair(quoted, "time", "%s", date); + _util_fmt_obj_end(); + + if (f.type == FMT_JSONSEQ) { + /* Tool meta-data is a separate object for JSONSEQ. */ + util_fmt_obj_end(); + } +} + +void util_fmt_obj_start(unsigned int oflags, const char *fmt, ...) +{ + struct obj_t *parent, *obj; + char *name = NULL; + sigset_t set; + va_list args; + + if (!f.hide_meta && !f.meta_done && f.lvl == 0) { + emit_meta_object(); + /* + * Allow override of top-level key name for supplementary + * output formats. + */ + if (!fmt) + name = util_strdup(program_invocation_short_name); + } + if (fmt) { + va_start(args, fmt); + util_vasprintf(&name, fmt, args); + va_end(args); + } + f.lvl++; + if (f.lvl > f.num_objs) + util_expand_array(&f.objs, &f.num_objs); + parent = curr_obj(-1); + obj = curr_obj(0); + obj->name = name; + obj->is_list = (oflags & FMT_LIST); + obj->is_row = (oflags & FMT_ROW); + obj->is_prefix = (oflags & FMT_PREFIX); + obj->index = 0; + if (f.obj_start) { + disable_int(&set); + f.obj_start(parent, obj); + enable_int(&set); + } + if (parent) + parent->index++; +} + +static void _util_fmt_obj_end(void) +{ + struct obj_t *obj, *parent; + sigset_t set; + + if (f.lvl == 0) { + fwarn("%s without util_fmt_obj_start", __func__); + return; + } + parent = curr_obj(-1); + obj = curr_obj(0); + if (f.obj_end) { + disable_int(&set); + f.obj_end(parent, obj); + enable_int(&set); + } + f.lvl--; + obj_free(obj); +} + +void util_fmt_obj_end(void) +{ + _util_fmt_obj_end(); + + if (f.lvl == 1 && f.meta_done && f.type != FMT_JSONSEQ) { + /* Emit closure for top-level meta-container object. */ + util_fmt_obj_end(); + } +} + +static char *add_prefix(const char *str, bool full) +{ + struct obj_t *obj; + unsigned int i; + char *prefix; + + prefix = util_strdup(""); + for (i = 0; i < f.lvl; i++) { + obj = &f.objs[i]; + if (!full && !obj->is_prefix) + continue; + if (obj->name) { + if (*prefix) + util_concatf(&prefix, "."); + util_concatf(&prefix, "%s", obj->name); + } + if (obj->is_list && full) + util_concatf(&prefix, "[%d]", obj->index - 1); + } + if (*prefix) + util_concatf(&prefix, "."); + util_concatf(&prefix, "%s", str); + + return prefix; +} + +void util_fmt_pair(unsigned int mflags, const char *key, const char *fmt, ...) +{ + char *val, *prefixed_key; + struct obj_t *obj; + bool is_filtered; + sigset_t set; + va_list args; + + obj = curr_obj(0); + if (!obj) { + fwarn("%s before util_fmt_obj_start", __func__); + return; + } + + /* Filter by key. */ + if (f.do_filter) { + prefixed_key = add_prefix(key, false); + is_filtered = !get_key(prefixed_key); + free(prefixed_key); + if (is_filtered) + return; + } + + /* Filter by validity. */ + if (f.hide_inval && (mflags & FMT_INVAL)) + return; + + va_start(args, fmt); + util_vasprintf(&val, fmt, args); + va_end(args); + + if (f.map) { + disable_int(&set); + f.map(obj, mflags, key, val); + enable_int(&set); + } + obj->index++; + + free(val); +} + +static char *pairs_quote(const char *str) +{ + static const struct quote_params pairs_quote_params = { + .double_chars = NULL, + .esc_map = "\"\"$$``\\\\\aa\bb\ee\ff\nn\rr\tt\vv", + .hex_char = 'x', + .hex_digits = 2, + .max_width_per_char = 4 /* '\x' + 2 hex_digits */, + }; + + return do_quote(str, &pairs_quote_params); +} + +static void pairs_map(struct obj_t *UNUSED(obj), unsigned int mflags, + const char *key, const char *val) +{ + char *full_key, *qval = NULL; + + if (mflags & FMT_INVAL) + val = ""; + indent(0); + if (f.quote_all || (mflags & FMT_QUOTE)) + qval = pairs_quote(val); + if (f.hide_prefix) { + fprintf(f.fd, "%s=%s\n", key, qval ?: val); + } else { + full_key = add_prefix(key, true); + fprintf(f.fd, "%s=%s\n", full_key, qval ?: val); + free(full_key); + } + free(qval); +} + +static char *json_quote(const char *str) +{ + static const struct quote_params json_quote_params = { + .double_chars = NULL, + .esc_map = "\"\"\\\\\bb\ff\nn\rr\tt", + .hex_char = 'u', + .hex_digits = 4, + .max_width_per_char = 6 /* '\u' + 4 hex_digits */, + }; + + return do_quote(str, &json_quote_params); +} + +static void json_obj_start(struct obj_t *parent, struct obj_t *obj) +{ + char *key; + + if (!parent && f.type == FMT_JSONSEQ) { + /* Emit leading record separator according to RFC 7464. */ + fprintf(f.fd, "\x1e"); + } + if (parent && parent->index > 0) + fprintf(f.fd, ",%s", f.nl); + indent(0); + if (parent && !parent->is_list && obj->name) { + key = json_quote(obj->name); + fprintf(f.fd, "%s: ", key); + free(key); + } + fprintf(f.fd, obj->is_list ? "[%s" : "{%s", f.nl); +} + +static void json_obj_end(struct obj_t *parent, struct obj_t *obj) +{ + if (obj->index > 0) + fprintf(f.fd, "%s", f.nl); + indent(0); + fprintf(f.fd, obj->is_list ? "]" : "}"); + if (!parent) + fprintf(f.fd, "\n"); +} + +/* + * Ensure syntactically correct JSON by emitting all pending closure elements. + * Called in signal context - only use signal-safe functions. + */ +static void json_term(void) +{ + struct obj_t *obj, *parent; + + for (; f.lvl > 0; f.lvl--) { + obj = curr_obj(0); + parent = curr_obj(-1); + if (obj->index > 0) + safe_write(f.nl); + _indent(0, true); + safe_write(obj->is_list ? "]" : "}"); + if (!parent) + safe_write(f.nl); + } +} + +static void json_map(struct obj_t *parent, unsigned int mflags, + const char *key, const char *val) +{ + char *qkey, *qval = NULL; + + qkey = json_quote(key); + if (mflags & FMT_INVAL) + qval = util_strdup("null"); + else if (f.quote_all || (mflags & FMT_QUOTE)) + qval = json_quote(val); + if (parent->index > 0) + fprintf(f.fd, ",%s", f.nl); + indent(1); + fprintf(f.fd, "%s: %s", qkey, qval ?: val); + free(qval); + free(qkey); +} + +static void csv_obj_start(struct obj_t *UNUSED(parent), struct obj_t *obj) +{ + if (!obj->is_row) + return; +} + +static void csv_obj_end(struct obj_t *UNUSED(parent), struct obj_t *obj) +{ + unsigned int i; + + if (!(obj->is_row || (f.lvl == 1 && f.csv_data))) + return; + if (f.csv_hdr) { + /* Print row with CSV header. */ + indent(0); + util_rec_print_hdr(f.csv_rec); + f.csv_hdr = false; + } + /* Print row with CSV data. */ + indent(0); + util_rec_print(f.csv_rec); + f.csv_data = false; + /* Reset non-persistent fields. */ + for (i = 0; i < f.num_keys; i++) { + if (!f.keys[i].persist) + util_rec_set(f.csv_rec, f.keys[i].name, "\"\""); + } +} + +static void csv_map(struct obj_t *UNUSED(obj), unsigned int mflags, + const char *key, const char *val) +{ + char *qval = NULL, *prefixed_key; + + /* Use empty string for invalid values. */ + if (mflags & FMT_INVAL) + val = ""; + /* Quote value if requested. */ + if (f.quote_all || (mflags & FMT_QUOTE)) + qval = csv_quote(val); + /* Process key and value. */ + prefixed_key = add_prefix(key, false); + if (update_key(prefixed_key, mflags & FMT_PERSIST)) { + util_rec_set(f.csv_rec, prefixed_key, "%s", qval ?: val); + f.csv_data = true; + } + free(prefixed_key); + free(qval); +} + +void util_fmt_init(FILE *fd, enum util_fmt_t type, unsigned int flags, + int api_level) +{ + memset(&f, 0, sizeof(f)); + f.type = type; + f.fd = fd; + f.fileno = fileno(fd); + f.hide_prefix = (flags & FMT_NOPREFIX); + f.hide_inval = !(flags & FMT_KEEPINVAL); + f.hide_meta = (flags & FMT_NOMETA); + f.quote_all = (flags & FMT_QUOTEALL); + f.do_filter = (flags & FMT_FILTER); + f.do_warn = (flags & FMT_WARN); + f.handle_int = (flags & FMT_HANDLEINT); + f.api_level = api_level; + if (type == FMT_JSONSEQ) + f.nl = ""; + else + f.nl = "\n"; + f.ind_width = 2; + f.ind_char = ' '; + f.meta_done = false; + switch (type) { + case FMT_PAIRS: + f.map = &pairs_map; + break; + case FMT_JSON: + case FMT_JSONSEQ: + f.obj_start = &json_obj_start; + f.obj_end = &json_obj_end; + f.map = &json_map; + f.term = &json_term; + break; + case FMT_CSV: + f.obj_start = &csv_obj_start; + f.obj_end = &csv_obj_end; + f.map = &csv_map; + f.csv_rec = util_rec_new_csv(","); + f.csv_hdr = true; + f.csv_data = false; + break; + } + /* Ensure consistent number format for callers that use setlocale(). */ + setlocale(LC_NUMERIC, "C"); + if (f.handle_int) { + setup_int_handler(); + sigemptyset(&no_int_mask); + sigaddset(&no_int_mask, SIGINT); + sigaddset(&no_int_mask, SIGTERM); + } +} diff --git a/libutil/util_fmt_example.c b/libutil/util_fmt_example.c new file mode 100644 index 00000000..a3f08fc6 --- /dev/null +++ b/libutil/util_fmt_example.c @@ -0,0 +1,251 @@ +/* + * util_fmt_example - Example program for util_fmt + * + * Copyright IBM Corp. 2024 + * + * s390-tools is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + */ + +#include +#include + +#include "lib/util_base.h" +#include "lib/util_fmt.h" + +#define API_LEVEL 1 + +static void meta_example(enum util_fmt_t format) +{ + util_fmt_init(stdout, format, FMT_DEFAULT, API_LEVEL); + + /* + * First call to util_fmt_obj_start() automatically adds meta-data + * object as required by s390-tools convention. + */ + util_fmt_obj_start(FMT_DEFAULT, NULL); + util_fmt_pair(FMT_QUOTE, "key", "value"); + util_fmt_obj_end(); + + util_fmt_exit(); +} + +static void simple_example(enum util_fmt_t format, int fmt_flags) +{ + /* + * Note: Meta-data is excluded in this example for readability but + * must be included in actual tool output. + */ + util_fmt_init(stdout, format, fmt_flags | FMT_NOMETA, API_LEVEL); + + /* + * { + * "child": { + * "key": "value", + * "invalid":"invalidvalue" <== Marked as invalid + * } + * } + */ + util_fmt_obj_start(FMT_DEFAULT, NULL); + util_fmt_obj_start(FMT_DEFAULT, "child"); + util_fmt_pair(FMT_QUOTE, "key", "value"); + util_fmt_pair(FMT_QUOTE | FMT_INVAL, "invalid", "invalidvalue"); + util_fmt_obj_end(); + util_fmt_obj_end(); + + util_fmt_exit(); +} + +static void list_example(enum util_fmt_t format, int flags) +{ + int i; + + /* + * Note: Meta-data is excluded in this example for readability but + * must be included in actual tool output. + */ + util_fmt_init(stdout, format, flags | FMT_NOMETA, API_LEVEL); + + /* + * "cond","key" + * condvalue0,value0 + * "",value1 + * "",value2 + * "",value3 + */ + util_fmt_obj_start(FMT_DEFAULT, NULL); + util_fmt_obj_start(FMT_LIST, "list"); + + for (i = 0; i < 4; i++) { + util_fmt_obj_start(FMT_ROW, NULL); + if (i == 0) + util_fmt_pair(flags, "cond", "condvalue%d", i); + util_fmt_pair(FMT_DEFAULT, "key", "value%d", i); + util_fmt_obj_end(); + } + + util_fmt_obj_end(); + util_fmt_obj_end(); + + util_fmt_exit(); +} + +#define NUM_KEYS 4 + +static void vary_example(enum util_fmt_t format, bool add) +{ + const char *keys[NUM_KEYS] = { "key_a", "key_b", "key_c", "key_d" }; + int i; + + /* + * Note: Meta-data is excluded in this example for readability but + * must be included in actual tool output. + */ + util_fmt_init(stdout, format, FMT_NOMETA, API_LEVEL); + + if (add) { + /* Make keys known before starting output. */ + for (i = 0; i < NUM_KEYS; i++) + util_fmt_add_key(keys[i]); + } + + util_fmt_obj_start(FMT_LIST, "list"); + for (i = 0; i < 4; i++) { + util_fmt_obj_start(FMT_ROW, NULL); + util_fmt_pair(FMT_DEFAULT, keys[i], "value%d", i); + util_fmt_obj_end(); + } + util_fmt_obj_end(); + + util_fmt_exit(); +} + +static void filter_example(enum util_fmt_t format) +{ + /* + * Note: Meta-data is excluded in this example for readability but + * must be included in actual tool output. + */ + util_fmt_init(stdout, format, FMT_FILTER | FMT_NOMETA, API_LEVEL); + util_fmt_add_key("key_a"); + /* + * { + * "key_a": "value_a", + * "key_b": "value_b" <== Not announced via util_fmt_add_key() + * } + */ + util_fmt_obj_start(FMT_DEFAULT, NULL); + util_fmt_pair(FMT_QUOTE, "key_a", "value_a"); + util_fmt_pair(FMT_QUOTE, "key_b", "value_b"); + util_fmt_obj_end(); + + util_fmt_exit(); +} + +static void prefix_example(enum util_fmt_t format, bool do_prefix) +{ + /* + * Note: Meta-data is excluded in this example for readability but + * must be included in actual tool output. + */ + util_fmt_init(stdout, format, FMT_NOMETA, API_LEVEL); + + /* + * { + * "key": "value0", + * "obj1": { // Marked as prefix object + * "key": "value1" + * } + * } + */ + util_fmt_obj_start(FMT_DEFAULT, "obj0"); + util_fmt_pair(FMT_QUOTE, "key", "value0"); + util_fmt_obj_start(do_prefix ? FMT_PREFIX : FMT_DEFAULT, "obj1"); + util_fmt_pair(FMT_QUOTE, "key", "value1"); + util_fmt_obj_end(); + util_fmt_obj_end(); + + util_fmt_exit(); +} + +static void announce(const char *example_name) +{ + static int example_number; + int i; + + if (example_number++ > 0) + printf("\n"); + + printf("%d. %s\n====", example_number, example_name); + for (i = strlen(example_name); i > 0; i--) + printf("="); + printf("\n"); +} + +int main(int UNUSED(argc), char *UNUSED(argv[])) +{ + announce("JSON output"); + simple_example(FMT_JSON, FMT_KEEPINVAL); + + announce("JSON without invalid pairs"); + simple_example(FMT_JSON, FMT_DEFAULT); + + announce("JSON formatted as sequence"); + simple_example(FMT_JSONSEQ, FMT_DEFAULT); + + announce("Pairs output"); + simple_example(FMT_PAIRS, FMT_KEEPINVAL); + + announce("Pairs output without invalid pairs"); + simple_example(FMT_PAIRS, FMT_DEFAULT); + + announce("Pairs without prefix"); + simple_example(FMT_PAIRS, FMT_NOPREFIX); + + announce("CSV output"); + simple_example(FMT_CSV, FMT_KEEPINVAL); + + announce("CSV list output"); + list_example(FMT_CSV, FMT_DEFAULT); + + announce("CSV list with persistent cond value"); + list_example(FMT_CSV, FMT_PERSIST); + + announce("JSON with filtered key"); + filter_example(FMT_JSON); + + announce("Pairs with filtered key"); + filter_example(FMT_PAIRS); + + announce("CSV with filtered key"); + filter_example(FMT_CSV); + + announce("CSV list with varying keys"); + vary_example(FMT_CSV, false); + + announce("CSV list with pre-announced varying keys"); + vary_example(FMT_CSV, true); + + announce("JSON output with meta-data"); + meta_example(FMT_JSON); + + announce("JSON sequence output with meta-data"); + meta_example(FMT_JSONSEQ); + + announce("Pairs output with meta-data"); + meta_example(FMT_PAIRS); + + announce("CSV output with meta-data"); + meta_example(FMT_CSV); + + announce("JSON output with duplicate keys"); + prefix_example(FMT_JSON, false); + + announce("CSV output with duplicate keys"); + prefix_example(FMT_CSV, false); + + announce("CSV output with duplicate keys distinguished by prefix"); + prefix_example(FMT_CSV, true); + + return 0; +}