/*
 * chpstat - Tool to display channel-path statistics
 *
 * 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 <fcntl.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#include "cmg.h"
#include "column.h"
#include "key.h"
#include "misc.h"

#include "lib/util_base.h"
#include "lib/util_file.h"
#include "lib/util_fmt.h"
#include "lib/util_libc.h"
#include "lib/util_opt.h"
#include "lib/util_path.h"
#include "lib/util_prg.h"
#include "lib/util_rec.h"
#include "lib/zt_common.h"

/* Output format API level. */
#define API_LEVEL		1

/* Default interval in seconds between output. */
#define DEFAULT_INTERVAL	5

/* Maximum interval in seconds after which timestamps wrap twice (approx.). */
#define MAX_INTERVAL		2140

/* Maximum number of CHPIDS. */
#define NUM_CHPIDS		256

/* Highest valid CHPID number. */
#define MAX_CHPID		255

/* Sysfs paths. */
#define SYS_CSS0	"devices/css0"
#define SYS_CM_ENABLE	SYS_CSS0 "/cm_enable"

/* ANSI X3.64 terminal control codes. */
#define ANSI_CLS		"\e[2J"
#define ANSI_LOCATE(x, y)	"\e[" STRINGIFY(x) ";" STRINGIFY(y) "H"
#define ANSI_BOLD		"\e[1m"
#define ANSI_REVERSE		"\e[7m"
#define ANSI_RESET		"\e[0m"

#define UNIT_DEC	1000UL
#define UNIT_BIN	1024UL
#define UNIT_AUTO	0

#define strlen_i(a)	((int)strlen(a))
#define strlen_u(a)	((unsigned int)strlen(a))

/* Channel-path measurement facility status values. */
enum cm_status_t {
	CM_UNSUPPORTED,
	CM_DISABLED,
	CM_ENABLED,
};

/* Program information. */
static const struct util_prg prg = {
	.desc = "Use chpstat to view channel-path statistics such as "
		"utilization and throughput, and to query and control the "
		"status of the channel-path statistics function.\n"
		"\n"
		"When run without further options, data for all channel-paths "
		"is displayed repeatedly with a " STRINGIFY(DEFAULT_INTERVAL)
		" second delay in table format. You can limit output to "
		"specific channel-paths by listing the associated CHPIDs on "
		"the command line.",
	.copyright_vec = {
		{
			.owner     = "IBM Corp.",
			.pub_first = 2024,
			.pub_last  = 2024,
		},
		UTIL_PRG_COPYRIGHT_END
	},
	.args = "[CHPIDS] [ACTIONS]",
};

enum {
	OPT_STATUS       = 's',
	OPT_ENABLE       = 'e',
	OPT_DISABLE      = 'd',
	OPT_LIST_COLUMNS = 'l',
	OPT_LIST_KEYS    = 'L',
	OPT_ITERATIONS   = 'n',
	OPT_INTERVAL     = 'i',
	OPT_COLUMNS      = 'c',
	OPT_KEYS         = 'k',
	OPT_ALL          = 'a',
	/* Options without short version below. */
	OPT_FORMAT       = 0x80, /* First non-printable character. */
	OPT_CHARS,
	OPT_UTIL,
	OPT_METRICS,
	OPT_CMG,
	OPT_SCALE,
	OPT_NO_ANSI,
	OPT_NO_PREFIX,
	OPT_DEBUG,
};

enum command_t {
	CMD_ENABLE       = OPT_ENABLE,
	CMD_DISABLE      = OPT_DISABLE,
	CMD_STATUS       = OPT_STATUS,
	CMD_LIST_COLUMNS = OPT_LIST_COLUMNS,
	CMD_LIST_KEYS    = OPT_LIST_KEYS,
	CMD_TABLE,
	CMD_LIST,
};

/*
 * Configuration of command line options
 */
static struct util_opt opt_vec[] = {
	{
		.desc = "ACTIONS",
		.flags = UTIL_OPT_FLAG_SECTION,
	},
	{
		.option = { "status", no_argument, NULL, OPT_STATUS },
		.desc = "Show channel-path statistics status",
	},
	{
		.option = { "enable", no_argument, NULL, OPT_ENABLE },
		.desc = "Enable channel-path statistics",
	},
	{
		.option = { "disable", no_argument, NULL, OPT_DISABLE },
		.desc = "Disable channel-path statistics",
	},
	{
		.option = { "list-columns", no_argument, NULL,
			    OPT_LIST_COLUMNS},
		.desc = "List available table columns",
	},
	{
		.option = { "list-keys", no_argument, NULL, OPT_LIST_KEYS},
		.desc = "List available data keys",
	},
	UTIL_OPT_HELP,
	UTIL_OPT_VERSION,
	{
		.desc = "OPTIONS",
		.flags = UTIL_OPT_FLAG_SECTION,
	},
	{
		.option = { "iterations", required_argument, NULL,
			    OPT_ITERATIONS },
		.argument = "NUM",
		.desc = "Display NUM reports before ending (0 for no end)",
	},
	{
		.option = { "interval", required_argument, NULL,
			    OPT_INTERVAL },
		.argument = "NUM",
		.desc = "Pause NUM seconds between display",
	},
	{
		.option = { "columns", required_argument, NULL, OPT_COLUMNS },
		.argument = "COL,..",
		.desc = "Show only specified columns in table output",
	},
	{
		.option = { "keys", required_argument, NULL, OPT_KEYS },
		.argument = "KEY,..",
		.desc = "Show only data for specified keys in list output",
	},
	{
		.option = { "all", no_argument, NULL, OPT_ALL },
		.desc = "Show all table columns and key data",
	},
	{
		.option = { "scale", required_argument, NULL, OPT_SCALE },
		.argument = "UNIT",
		.flags = UTIL_OPT_FLAG_NOSHORT,
		.desc = "Scale BPS values by UNIT (number, suffix or auto)",
	},
	{
		.option = { "cmg", required_argument, NULL, OPT_CMG },
		.argument = "CMG,..",
		.flags = UTIL_OPT_FLAG_NOSHORT,
		.desc = "Show data for specified CMGs only",
	},
	{
		.option = { "format", required_argument, NULL, OPT_FORMAT },
		.argument = "FORMAT",
		.flags = UTIL_OPT_FLAG_NOSHORT,
		.desc = "List data in specified FORMAT (" FMT_TYPE_NAMES ")",
	},
	{
		.option = { "chars", no_argument, NULL, OPT_CHARS },
		.flags = UTIL_OPT_FLAG_NOSHORT,
		.desc = "List channel-path measurement characteristics",
	},
	{
		.option = { "util", no_argument, NULL, OPT_UTIL },
		.flags = UTIL_OPT_FLAG_NOSHORT,
		.desc = "List unprocessed utilization data",
	},
	{
		.option = { "metrics", no_argument, NULL, OPT_METRICS },
		.flags = UTIL_OPT_FLAG_NOSHORT,
		.desc = "List performance metrics",
	},
	{
		.option = { "no-ansi", no_argument, NULL, OPT_NO_ANSI },
		.flags = UTIL_OPT_FLAG_NOSHORT,
		.desc = "Do not use ANSI terminal codes in output",
	},
	{
		.option = { "no-prefix", no_argument, NULL, OPT_NO_PREFIX },
		.flags = UTIL_OPT_FLAG_NOSHORT,
		.desc = "Hide key prefix in pairs output format",
	},
	{
		.option = { "debug", no_argument, NULL, OPT_DEBUG },
		.flags = UTIL_OPT_FLAG_NOSHORT,
		.desc = "Print debugging information",
	},
	UTIL_OPT_END
};

