mirror of
https://github.com/ibm-s390-linux/s390-tools.git
synced 2026-08-05 02:14:52 +00:00
This patch modifies the default behavior of CSV header generation: header cells are no longer enclosed in double quotes unless the FMT_QUOTEALL flag is explicitly set. According to RFC 4180, quoting is only required when a cell contains control characters, commas, or double quotes. The goal of this change is to produce cleaner and more readable CSV output by default, and to avoid unnecessary quoting in header rows. It also simplifies algorithms that rely on FMT_UTIL and improves compatibility with downstream tools expecting unquoted headers. Tools that expect unquoted headers include: * SQL tools: Headers optional, usually unquoted * R (read.csv): Uses header=TRUE, no quotes needed * Pandas: Assumes headers, quoting not required * Excel: Detects headers; quotes only for special chars Signed-off-by: Jan Polensky <japo@linux.ibm.com> Reviewed-by: Peter Oberparleiter <oberpar@linux.ibm.com> Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
776 lines
16 KiB
C
776 lines
16 KiB
C
/*
|
|
* 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) {
|
|
if (f.quote_all) {
|
|
hdr = csv_quote(name);
|
|
util_rec_def(f.csv_rec, name, UTIL_REC_ALIGN_LEFT, 0, hdr);
|
|
free(hdr);
|
|
} else {
|
|
util_rec_def(f.csv_rec, name, UTIL_REC_ALIGN_LEFT, 0, name);
|
|
}
|
|
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);
|
|
}
|
|
|
|
static bool hide_meta_env(void)
|
|
{
|
|
char *v;
|
|
|
|
v = secure_getenv("FMT_NOMETA");
|
|
return (v && strcmp(v, "1") == 0);
|
|
}
|
|
|
|
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) || hide_meta_env();
|
|
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);
|
|
}
|
|
}
|