libutil: add output format helpers

Add helper functions for converting structured key-value data into
different formats such as JSON, text pairs, and CSV.

Using these functions the resulting output format can be dynamically
configured at run-time without the need to duplicate output-generating
code for each format type. Also format-specific requirements such as
quoting, indentation, and comma-placement are automatically taken care
of.

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 most recent object or list
 util_fmt_exit()      => Cleanup

Notes:
 - Supported data elements are objects, lists and key-value pairs
   (mappings)
 - Scalars are only supported as part of a mapping

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Peter Oberparleiter <oberpar@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
This commit is contained in:
Peter Oberparleiter
2024-02-05 14:40:25 +01:00
committed by Steffen Eiden
parent d8e5bc07aa
commit 38ea8fc3ee
3 changed files with 1249 additions and 0 deletions

235
include/lib/util_fmt.h Normal file
View File

@@ -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 <stdbool.h>
#include <stdio.h>
/* 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.
* <name>: { }
* - 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 */

763
libutil/util_fmt.c Normal file
View File

@@ -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 <ctype.h>
#include <err.h>
#include <errno.h>
#include <limits.h>
#include <locale.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#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);
}
}

251
libutil/util_fmt_example.c Normal file
View File

@@ -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 <stdlib.h>
#include <string.h>
#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;
}