/* Settings from command line options. */
static struct {
	bool cmd_specified;
	enum command_t cmd;
	int iterations;
	bool forever;
	int interval;
	int groups;
	bool groups_specified;
	bool all;
	bool use_ansi;
	bool use_prefix;
	bool debug;
	enum util_fmt_t fmt;
	bool fmt_specified;
	bool cmgs_specified;
	bool columns_specified;
	bool keys_specified;
	unsigned long unit;
	char unit_suffix;
	bool unit_specified;
	bool unit_iec;
} opts;

/* Per CHPID run-time data. */
static struct chpid_data_t {
	int id;
	bool selected;
	struct cmg_t *cmg;
	int type;
	int shared;
	char *speed;
	struct cmg_data_t data;
} chpid_data[NUM_CHPIDS];

#define chpid_for_each(c) \
	for (int __i = 0; __i < NUM_CHPIDS && ((c) = &chpid_data[__i]); __i++)

/*
 * Buffer should be large enough to contain a full table of output columns for
 * up to 256 CHPIDs.
 */
#define OUTPUT_BUFFER_SIZE	131072

/*
 * Make stdout fully buffered to enable writing a full table to terminal in a
 * single flush write. This helps reduce visual flicker on output terminals.
 */
static void buffer_stdout(bool on)
{
	static void *buffer;

	if (on) {
		free(buffer);
		buffer = util_zalloc(OUTPUT_BUFFER_SIZE);
		setvbuf(stdout, buffer, _IOFBF, OUTPUT_BUFFER_SIZE);
	} else {
		setlinebuf(stdout);
		free(buffer);
		buffer = NULL;
	}
}

/*
 * Emit ANSI terminal control @codes depending on the global use_ansi option.
 */
static void ansi(const char *codes)
{
	if (opts.use_ansi)
		printf("%s", codes);
}

/*
 * Initialize default settings.
 */
static void init_opts(void)
{
	opts.cmd            = CMD_TABLE;
	opts.cmd_specified  = false;
	opts.iterations     = -1;
	opts.forever        = true;
	opts.interval       = DEFAULT_INTERVAL;
	opts.groups         = 0;
	opts.groups_specified = false;
	opts.all            = false;
	opts.use_ansi       = isatty(STDOUT_FILENO);
	opts.use_prefix     = true;
	opts.fmt            = FMT_JSON;
	opts.fmt_specified  = false;
	opts.cmgs_specified = false;
	opts.unit           = UNIT_AUTO;
	opts.unit_suffix    = 0;
	opts.unit_specified = false;
	opts.unit_iec       = false;
}

/*
 * Initialize chpid data array.
 */
static void init_chpid_data(void)
{
	int chpid;

	for (chpid = 0; chpid < NUM_CHPIDS; chpid++) {
		memset(&chpid_data[chpid], 0, sizeof(chpid_data[chpid]));
		chpid_data[chpid].id = chpid;
	}
}

/*
 * Release memory used by chpid data array.
 */
static void free_chpid_data(void)
{
	int chpid;

	for (chpid = 0; chpid < NUM_CHPIDS; chpid++)
		free(chpid_data[chpid].speed);
}

/*
 * Parse a command line argument @arg that was specified for command line
 * option @name as an integer and check if the result is between @min and @max.
 *
 * Return the resulting integer on success. On failure, exit with an
 * appropriate error message.
 */
static int parse_int(const char *name, const char *arg, long min, long max)
{
	char *endptr;
	long v;

	v = strtol(arg, &endptr, 10);
	if (*endptr || !*arg) {
		errx(EXIT_USAGE, "Value for option --%s is invalid: %s",
		     name, arg);
	}
	if (v < min) {
		errx(EXIT_USAGE, "Value %s for option --%s is too small "
		     "(min %ld)", arg, name, min);
	}
	if (v > max) {
		errx(EXIT_USAGE, "Value %s for option --%s is too large "
		     "(max %ld)", arg, name, max);
	}
	return (int)v;
}

/*
 * Parse a command line argument @arg as CHPID. Return the CHPID on success.
 * On failure, exit with an error message.
 */
static int parse_chpid(const char *arg)
{
	int id;
	char c;

	if ((sscanf(arg, "%x %c", &id, &c) == 1 ||
	     sscanf(arg, "0.%x %c", &id, &c) == 1) && id <= MAX_CHPID)
		return id;

	errx(EXIT_USAGE, "Invalid CHPID '%s'", arg);
}

/*
 * Parse the comma-separated list of column names in @arg and select the
 * associated columns.
 */
static void parse_columns(char *arg)
{
	struct column_t *col;
	char *name;

	while ((name = strsep(&arg, ","))) {
		col = column_get_by_name(name);
		if (!col)
			errx(EXIT_USAGE, "Unknown column name '%s'", name);
		column_select(col);
	}
}

/*
 * Parse the comma-separated list of key names in @arg and add it to the
 * list of specified keys.
 */
static void parse_keys(char *arg)
{
	struct key_t *key;
	char *name;

	while ((name = strsep(&arg, ","))) {
		key = key_get_by_name(name);
		if (!key)
			errx(EXIT_USAGE, "Unknown key '%s'", name);
		key_select(key);
	}
}

/*
 * Parse the comma-separated list of CMG values in @arg and select the
 * associated CMG.
 */
static void parse_cmgs(char *arg)
{
	struct cmg_t *cmg_t;
	char *name;
	int cmg;

	while ((name = strsep(&arg, ","))) {
		cmg = parse_int("cmg", name, 1, 255);
		cmg_t = cmg_get(cmg);
		if (!cmg_t)
			errx(EXIT_USAGE, "Unsupported CMG '%s'", name);
		cmg_t->selected = true;
	}
}

static bool suffix_to_unit(char *arg, unsigned long *unit_ptr, char *suffix_ptr,
			   bool *iec_ptr)
{
	const char *suffixes = "KMGT";
	unsigned long unit, base;
	size_t len = strlen(arg);
	char suffix;
	bool iec;
	int i;

	if (len == 1)
		iec = false;
	else if (len == 2 && toupper(arg[1]) == 'I')
		iec = true;
	else
		return false;
	suffix = (char)toupper(*arg);
	base = iec ? UNIT_BIN : UNIT_DEC;
	unit = base;
	for (i = 0; suffixes[i]; i++) {
		if (suffix == suffixes[i]) {
			*unit_ptr = unit;
			*suffix_ptr = suffix;
			*iec_ptr = iec;
			return true;
		}
		unit *= base;
	}
	return false;
}

/*
 * Parse a scale unit value in @arg and return the resulting scaling factor.
 */
static unsigned long parse_unit(char *arg)
{
	unsigned long unit;
	char *endptr;

	if (suffix_to_unit(arg, &unit, &opts.unit_suffix, &opts.unit_iec))
		return unit;
	if (strcmp(arg, "auto-iec") == 0) {
		opts.unit_iec = true;
		return UNIT_AUTO;
	}
	if (strcmp(arg, "auto") == 0)
		return UNIT_AUTO;
	/* Parse as number. */
	unit = strtoul(arg, &endptr, 10);
	if (!*endptr && *arg && unit > 0 &&
	    !(unit == ULONG_MAX && errno == ERANGE)) {
		return unit;
	}

	errx(EXIT_USAGE, "Unsupported scaling unit '%s'", optarg);
}

/*
 * Determine the current status of the channel-path measurement facility.
 */
static enum cm_status_t get_cm_status(void)
{
	enum cm_status_t result;
	char *path;
	int i;

	path = util_path_sysfs(SYS_CM_ENABLE);
	if (!util_path_exists(path)) {
		result = CM_UNSUPPORTED;
		goto out;
	}
	if (util_file_read_i(&i, 10, path))
		errx(EXIT_RUNTIME, "Unable to read file '%s'", path);
	if (i == 0)
		result = CM_DISABLED;
	else
		result = CM_ENABLED;
out:
	free(path);

	return result;
}

/*
 * Change the channel-path facility status to the value represented by @on.
 */
static void set_cm_enable(long on)
{
	char *path;

	if (on)
		printf("Enabling channel-path statistics\n");
	else
		printf("Disabling channel-path statistics\n");

	path = util_path_sysfs(SYS_CM_ENABLE);
	if (util_file_write_l(on, 10, "%s", path) == 0) {
		free(path);
		return;
	}

	switch (errno) {
	case EIO:
		errx(EXIT_RUNTIME, "Unable to enable channel-path statistics\n"
		     "Check if your system is authorized (see section "
		     "AUTHORIZATION in 'man %s')",
		     program_invocation_short_name);
		break;
	default:
		err(EXIT_RUNTIME, "Unable to write to file '%s'", path);
	}
}

/*
 * Get sysfs path for attribute @filename of CHPID @chpid. If @filename is
 * not specified, return path to CHPID sysfs directory.
 */
static char *get_chpid_path(int chpid, const char *filename)
{
	return util_path_sysfs(SYS_CSS0 "/chp0.%02x%s%s", chpid,
			       filename ? "/" : "", filename ? filename : "");
}

/*
 * Check if CHPID @chpid exists.
 */
static bool chpid_exists(int chpid)
{
	char *path;
	bool rc;

	path = get_chpid_path(chpid, NULL);
	rc = util_path_exists("%s", path);
	free(path);

	return rc;
}

/*
 * Print debugging information related to reading file @path.
 */
static void debug_text(const char *path, const char *txt)
{
	if (!opts.debug)
		return;
	if (txt)
		printf("DEBUG: read(%s)='%s'\n", path, txt);
	else
		printf("DEBUG: read(%s)=- errno=%d\n", path, errno);
}

/*
 * Print debugging information related to reading file @path.
 */
static void debug_bin(const char *path, void *buffer, size_t expect, ssize_t rc)
{
	if (!opts.debug)
		return;
	printf("DEBUG: read(%s)=%d/%d\n", path, (int)rc, (int)expect);
	util_hexdump_grp(stdout, "DEBUG", buffer, 4, (int)rc, 0);
}

static char *read_chpid_attr_as_text(int chpid, const char *name, bool try)
{
	char *path, *value = NULL;

	path = get_chpid_path(chpid, name);
	if (try && !util_path_exists(path))
		goto out;
	value = util_file_read_text_file(path, true);
	debug_text(path, value);
	if (!value && !try)
		err(EXIT_RUNTIME, "Unable to read file '%s'", path);
out:
	free(path);

	return value;
}

/*
 * Read and return the value of the cmg sysfs attribute for CHPID @chpid.
 * Return %-1 if the CMG value is "unknown".
 */
static int read_cmg(int chpid)
{
	char *value;
	int cmg;

	value = read_chpid_attr_as_text(chpid, "cmg", false);
	if (strcmp(value, "unknown") == 0)
		cmg =  -1;
	else
		cmg = atoi(value);
	free(value);

	return cmg;
}

/*
 * Read and return the integer value of sysfs attribute @name for CHPID @chpid.
 * The integer is interpreted using base @base.
 */
static int read_chpid_attr_as_int(int chpid, const char *name, int base)
{
	char *value;
	long i;

	value = read_chpid_attr_as_text(chpid, name, false);
	i = strtol(value, NULL, base);
	free(value);

	return (int)i;
}

/*
 * Read and return the value of sysfs attribute speed_bps for CHPID @chpid.
 * Return "-" if the attribute cannot be read, or if the value is unavailable.
 */
static char *read_speed(int chpid)
{
	char *value;

	value = read_chpid_attr_as_text(chpid, "speed_bps", true);
	/* Unavailable speed value is reported as "0". */
	if (value && strcmp(value, "0") == 0) {
		free(value);
		value = NULL;
	}

	return value;
}

/* Read @count bytes of binary data from file @path to @buffer. On success,
 * return %true. If @allow_eof is %true, return %false if not all data could
 * be read. Otherwise exit with an error message.
 */
static bool read_bin(const char *path, void *buffer, size_t count,
		     bool allow_eof)
{
	int fd;
	ssize_t rc = -1;

	fd = open(path, O_RDONLY);
	if (fd == -1)
		goto err;
	rc = read(fd, buffer, count);
	debug_bin(path, buffer, count, rc);
	if (rc < 0)
		goto err;
	if ((size_t)rc < count && !allow_eof)
		goto err_eof;
	close(fd);

	return (size_t)rc == count;

err_eof:
	errx(EXIT_RUNTIME, "Unable to read file '%s': Unexpected end of file",
	     path);
err:
	err(EXIT_RUNTIME, "Unable to read file '%s'", path);
}

/*
 * Read the channel-measurements characteristics block for CHPID @chpid to
 * @cmcb. Return %true if full CMCB was read, %false otherwise.
 */
static bool read_cmcb(int chpid, cmcb_t *cmcb)
{
	bool full = false;
	char *path;

	memset(cmcb, 0, sizeof(*cmcb));
	path = get_chpid_path(chpid, "measurement_chars_full");
	if (util_path_exists(path)) {
		/* Full CMCB starts at offset 0. */
		read_bin(path, cmcb, CMCB_SIZE, false);
		full = true;
	} else {
		free(path);
		path = get_chpid_path(chpid, "measurement_chars");
		/* Partial CMCB contains data starting at word 3. */
		read_bin(path, (char *)cmcb + PARTIAL_CMCB_OFFSET,
			 PARTIAL_CMCB_SIZE, false);
	}
	free(path);

	return full;
}

/*
 * Read the channel-utilization entry for CHPID @chpid to @cue.
 */
static void read_cue(int chpid, int cmg, cue_t *cue)
{
	char *path;

	memset(cue, 0, sizeof(*cue));
	path = get_chpid_path(chpid, "measurement");
	read_bin(path, cue, sizeof(*cue), false);
	free(path);

	/* Some firmware levels incorrectly report a CUIV of 0xfe for CMG 3 -
	 * fix this here to enable access to the full CUE data. */
	if (cmg == 3 && cue->common.cuiv == 0xfe) {
		if (opts.debug)
			printf("DEBUG: Fixing CMG 3 CUIV (0xfe => 0xff)\n");
		cue->common.cuiv = 0xff;
	}
}

/*
 * Read the extended channel-utilization entry for CHPID @chpid to @ext_cue.
 */
static bool read_ext_cue(int chpid, ext_cue_t *ext_cue)
{
	static bool once;
	bool e = false;
	char *path;

	memset(ext_cue, 0, sizeof(*ext_cue));
	path = get_chpid_path(chpid, "ext_measurement");
	/* Older kernels might not have extended measurement support. */
	if (util_path_exists(path)) {
		e = read_bin(path, ext_cue, sizeof(*ext_cue), true);
	} else if (!once) {
		warnx("Missing kernel support for extended channel-path "
		      "measurement data");
		once = true;
	}
	free(path);

	return e;
}

static bool cue_modified(cue_t *a, cue_t *b)
{
	return a->common.timestamp != b->common.timestamp;
}

/*
 * Read utilization data @util for @chpid with CMG @cmg.
 */
static void read_util(int chpid, int cmg, struct util_t *util)
{
	bool repeat = false, *e = &util->extended;
	ext_cue_t *ext_cue = &util->ext_cue;
	cue_t cue2, *cue = &util->cue;

	/* Loop to ensure that basic and extended CUEs are in sync. */
	do {
		read_cue(chpid, cmg, cue);
		*e = read_ext_cue(chpid, ext_cue);
		if (*e) {
			/* Re-read CUE to make sure it hasn't changed. */
			read_cue(chpid, cmg, &cue2);
			repeat = cue_modified(cue, &cue2);
		}
	} while (repeat);
}

/*
 * Select @chpid and read initial CHPID data from sysfs. If @try is %true,
 * return %true if CHPID is available, %false otherwise. If @try is %false,
 * exit with an error message if CHPID is unavailable.
 */
static bool select_chpid(int chpid, bool try)
{
	struct chpid_data_t *c = &chpid_data[chpid];
	struct cmg_t *cmg_t;
	int cmg;

	if (!chpid_exists(chpid)) {
		if (try)
			return false;
		errx(EXIT_RUNTIME, "CHPID 0.%02x does not exist", chpid);
	}
	cmg = read_cmg(chpid);
	if (cmg == -1) {
		if (try)
			return false;
		errx(EXIT_RUNTIME, "No statistics available for CHPID 0.%02x",
		     chpid);
	}
	cmg_t = cmg_get(cmg);
	if (!cmg_t) {
		if (try)
			return false;
		errx(EXIT_RUNTIME, "CHPID 0.%02x uses unsupported CMG %d",
		     chpid, cmg);
	}
	if (opts.cmgs_specified && !cmg_t->selected) {
		if (try)
			return false;
		errx(EXIT_RUNTIME, "CHPID 0.%02x excluded by --cmg option",
		     chpid);
	}

	cmg_t->found++;
	c->cmg        = cmg_t;
	c->type       = read_chpid_attr_as_int(chpid, "type", 16);
	c->shared     = read_chpid_attr_as_int(chpid, "shared", 10);
	c->speed      = read_speed(chpid);
	if (cmg_t->has_cmcb)
		c->data.full_cmcb = read_cmcb(chpid, &c->data.cmcb);
	read_util(chpid, cmg, &c->data.util_a);
	c->data.util_b = c->data.util_a;
	c->selected   = true;

	return true;
}

/*
 * Select all available CHPIDs.
 */
static int select_all_chpids(void)
{
	int chpid, num_selected = 0;

	for (chpid = 0; chpid < NUM_CHPIDS; chpid++) {
		if (select_chpid(chpid, true))
			num_selected++;
	}

	return num_selected;
}

/*
 * Parse all positional parameters in @argv starting with @first up to @argc
 * and select all specified CHPIDs.
 */
static int parse_chpids(int first, int argc, char *argv[])
{
	int i, chpid, num_selected = 0;

	/* Parse optional CHPID selection. */
	for (i = first; i < argc; i++) {
		chpid = parse_chpid(argv[i]);
		if (chpid_data[chpid].selected)
			continue;
		select_chpid(chpid, false);
		num_selected++;
	}

	return num_selected;
}

/*
 * Enable channel-path statistics.
 */
static void cmd_enable(enum cm_status_t status)
{
	if (status == CM_ENABLED)
		printf("Channel-path statistics already enabled\n");
	else
		set_cm_enable(1);
}

/*
 * Disable channel-path statistics.
 */
static void cmd_disable(enum cm_status_t status)
{
	if (status == CM_DISABLED)
		printf("Channel-path statistics already disabled\n");
	else
		set_cm_enable(0);
}

/* pr_pair(key, fmt) */
#define pr_pair(k, fmt, ...) \
	util_fmt_pair(FMT_PERSIST, (k), (fmt), ##__VA_ARGS__)
/* pr_pair_quoted(key, fmt) */
#define pr_pair_quoted(k, fmt, ...) \
	util_fmt_pair(FMT_PERSIST | FMT_QUOTE, (k), (fmt), ##__VA_ARGS__)

/*
 * Show status of the channel-path statistics function in human-readable form.
 */
static void cmd_status_default(enum cm_status_t status)
{
	printf("Channel-path statistics are ");
	switch (status) {
	case CM_UNSUPPORTED:
		printf("not supported on this system\n");
		break;
	case CM_DISABLED:
		printf("disabled\n");
		break;
	case CM_ENABLED:
		printf("enabled\n");
		break;
	}
}

/*
 * Show status of the channel-path statistics function in machine-readable
 * format.
 */
static void cmd_status_fmt(enum cm_status_t status)
{
	const char *str = "";

	switch (status) {
	case CM_UNSUPPORTED:
		str = "unsupported";
		break;
	case CM_DISABLED:
		str = "disabled";
		break;
	case CM_ENABLED:
		str = "enabled";
		break;
	}

	util_fmt_add_key("status");
	util_fmt_obj_start(FMT_ROW, "chpstat_status");
	pr_pair_quoted("status", "%s", str);
	util_fmt_obj_end();
}

/*
 * Show status of the channel-path statistics function.
 */
static void cmd_status(enum cm_status_t status)
{
	if (opts.fmt_specified)
		cmd_status_fmt(status);
	else
		cmd_status_default(status);
}

/*
 * Update channel-utilization data for CHPID @c. Return %true if new data was
 * found, %false otherwise.
 */
static bool _update_util(struct chpid_data_t *c)
{
	struct util_t util;

	read_util(c->id, c->cmg->cmg, &util);
	if (!cue_modified(&util.cue, &c->data.util_b.cue))
		return false;

	c->data.util_a = c->data.util_b;
	c->data.util_b = util;

	return true;
}

/*
 * Update channel-utilization data for CHPID @c. If @wait is %true, repeat
 * update attempts every second until new data is available.
 */
static void update_util(struct chpid_data_t *c, bool wait)
{
	while (!_update_util(c) && wait)
		sleep(1);
}

/*
 * Update channel-path utilization data for all CHPIDs. If @wait is %true,
 * repeat the process until all CHPIDs have new data.
 */
static void update_util_all(bool wait)
{
	struct chpid_data_t *c;

	chpid_for_each(c) {
		if (c->selected)
			update_util(c, wait);
	}
}

/*
 * Return hostname of local host.
 */
static char *get_host_name(void)
{
	char host[HOST_NAME_MAX + 1] = { 0 }, *d;

	gethostname(host, sizeof(host) - 1);
	d = strchr(host, '.');
	if (d)
		*d = 0;
	if (!host[0])
		strncpy(host, "-", sizeof(host) - 1);

	return util_strdup(host);
}

/*
 * Print a header with iteration-specific information for iteration @iteration.
 */
static void pr_iteration_header(int iteration)
{
	unsigned int quoted = FMT_PERSIST | FMT_QUOTE, unquoted = FMT_PERSIST;
	char str[30], *host, *b = "", *r = "";
	struct timeval tv;
	struct tm *tm;

	if (opts.use_ansi) {
		b = ANSI_BOLD;
		r = ANSI_RESET;
	}

	gettimeofday(&tv, NULL);
	tm = localtime(&tv.tv_sec);
	host = get_host_name();

	if (opts.cmd == CMD_TABLE) {
		printf("%sIteration:%s %d  ", b, r, iteration);

		if (tm) {
			strftime(str, sizeof(str), "%F", tm);
			printf("%sDate:%s %s  ", b, r, str);
			strftime(str, sizeof(str), "%T%z", tm);
			printf("%sTime:%s %s  ", b, r, str);
		}
		printf("%sHost:%s %s\n", b, r, host);
	} else {
		util_fmt_pair(unquoted, KEY_ITERATION, "%d", iteration);
		util_fmt_pair(unquoted, KEY_TIME_EPOCH, "%llu", tv.tv_sec);
		if (tm) {
			strftime(str, sizeof(str), "%F %T%z", tm);
			util_fmt_pair(quoted, KEY_TIME, "%s", str);
		} else {
			util_fmt_pair(quoted | FMT_INVAL, KEY_TIME, "", str);
		}
	}

	free(host);
}

/*
 * Print generic, non-cmg specific data for CHPID @c.
 */
static void pr_chpid(struct chpid_data_t *c)
{
	pr_pair_quoted(KEY_CHPID, "0.%02x", c->id);
	pr_pair(KEY_TYPE, "%d", c->type);
	pr_pair(KEY_CMG, "%d", c->cmg->cmg);
	pr_pair(KEY_SHARED, "%d", c->shared);
	if (c->speed) {
		pr_pair_quoted(KEY_SPEED, "%s", c->speed);
	} else {
		util_fmt_pair(FMT_PERSIST | FMT_QUOTE | FMT_INVAL, KEY_SPEED,
			      "");
	}
}

static void pr_cmg_inval_pair(struct cmg_pair_t *p)
{
	if (p->type == CMG_FLOAT)
		util_fmt_pair(FMT_INVAL, p->key, "-1.0");
	else
		util_fmt_pair(FMT_INVAL, p->key, "-1");
}

static void pr_cmg_pair(struct cmg_pair_t *p)
{
	if (!p->valid) {
		pr_cmg_inval_pair(p);
		return;
	}

	switch (p->type) {
	case CMG_U32:
		util_fmt_pair(FMT_DEFAULT, p->key, "%u", p->value_u32);
		break;
	case CMG_U64:
		util_fmt_pair(FMT_DEFAULT, p->key, "%llu", p->value_u64);
		break;
	case CMG_FLOAT:
		util_fmt_pair(FMT_DEFAULT, p->key, "%.1f", p->value_double);
		break;
	}
}

static void pr_cmg_pairs(struct cmg_pair_t *pairs)
{
	int i;

	for (i = 0; pairs[i].key; i++)
		pr_cmg_pair(&pairs[i]);
}

static void pr_group(struct chpid_data_t *c, struct cmg_t *cmg,
		     enum key_group_t group)
{
	struct cmg_pair_t *pairs = NULL;

	pairs = cmg->get_values(&c->data, group);
	util_fmt_obj_start(FMT_DEFAULT, key_group_to_str(group));
	pr_cmg_pairs(pairs);
	util_fmt_obj_end();
	cmg_free_pairs(pairs);
}

/*
 * Show list of channel-path statistics once.
 */
static void cmd_list_once(void)
{
	struct chpid_data_t *c;
	struct cmg_t *cmg;

	util_fmt_obj_start(FMT_LIST, "channel_paths");
	chpid_for_each(c) {
		if (!c->selected)
			continue;
		cmg = c->cmg;
		if (opts.groups & (KEY_GRP_UTIL | KEY_GRP_METRICS))
			update_util(c, false);
		if (opts.groups & KEY_GRP_METRICS)
			cmg->update_metrics(&c->data);

		util_fmt_obj_start(FMT_ROW, NULL);
		if (opts.groups & KEY_GRP_CHP)
			pr_chpid(c);
		util_fmt_obj_start(FMT_DEFAULT, "cmg%d", cmg->cmg);
		if (opts.groups & KEY_GRP_CHARS)
			pr_group(c, cmg, KEY_GRP_CHARS);
		if (opts.groups & KEY_GRP_UTIL)
			pr_group(c, cmg, KEY_GRP_UTIL);
		if (opts.groups & KEY_GRP_METRICS)
			pr_group(c, cmg, KEY_GRP_METRICS);
		util_fmt_obj_end();
		util_fmt_obj_end();
	}
	util_fmt_obj_end();
}

/* Register the keys of all key-value pairs that will be printed. */
static void register_selected_keys(void)
{
	struct key_t *key;

	key_for_each_selected(key)
		util_fmt_add_key("%s", key->name);
}

/* Report an error if a key specified on the command line is not reported by
 * any available CHPID. */
static void check_key_availability(void)
{
	struct key_t *key;

	key_for_each_selected(key) {
		if (key->found)
			continue;
		errx(EXIT_RUNTIME, "No available CHPID provides key '%s' "
		     "(needs CMG %s)", key->name, key->cmg_str);
	}
}

static void apply_key_selection(void)
{
	bool do_filter = (opts.cmd == CMD_LIST);
	struct cmg_t *cmg_t;

	if (opts.all) {
		key_select_all();
	} else if (opts.groups_specified || opts.cmgs_specified ||
		 opts.keys_specified) {
		/* Apply selection options. */
		if (opts.groups_specified)
			key_select_by_groups(opts.groups, do_filter);
		if (opts.cmgs_specified) {
			cmg_for_each(cmg_t) {
				if (!cmg_t->selected)
					continue;
				key_select_by_cmg(cmg_t->cmg);
			}
		}
	} else {
		/* Select default keys suitable for command. */
		if (opts.cmd == CMD_LIST_KEYS)
			key_select_all();
		else
			key_select_by_groups(opts.groups, do_filter);
	}

	if (opts.cmd == CMD_LIST_KEYS)
		key_sort_selected();
}

/*
 * Show channel-path statistics in list format.
 */
static void cmd_list(void)
{
	int i;

	if (!opts.groups_specified && !opts.keys_specified)
		opts.groups = KEY_GRP_METRICS;
	opts.groups |= KEY_GRP_META | KEY_GRP_ITERATION | KEY_GRP_CHP;

	/* Re-initialize keys to get current key->found values. */
	key_init(opts.all);
	if (opts.keys_specified)
		check_key_availability();
	apply_key_selection();
	register_selected_keys();
	/* Select CMG key groups needed for selected keys. */
	opts.groups |= key_get_selected_groups();

	if (opts.groups & KEY_GRP_METRICS)
		update_util_all(true);

	if (opts.fmt != FMT_JSONSEQ)
		util_fmt_obj_start(FMT_LIST, NULL);
	for (i = 0; opts.forever || i < opts.iterations; i++) {
		if (i > 0)
			sleep((unsigned int)opts.interval);

		util_fmt_obj_start(0, NULL);
		if (opts.groups & KEY_GRP_ITERATION)
			pr_iteration_header(i);
		cmd_list_once();
		util_fmt_obj_end();

		/* Flush here to allow immediate consumption of full reports
		 * via pipes (e.g. for use with grep). */
		fflush(stdout);
	}
	if (opts.fmt != FMT_JSONSEQ)
		util_fmt_obj_end();
}

/*
 * Allocate a table using the names and widths of all selected columns.
 */
static struct util_rec *define_table(void)
{
	struct util_rec *rec;
	struct column_t *col;

	rec = util_rec_new_wide(NULL);
	column_for_each_selected(col) {
		util_rec_def(rec, col->name, UTIL_REC_ALIGN_RIGHT,
			     (int)col->width + (int)col->pad, "");
	}

	return rec;
}

/*
 * Scale value @v by @unit and return the suffix of the applied
 * unit-multiplier.
 */
static char scale_auto(struct cmg_pair_t *p, int unit)
{
	char suffixes[] = "\0KMGTPE";
	int i;

	for (i = 0; suffixes[i + 1]; i++) {
		switch (p->type) {
		case CMG_U32:
			if (p->value_u32 < (u32)unit)
				goto out;
			p->value_u32 /= (u32)unit;
			break;
		case CMG_U64:
			if (p->value_u64 < (u64)unit)
				goto out;
			p->value_u64 /= (u64)unit;
			break;
		case CMG_FLOAT:
			if (p->value_double < (double)unit)
				goto out;
			p->value_double /= (double)unit;
			break;
		default:
			break;
		}
	}

out:
	return suffixes[i];
}

static void scale_fixed(struct cmg_pair_t *p, unsigned long unit)
{
	switch (p->type) {
	case CMG_U32:
		p->value_u32 /= (u32)unit;
		break;
	case CMG_U64:
		p->value_u64 /= (u64)unit;
		break;
	case CMG_FLOAT:
		p->value_double /= (double)unit;
		break;
	default:
		break;
	}
}

/*
 * Add @value in formatted form to @column of @table. Scale value if defined
 * for @column.
 */
static void add_pair_value(struct util_rec *table, struct column_t *col,
			   struct cmg_pair_t *pair)
{
	char suffix = 0, suffix2 = opts.unit_iec ? 'i' : 0, str[16];
	int p;

	if (!pair->valid) {
		util_rec_set(table, col->name, "-");
		return;
	}

	if (pair->unit == CMG_NUMBER) {
		suffix = scale_auto(pair, UNIT_DEC);
	} else if (pair->unit == CMG_BPS) {
		if (opts.unit == UNIT_AUTO)
			suffix = scale_auto(pair, opts.unit_iec ? UNIT_BIN : UNIT_DEC);
		else
			scale_fixed(pair, opts.unit);
	}

	switch (pair->type) {
	case CMG_U32:
		snprintf(str, sizeof(str), "%u%c%c", pair->value_u32, suffix,
			 suffix2);
		break;
	case CMG_U64:
		snprintf(str, sizeof(str), "%llu%c%c", pair->value_u64, suffix,
			 suffix2);
		break;
	case CMG_FLOAT:
		/* Find highest precision that fits into @width characters. */
		for (p = 2; p >= 0; p--) {
			snprintf(str, sizeof(str), "%.*f%c%c", p,
				 pair->value_double, suffix, suffix2);
			if (strlen(str) <= col->width)
				break;
		}
		break;
	}
	util_rec_set(table, col->name, "%s", str);
}

/*
 * Add generic, non-cmg specific data for column @col and CHPID @c to @table.
 * Return %true if requested column was handled, %false otherwise.
 */
static bool add_column_generic(struct util_rec *table, struct column_t *col,
			       struct chpid_data_t *c)
{
	const char *name = col->name;

	switch (col->id) {
	case COL_CHPID:
		util_rec_set(table, name, "%02x", c->id);
		break;
	case COL_TYPE:
		util_rec_set(table, name, "%02x", c->type);
		break;
	case COL_CMG:
		util_rec_set(table, name, "%d", c->cmg->cmg);
		break;
	case COL_SHARED:
		util_rec_set(table, name, "%d", c->shared);
		break;
	case COL_SPEED:
		if (!c->speed)
			return false;
		util_rec_set(table, name, "%s", c->speed);
		break;
	default:
		return false;
	}

	return true;
}

static bool add_column_cmg(struct util_rec *table, struct column_t *col,
			   struct cmg_pair_t *pairs)
{
	int i;

	for (i = 0; pairs[i].key; i++) {
		if (pairs[i].col != col->id)
			continue;
		add_pair_value(table, col, &pairs[i]);
		return true;
	}

	return false;
}

/*
 * Add row data for CHPID @c to @table.
 */
static void add_table_row(struct util_rec *table, struct chpid_data_t *c,
			  struct cmg_pair_t *pairs)
{
	struct column_t *col;

	column_for_each_selected(col) {
		if (add_column_generic(table, col, c))
			continue;
		if (add_column_cmg(table, col, pairs))
			continue;
		/* Default text for unavailable column values. */
		util_rec_set(table, col->name, "-");
	}
}

/*
 * Find the longest number of consecutive columns that use the same non-empty
 * group header. Start with selected column @start. Update @num_ptr to contain
 * the number of consecutive columns, and @width_ptr to contain the total
 * width of these columns, including separating spaces.
 */
static void get_hdr_group_size(unsigned int start, unsigned int *num_ptr,
			       unsigned int *width_ptr)
{
	struct column_t *col;
	const char *last_hdr = "";
	unsigned int i, num, width;

	num = 0;
	width = 0;
	for (i = start; (col = column_get_by_index(i, true)); i++) {
		if (i > start) {
			if (!*last_hdr ||
			    strcmp(col->hdr1_group, last_hdr) != 0)
				break;
			/* Account for space between columns. */
			width++;
		}
		num++;
		width += col->width;
		last_hdr = col->hdr1_group;
	}
	*num_ptr = num;
	*width_ptr = width;
}

/*
 * Print @str centered in a space of @width characters.
 */
static void pr_centered(const char *str, unsigned int width)
{
	unsigned int a, b, l;

	l = strlen_u(str);
	width = MAX(width, l);
	a = (width - l) / 2;
	b = width - l - a;
	printf("%*s%s%*s", b, "", str, a, "");
}

/*
 * Distribute @extra characters evenly to column widths for @num selected
 * columns starting with @start.
 */
static void enlarge_columns(unsigned int start, unsigned int num,
			    unsigned int extra)
{
	struct column_t *col;
	unsigned int i, end, delta;

	end = start + num;
	for (i = start; i < end; i++) {
		col = column_get_by_index(i, true);
		delta = extra / (end - i);
		col->width += delta;
		extra -= delta;
	}
}

/*
 * Make sure that group header is centered over the non-spacing portions of
 * line 2 headers: |     HDR1      | => |       HDR1    |
 *                 |   HDR2    HDR2|    |   HDR2    HDR2|
 */
static void pr_hdr1_spacing(struct column_t *col, unsigned int *width_ptr)
{
	unsigned int w1, w2, spacing;

	w1 = strlen_u(col->hdr1_group);
	w2 = strlen_u(col->hdr2);
	if (col->width <= w2)
		return;
	spacing = col->width - w2;
	if (*width_ptr - spacing >= w1) {
		printf("%*s", spacing, "");
		*width_ptr -= spacing;
	}
}

/*
 * Print multi-line table header and update column widths based on header
 * lengths.
 */
static void print_table_header(void)
{
	struct column_t *col;
	unsigned int i, next_i, num, width, hdr_width;
	const char *last_hdr1 = NULL;

	/* Print first header line and update column width based on heading. */
	for (i = 0; (col = column_get_by_index(i, true)); i = next_i) {
		if (last_hdr1 && strcmp(last_hdr1, col->hdr1_group) != 0) {
			col->pad = 1;
			printf("%*s", col->pad + 1, "");
		}
		last_hdr1 = col->hdr1_group;
		get_hdr_group_size(i, &num, &width);
		if (num == 1) {
			/* Update column width in case heading is wider. */
			width = MAX(width, strlen_u(col->hdr1_single));
			width = MAX(width, strlen_u(col->hdr2));
			col->width = width;
			printf("%*s", col->width, col->hdr1_single);
			next_i = i + 1;
			continue;
		}
		hdr_width = strlen_u(col->hdr1_group);
		if (hdr_width > width) {
			/* Header text is longer than sum of column widths,
			 * increase column widths accordingly.*/
			enlarge_columns(i, num, hdr_width - width);
			width = hdr_width;
		}
		pr_hdr1_spacing(col, &width);
		pr_centered(col->hdr1_group, width);
		next_i = i + num;
	}
	printf("\n");

	/* Print second header line. */
	i = 0;
	column_for_each_selected(col) {
		if (i++ > 0)
			printf("%*s", col->pad + 1, "");
		printf("%*s", col->width, col->hdr2);
	}
	printf("\n");
}

/*
 * Show table of channel-path statistics once.
 */
static void cmd_table_once(void)
{
	struct cmg_pair_t *pairs;
	struct util_rec *table;
	struct chpid_data_t *c;
	struct cmg_t *cmg;

	ansi(ANSI_BOLD ANSI_REVERSE);
	print_table_header();
	ansi(ANSI_RESET);
	table = define_table();

	chpid_for_each(c) {
		if (!c->selected)
			continue;
		cmg = c->cmg;

		/* Update data. */
		update_util(c, false);
		cmg->update_metrics(&c->data);

		/* Add data to table. */
		pairs = cmg->get_values(&c->data, KEY_GRP_ALL);
		add_table_row(table, c, pairs);
		cmg_free_pairs(pairs);
		util_rec_print(table);
	}

	util_rec_free(table);

	if (opts.unit != UNIT_AUTO && !opts.unit_suffix)
		printf("\n* = %lu B/s\n", opts.unit);
}

static void apply_column_selection(void)
{
	struct cmg_t *cmg_t;

	if (opts.all) {
		column_select_all();
	} else if (opts.cmgs_specified) {
		/* Choose columns appropriate for CMGs specified via --cmg. */
		cmg_for_each(cmg_t) {
			if (!cmg_t->selected)
				continue;
			column_select_id_list(cmg_t->default_column_ids);
		}
	} else if (!opts.columns_specified) {
		/* Select default columns suitable for command. */
		if (opts.cmd == CMD_LIST_COLUMNS)
			column_select_all();
		else
			column_select_default();
	}
}

/* Artificial upper limit for BPS needed to determine maximum column widths. */
#define MAX_BPS		(1024.0 * /* MiB/s per GFC */ 100 * (/* MiB */ 1048576))

/*
 * Make sure each column fits expected values.
 */
static void calc_column_widths(void)
{
	struct column_t *col;
	const char *last_hdr1 = NULL;
	char str[32];
	unsigned int width;
	double v;

	column_for_each_selected(col) {
		/* Make sure column fits at least a single header. */
		col->width = MAX(col->width, strlen_u(col->hdr2));
		/* Make sure column fits maximum value. */
		width = col->width;
		switch (col->unit) {
		case COL_PERCENT:
			width = 4; /* "99.9" */
			break;
		case COL_NUMBER:
			width = 4; /* "999k" */
			break;
		case COL_BPS:
			if (!opts.unit_specified || opts.unit == UNIT_AUTO) {
				width = 4; /* "999K" */
			} else {
				v = MAX_BPS / (double)opts.unit;
				snprintf(str, sizeof(str) - 1, "%.0f%c%c", v,
					 opts.unit_suffix, opts.unit_iec ? 'i' : 0);
				width = strlen_u(str);
			}
			break;
		default:
			break;
		}
		col->width = MAX(col->width, width);
		/* Double space between groups. */
		if (last_hdr1 && strcmp(last_hdr1, col->hdr1_group) != 0)
			col->pad++;
		last_hdr1 = col->hdr1_group;
	}
}

/*
 * Show table of channel-path statistics continuously as specified by
 * command line options.
 */
static void cmd_table(void)
{
	int i;

	apply_column_selection();
	calc_column_widths();
	column_update_bps_suffix(opts.unit == UNIT_AUTO, opts.unit_iec,
				 opts.unit_suffix);

	printf("Collecting initial utilization data\n");
	update_util_all(true);

	buffer_stdout(true);
	for (i = 0; opts.forever || i < opts.iterations; i++) {
		if (i > 0) {
			sleep((unsigned int)opts.interval);
			printf("\n");
		}

		/* Clear screen + move cursor to top-left of terminal (1,1). */
		ansi(ANSI_CLS ANSI_LOCATE(1, 1));

		pr_iteration_header(i);
		printf("\n");
		cmd_table_once();

		/* Make table visible in one go to avoid screen flicker. */
		fflush(stdout);
	}
	buffer_stdout(false);
}

#define HDR_COLUMN	"column"
#define HDR_HEADING	"heading"
#define HDR_DESC	"description"

/*
 * List available table columns in table format.
 */
static void cmd_list_columns_table(void)
{
	struct util_rec *table;
	struct column_t *col;
	int w, w1, w2, w3;

	/* Determine column widths. */
	w1 = strlen_i(HDR_COLUMN);
	w2 = strlen_i(HDR_HEADING);
	w3 = strlen_i(HDR_DESC);
	column_for_each_selected(col) {
		w1 = MAX(w1, strlen_i(col->name));
		w = strlen_i(col->hdr1_single) + strlen_i(col->hdr2) + 1;
		w2 = MAX(w2, w);
		w3 = MAX(w3, strlen_i(col->desc));
	}
	w1++;
	w2++;

	/* Print table heading. */
	table = util_rec_new_wide(NULL);
	util_rec_def(table, HDR_COLUMN, UTIL_REC_ALIGN_LEFT, w1, "COLUMN");
	util_rec_def(table, HDR_HEADING, UTIL_REC_ALIGN_LEFT, w2, "HEADING");
	util_rec_def(table, HDR_DESC, UTIL_REC_ALIGN_LEFT, w3, "DESCRIPTION");
	util_rec_print_hdr(table);

	/* Print rows of column data. */
	column_for_each_selected(col) {
		util_rec_set(table, HDR_COLUMN, col->name);
		if (*col->hdr1_single) {
			util_rec_set(table, HDR_HEADING, "%s %s",
				     col->hdr1_single, col->hdr2);
		} else {
			util_rec_set(table, HDR_HEADING, "%s", col->hdr2);
		}
		util_rec_set(table, HDR_DESC, "%s", col->desc);
		util_rec_print(table);
	}

	util_rec_free(table);
}

#define HDR_NAME	"name"
#define HDR_HEAD	"heading"

/*
 * List available table columns in machine-readable format.
 */
static void cmd_list_columns_fmt(void)
{
	struct column_t *col;

	util_fmt_add_key(HDR_NAME);
	util_fmt_add_key(HDR_HEAD);
	util_fmt_add_key(HDR_DESC);

	util_fmt_obj_start(FMT_LIST, "chpstat_list_columns");
	column_for_each_selected(col) {
		util_fmt_obj_start(FMT_ROW, NULL);
		pr_pair_quoted(HDR_NAME, "%s", col->name);
		if (*col->hdr1_single) {
			pr_pair_quoted(HDR_HEAD, "%s %s",
				       col->hdr1_single, col->hdr2);
		} else {
			pr_pair_quoted(HDR_HEAD, "%s", col->hdr2);
		}
		pr_pair_quoted(HDR_DESC, "%s", col->desc);
		util_fmt_obj_end();
	}
	util_fmt_obj_end();
}

/*
 * List available table columns.
 */
static void cmd_list_columns(void)
{
	apply_column_selection();
	if (opts.fmt_specified)
		cmd_list_columns_fmt();
	else
		cmd_list_columns_table();
}

#define HDR_KEY		"key"
#define HDR_GROUP	"group"
#define HDR_CMGS	"cmgs"

/*
 * List available table columns in table format.
 */
static void cmd_list_key_table(void)
{
	struct util_rec *table;
	struct key_t *key;
	int w1, w2, w3;

	/* Determine column width. */
	w1 = strlen_i(HDR_KEY);
	w2 = strlen_i(HDR_GROUP);
	w3 = strlen_i(HDR_CMGS);
	key_for_each_selected(key) {
		w1 = MAX(w1, strlen_i(key->name));
		w2 = MAX(w2, strlen_i(key_group_to_str(key->group)));
		w3 = MAX(w3, strlen_i(key->cmg_str));
	}
	/* Increase spacing between columns to improve readability. */
	w1++;
	w2++;
	/* Print table heading. */
	table = util_rec_new_wide(NULL);
	util_rec_def(table, HDR_KEY, UTIL_REC_ALIGN_LEFT, w1, "KEY");
	util_rec_def(table, HDR_GROUP, UTIL_REC_ALIGN_LEFT, w2, "GROUP");
	util_rec_def(table, HDR_CMGS, UTIL_REC_ALIGN_LEFT, w3, "CMGS");
	util_rec_print_hdr(table);
	/* Print rows of column data. */
	key_for_each_selected(key) {
		util_rec_set(table, HDR_KEY, "%s", key->name);
		util_rec_set(table, HDR_GROUP, "%s",
			     key_group_to_str(key->group));
		util_rec_set(table, HDR_CMGS, "%s", key->cmg_str);
		util_rec_print(table);
	}

	util_rec_free(table);
}

/*
 * List available keys in machine-readable format.
 */
static void cmd_list_key_fmt(void)
{
	struct key_t *key;

	util_fmt_add_key(HDR_KEY);
	util_fmt_add_key(HDR_GROUP);
	util_fmt_add_key(HDR_CMGS);

	util_fmt_obj_start(FMT_LIST, "chpstat_list_keys");
	key_for_each_selected(key) {
		util_fmt_obj_start(FMT_ROW, NULL);
		util_fmt_pair(FMT_QUOTE, HDR_KEY, "%s", key->name);
		util_fmt_pair(FMT_QUOTE, HDR_GROUP, "%s",
			      key_group_to_str(key->group));
		util_fmt_pair(FMT_QUOTE, HDR_CMGS, "%s", key->cmg_str);
		util_fmt_obj_end();
	}
	util_fmt_obj_end();
}

/*
 * List available keys.
 */
static void cmd_list_keys(void)
{
	apply_key_selection();
	if (opts.fmt_specified)
		cmd_list_key_fmt();
	else
		cmd_list_key_table();
}

/*
 * Return long command line name for specified @cmd option.
 */
static const char *cmd_to_optstr(enum command_t cmd)
{
	unsigned int i;

	for (i = 0; i < ARRAY_SIZE(opt_vec); i++) {
		if (opt_vec[i].option.val == (int)cmd)
			return opt_vec[i].option.name;
	}
	return "";
}

/*
 * Set tool command @cmd and check for conflicting options.
 */
static void set_cmd(enum command_t cmd)
{
	if (!opts.cmd_specified || opts.cmd == cmd) {
		opts.cmd = cmd;
		opts.cmd_specified = true;
		return;
	}
	errx(EXIT_USAGE, "Options --%s and --%s cannot be specified together",
	     cmd_to_optstr(opts.cmd), cmd_to_optstr(cmd));
}

/*
 * Set output format to the format specified by @name.
 */
static void set_fmt(const char *name)
{
	enum util_fmt_t fmt;

	if (!util_fmt_name_to_type(name, &fmt)) {
		errx(EXIT_USAGE, "Unknown format '%s' - supported formats: "
		     FMT_TYPE_NAMES, name);
	}
	if (opts.fmt_specified && fmt != opts.fmt) {
		errx(EXIT_USAGE, "Option --format cannot be specified multiple "
		     "times");
	}
	opts.fmt = fmt;
	opts.fmt_specified = true;
}

static void init_fmt(void)
{
	unsigned int flags = 0;

	if (opts.keys_specified)
		flags |= FMT_FILTER;
	if (!opts.use_prefix)
		flags |= FMT_NOPREFIX;
	if (opts.fmt == FMT_JSON || opts.fmt == FMT_JSONSEQ) {
		/* Ensure correct JSON even if interrupted. */
		flags |= FMT_HANDLEINT;
	}
	if (opts.fmt == FMT_CSV) {
		/* Quote all values to ensure compatibility with a multitude
		 * of CSV consumers. */
		flags |= FMT_QUOTEALL;
	}
	if (opts.keys_specified || opts.all) {
		/* Always show data for keys with no valid value if the key
		 * was specifically requested by the user. */
		flags |= FMT_KEEPINVAL;
	}
	if (opts.debug)
		flags |= FMT_WARN;
	util_fmt_init(stdout, opts.fmt, flags, API_LEVEL);
	util_fmt_set_indent(0, 2, ' ');
}

/*
 * Parse options and execute the command
 */
int main(int argc, char *argv[])
{
	enum cm_status_t status;
	int c, num_selected;

	util_prg_init(&prg);
	util_opt_init(opt_vec, NULL);
	init_opts();
	init_chpid_data();
	key_init(opts.all);

	/* Parse command-line parameters. */
	while (1) {
		c = util_opt_getopt_long(argc, argv);
		if (c == -1)
			break;
		switch (c) {
		case 'h':
			/* --help */
			util_prg_print_help();
			util_opt_print_help();
			goto out;
		case 'v':
			/* --version */
			util_prg_print_version();
			goto out;
		case OPT_ENABLE:
			/* --enable */
			set_cmd(CMD_ENABLE);
			break;
		case OPT_DISABLE:
			/* --disable */
			set_cmd(CMD_DISABLE);
			break;
		case OPT_STATUS:
			/* --status */
			set_cmd(CMD_STATUS);
			break;
		case OPT_LIST_COLUMNS:
			/* --list-columns */
			set_cmd(CMD_LIST_COLUMNS);
			break;
		case OPT_COLUMNS:
			/* --columns */
			opts.columns_specified = true;
			parse_columns(optarg);
			break;
		case OPT_LIST_KEYS:
			/* --list-keys */
			set_cmd(CMD_LIST_KEYS);
			break;
		case OPT_KEYS:
			/* --keys KEY1,.. */
			opts.keys_specified = true;
			parse_keys(optarg);
			break;
		case OPT_CMG:
			/* --cmg NUM,.. */
			opts.cmgs_specified = true;
			parse_cmgs(optarg);
			break;
		case OPT_SCALE:
			/* --scale UNIT */
			opts.unit_specified = true;
			opts.unit = parse_unit(optarg);
			break;
		case OPT_ITERATIONS:
			/* --iterations NUM */
			opts.iterations = parse_int("iterations", optarg, 0,
						    INT_MAX);
			opts.forever = (opts.iterations < 1);
			break;
		case OPT_INTERVAL:
			/* --interval NUM */
			opts.interval = parse_int("interval", optarg, 1,
						  MAX_INTERVAL);
			break;
		case OPT_CHARS:
			/* --chars */
			opts.groups_specified = true;
			opts.groups |= KEY_GRP_CHARS;
			break;
		case OPT_UTIL:
			/* --util */
			opts.groups_specified = true;
			opts.groups |= KEY_GRP_UTIL;
			break;
		case OPT_METRICS:
			/* --metrics */
			opts.groups_specified = true;
			opts.groups |= KEY_GRP_METRICS;
			break;
		case OPT_ALL:
			/* --all */
			opts.all = true;
			break;
		case OPT_NO_ANSI:
			/* --no-ansi */
			opts.use_ansi = false;
			break;
		case OPT_NO_PREFIX:
			/* --no-prefix */
			opts.use_prefix = false;
			break;
		case OPT_DEBUG:
			/* --debug */
			opts.debug = true;
			break;
		case OPT_FORMAT:
			/* --format FORMAT */
			set_fmt(optarg);
			break;
		default:
			util_opt_print_parse_error((char)c, argv);
			return EXIT_USAGE;
		}
	}

	if (!opts.cmd_specified) {
		/* List output is implied by --chars, --util, --metrics, --keys
		 * and --format. */
		if (opts.groups_specified || opts.keys_specified ||
		    opts.fmt_specified)
			opts.cmd = CMD_LIST;
	}

	if (optind != argc && opts.cmd != CMD_LIST && opts.cmd != CMD_TABLE) {
		errx(EXIT_USAGE, "Unexpected parameter specified: %s",
		     argv[optind]);
	}

	/* Initialize output formatter. */
	init_fmt();

	/* Handle functions that work without CM support. */
	status = get_cm_status();
	switch (opts.cmd) {
	case CMD_STATUS:
		cmd_status(status);
		goto out;
	case CMD_LIST_COLUMNS:
		cmd_list_columns();
		goto out;
	case CMD_LIST_KEYS:
		cmd_list_keys();
		goto out;
	default:
		break;
	}

	/* Ensure measurements are available beyond this point. */
	if (status == CM_UNSUPPORTED) {
		errx(EXIT_RUNTIME, "This system does not support channel-path "
		     "statistics");
	}

	/* Handle supplemental functions. */
	switch (opts.cmd) {
	case CMD_ENABLE:
		cmd_enable(status);
		goto out;
	case CMD_DISABLE:
		cmd_disable(status);
		goto out;
	default:
		break;
	}

	/* Ensure measurements are enabled beyond this point. */
	if (status == CM_DISABLED) {
		errx(EXIT_RUNTIME, "Channel-path statistics are disabled\n"
		     "Use '%s --enable' to enable statistics",
		     program_invocation_short_name);
	}

	/* Determine CHPID list. */
	if (optind != argc)
		num_selected = parse_chpids(optind, argc, argv);
	else
		num_selected = select_all_chpids();
	if (num_selected == 0)
		errx(EXIT_RUNTIME, "No available CHPIDs found");

	switch (opts.cmd) {
	case CMD_LIST:
		cmd_list();
		break;
	default:
		cmd_table();
		break;
	}

out:
	util_fmt_exit();
	free_chpid_data();
	key_exit();
	cmg_exit();
	column_exit();

	return EXIT_OK;
}
