/*
 * zdev - Modify and display the persistent configuration of devices
 *
 * lszdev: Display configuration of z Systems specific devices
 *
 * Copyright IBM Corp. 2016, 2017
 *
 * 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 <getopt.h>
#include <string.h>
#include <unistd.h>

#include "lib/zt_common.h"

#include "blkinfo.h"
#include "ccw.h"
#include "ctc.h"
#include "device.h"
#include "devtype.h"
#include "inuse.h"
#include "misc.h"
#include "module.h"
#include "namespace.h"
#include "opts.h"
#include "path.h"
#include "scsi.h"
#include "select.h"
#include "subtype.h"
#include "table.h"
#include "table_types.h"

/* Main program action. */
typedef enum {
	ACT_LIST,
	ACT_INFO,
	ACT_LIST_COLUMNS,
	ACT_LIST_TYPES,
	ACT_HELP,
	ACT_VERSION,
} action_t;

/* Representation of command line options. */
struct options {
	/* Selection. */
	struct select_opts *select;
	unsigned int type:1;

	/* Actions */
	unsigned int info;
	unsigned int list_columns:1;
	unsigned int list_types:1;
	unsigned int help:1;
	unsigned int version:1;

	/* Options */
	config_t config;
	unsigned int active:1;
	unsigned int persistent:1;
	unsigned int auto_conf:1;
	struct util_list *columns;	/* List of struct strlist_node */
	unsigned int no_headings:1;
	struct util_list *base;		/* List of struct strlist_node */
	unsigned int pairs:1;
	unsigned int verbose:1;
	unsigned int quiet:1;
};

/* Makefile converts lszdev_usage.txt into C file which we include here. */
static const char *usage_text =
#include "lszdev_usage.c"
	;

#define OPT_ANONYMOUS_BASE	0x80
enum {
	OPT_ALL			= (OPT_ANONYMOUS_BASE+__COUNTER__),
	OPT_CONFIGURED		= (OPT_ANONYMOUS_BASE+__COUNTER__),
	OPT_EXISTING		= (OPT_ANONYMOUS_BASE+__COUNTER__),
	OPT_ONLINE		= (OPT_ANONYMOUS_BASE+__COUNTER__),
	OPT_OFFLINE		= (OPT_ANONYMOUS_BASE+__COUNTER__),
	OPT_FAILED		= (OPT_ANONYMOUS_BASE+__COUNTER__),
	OPT_BY_PATH		= (OPT_ANONYMOUS_BASE+__COUNTER__),
	OPT_BY_NODE		= (OPT_ANONYMOUS_BASE+__COUNTER__),
	OPT_BY_INTERFACE	= (OPT_ANONYMOUS_BASE+__COUNTER__),
	OPT_BY_ATTRIB		= (OPT_ANONYMOUS_BASE+__COUNTER__),
	OPT_TYPE		= 't',
	OPT_INFO		= 'i',
	OPT_LIST_COLUMNS	= 'l',
	OPT_LIST_TYPES		= 'L',
	OPT_HELP		= 'h',
	OPT_VERSION		= 'v',
	OPT_ACTIVE		= 'a',
	OPT_PERSISTENT		= 'p',
	OPT_COLUMNS		= 'c',
	OPT_NO_HEADINGS		= 'n',
	OPT_BASE		= (OPT_ANONYMOUS_BASE+__COUNTER__),
	OPT_VERBOSE		= 'V',
	OPT_QUIET		= 'q',
	OPT_PAIRS		= 'P',
	OPT_AUTO_CONF		= (OPT_ANONYMOUS_BASE+__COUNTER__),
};

static struct opts_conflict conflict_list[] = {
	OPTS_CONFLICT(OPT_INFO,
		      OPT_LIST_COLUMNS, OPT_LIST_TYPES, OPT_HELP, OPT_VERSION,
		      OPT_COLUMNS, OPT_NO_HEADINGS,
		      0),
	OPTS_CONFLICT(OPT_LIST_COLUMNS,
		      OPT_LIST_TYPES, OPT_HELP, OPT_VERSION,
		      0),
	OPTS_CONFLICT(OPT_LIST_TYPES,
		      OPT_HELP, OPT_VERSION,
		      0),
	OPTS_CONFLICT(OPT_HELP,
		      OPT_VERSION,
		      0),
	OPTS_CONFLICT(OPT_TYPE,
		      OPT_CONFIGURED, OPT_EXISTING, OPT_ONLINE, OPT_OFFLINE,
		      OPT_BY_PATH, OPT_BY_NODE, OPT_BY_INTERFACE, OPT_FAILED,
		      0),
	OPTS_CONFLICT(OPT_ONLINE,
		      OPT_OFFLINE),
	OPTS_CONFLICT(OPT_QUIET,
		      OPT_VERBOSE),
	OPTS_CONFLICT(0, 0),
};

/* Command line options. */
static const struct option opt_list[] = {
	/* Selection. */
	{ "all",		no_argument,	NULL, OPT_ALL },
	{ "configured",		no_argument,	NULL, OPT_CONFIGURED },
	{ "existing",		no_argument,	NULL, OPT_EXISTING },
	{ "online",		no_argument,	NULL, OPT_ONLINE },
	{ "offline",		no_argument,	NULL, OPT_OFFLINE },
	{ "failed",		no_argument,	NULL, OPT_FAILED },
	{ "by-path",		required_argument, NULL, OPT_BY_PATH },
	{ "by-node",		required_argument, NULL, OPT_BY_NODE },
	{ "by-interface",	required_argument, NULL, OPT_BY_INTERFACE },
	{ "by-attrib",		required_argument, NULL, OPT_BY_ATTRIB },
	{ "type",		no_argument,	NULL, OPT_TYPE },

	/* Actions. */
	{ "info",		no_argument,	NULL, OPT_INFO },
	{ "list-columns",	no_argument,	NULL, OPT_LIST_COLUMNS },
	{ "list-types",		no_argument,	NULL, OPT_LIST_TYPES },
	{ "help",		no_argument,	NULL, OPT_HELP },
	{ "version",		no_argument,	NULL, OPT_VERSION },

	/* Options. */
	{ "active",		no_argument,	NULL, OPT_ACTIVE },
	{ "persistent",		no_argument,	NULL, OPT_PERSISTENT },
	{ "auto-conf",		no_argument,	NULL, OPT_AUTO_CONF },
	{ "columns",		required_argument, NULL, OPT_COLUMNS },
	{ "no-headings",	no_argument,	NULL, OPT_NO_HEADINGS },
	{ "base",		required_argument, NULL, OPT_BASE },
	{ "pairs",		no_argument,	NULL, OPT_PAIRS },
	{ "verbose",		no_argument,	NULL, OPT_VERBOSE },
	{ "quiet",		no_argument,	NULL, OPT_QUIET },
	{ NULL,			no_argument,	NULL, 0 },
};

/* Command line abbreviations. */
static const char opt_str[] = ":tilLhvapPc:nVq";

/* Initialize options data structure. */
static void init_options(struct options *opts)
{
	memset(opts, 0, sizeof(struct options));
	opts->select = select_opts_new();
	opts->columns = strlist_new();
	opts->base = strlist_new();
}

/* Release memory used in options data structure. */
static void free_options(struct options *opts)
{
	if (!opts)
		return;
	select_opts_free(opts->select);
	strlist_free(opts->columns);
	strlist_free(opts->base);
}

/* Print usage information. */
static void print_usage(void)
{
	printf("%s", usage_text);
}

/* Print version information. */
static void print_version(void)
{
	printf("%s version %s\n", toolname, RELEASE_STRING);
}

/* Determine main program action from parse command line options. */
static action_t get_action(struct options *opts)
{
	if (opts->help)
		return ACT_HELP;
	if (opts->version)
		return ACT_VERSION;
	if (opts->info)
		return ACT_INFO;
	if (opts->list_columns)
		return ACT_LIST_COLUMNS;
	if (opts->list_types)
		return ACT_LIST_TYPES;
	return ACT_LIST;
}

/* Check options data structure for syntax errors. */
static exit_code_t check_options(struct options *opts,
				 int specified[OPTS_MAX + 1], int op)
{
	if (opts_check_conflict(op, specified, conflict_list, opt_list))
		return EXIT_USAGE_ERROR;

	return EXIT_OK;
}

/* Determine if STR is a valid device specification. */
static bool is_devspec(struct devtype *dt, struct subtype *st, char *str)
{
	char *copy, *curr, *next;
	bool rc;

	/* ID,ID-ID,... */
	copy = misc_strdup(str);
	next = copy;

	/* Separate by comma. */
	rc = true;
	if (st) {
		/* Is this a valid ID or range for this subtype? */
		while ((curr = strsep(&next, ","))) {
			if (!ns_is_id_valid(st->namespace, curr) &&
			    !ns_is_id_range_valid(st->namespace, curr)) {
				rc = false;
				break;
			}
		}
	} else if (dt) {
		/* Is this a valid ID or range for any subtype of this
		 * devtype? */
		while ((curr = strsep(&next, ","))) {
			if (!devtype_is_id_valid(dt, curr) &&
			    !devtype_is_id_range_valid(dt, curr)) {
				rc = false;
				break;
			}
		}
	} else {
		/* Is this a valid ID or range for any subtype? */
		while ((curr = strsep(&next, ","))) {
			if (!namespaces_is_id_valid(curr) &&
			    !namespaces_is_id_range_valid(curr)) {
				rc = false;
				break;
			}
		}
	}

	free(copy);

	return rc;
}

static exit_code_t check_devid(struct devtype *only_dt, struct subtype *only_st,
			       const char *id)
{
	int i, j;
	struct devtype *dt;
	struct subtype *st;
	struct namespace *ns;

	for (i = 0; (dt = devtypes[i]); i++) {
		if (only_dt && dt != only_dt)
			continue;
		for (j = 0; (st = dt->subtypes[j]); j++) {
			if (only_st && st != only_st)
				continue;
			if (ns_is_id_valid(st->namespace, id))
				return EXIT_OK;
		}
	}

	/* ID not valid - generate corresponding error message and exit code. */
	ns = NULL;
	if (only_st)
		ns = only_st->namespace;
	else if (only_dt && devtype_count_namespaces(only_dt) == 1)
		ns = only_dt->subtypes[0]->namespace;
	else
		ns = devtype_most_similar_namespace(only_dt, NULL, id);

	if (ns)
		return ns->is_id_valid(id, err_print);

	if (only_dt) {
		error("Unrecognized %s ID format: %s\n",
		      only_dt->devname, id);
		return EXIT_INVALID_ID;
	}

	syntax("Unknown device type or device ID format: %s\n", id);

	return EXIT_USAGE_ERROR;

}

static exit_code_t check_range(struct devtype *only_dt, struct subtype *only_st,
			       const char *range)
{
	int i, j;
	struct devtype *dt;
	struct subtype *st;
	struct namespace *ns;

	for (i = 0; (dt = devtypes[i]); i++) {
		if (only_dt && dt != only_dt)
			continue;
		for (j = 0; (st = dt->subtypes[j]); j++) {
			if (only_st && st != only_st)
				continue;
			if (ns_is_id_range_valid(st->namespace, range))
				return EXIT_OK;
		}
	}

	/* Range not valid - generate corresponding error message and exit
	 * code. */
	ns = NULL;
	if (only_st)
		ns = only_st->namespace;
	else if (only_dt && devtype_count_namespaces(only_dt) == 1)
		ns = only_dt->subtypes[0]->namespace;

	if (ns)
		return ns->is_id_range_valid(range, err_print);

	if (only_dt) {
		error("Unrecognized %s device ID range format: %s\n",
		      only_dt->devname, range);
		return EXIT_INVALID_ID;
	}

	syntax("Unknown device type or device ID range format: %s\n", range);

	return EXIT_USAGE_ERROR;
}

/* Get corresponding exit code and error message for invalid devspec. */
static exit_code_t check_devspec(struct devtype *dt, struct subtype *st,
				 const char *devspec)
{
	char *copy, *curr, *next;
	exit_code_t rc = EXIT_OK;

	/* ID,ID-ID,... */
	copy = misc_strdup(devspec);
	next = copy;

	/* Separate by comma. */
	while ((curr = strsep(&next, ","))) {
		if (strchr(curr, '-'))
			rc = check_range(dt, st, curr);
		else
			rc = check_devid(dt, st, curr);
		if (rc)
			break;
	}
	free(copy);

	return rc;
}

static exit_code_t check_devspecs(struct options *opts)
{
	struct strlist_node *str;
	exit_code_t rc = EXIT_OK;

	util_list_iterate(&opts->select->devids, str) {
		rc = check_devspec(opts->select->devtype, opts->select->subtype,
				   str->str);
		if (rc)
			break;
	}

	return rc;
}

/* Parse positional parameters. */
static exit_code_t parse_positional(struct options *opts, int argc,
				    char *argv[], int start)
{
	action_t action = get_action(opts);
	struct subtype *st;
	struct devtype *dt;
	int i;
	bool got_devspec;
	enum {
		invalid,
		required,
		optional,
	} ex_dev, ex_dt;

	/* Determine valid parameter combinations for each action. */
	switch (action) {
	case ACT_LIST:
	case ACT_INFO:
		if (opts->type) {
			ex_dev = invalid;
			ex_dt = required;
		} else {
			ex_dev = optional;
			ex_dt = optional;
		}
		break;
	case ACT_LIST_TYPES:
		ex_dev = invalid;
		ex_dt = optional;
		break;
	default:
		ex_dev = invalid;
		ex_dt = invalid;
		break;
	}

	/* Collect parameters. */
	for (i = start; i < argc; i++) {
		dt = devtype_find(argv[i]);
		st = subtype_find(argv[i]);

		if (ex_dt != invalid && st && !opts->select->devtype) {
			/* Got expected subtype. */
			opts->select->subtype = st;
			opts->select->devtype = st->devtype;
			continue;
		}
		if (ex_dt != invalid && dt && !opts->select->devtype) {
			/* Got expected devtype. */
			opts->select->devtype = dt;
			continue;
		}
		if (ex_dev != invalid && is_devspec(opts->select->devtype,
						    opts->select->subtype,
						    argv[i])) {
			/* Got expected device ID. */
			strlist_add_multi(&opts->select->devids, argv[i],
					  ",", 0);
			continue;
		}

		/* Handle error cases. */
		if (ex_dt != invalid && ex_dev == invalid) {
			if (opts->select->devtype)
				goto err_extra;
			else
				goto err_unknown_devtype;
		}
		if (ex_dt == invalid && ex_dev != invalid)
			goto err_unknown_dev;
		if (ex_dt != invalid && ex_dev != invalid) {
			if (opts->select->devtype)
				goto err_unknown_dev;
			goto err_unknown_dev_or_devtype;
		}

		goto err_extra;
	}

	/* Check for missing parameters. */
	got_devspec = select_opts_dev_specified(opts->select);

	if (ex_dt == required && !opts->select->devtype)
		goto err_no_devtype;
	if (ex_dev == required && !got_devspec)
		goto err_no_dev;
	if (ex_dev != invalid)
		goto check_devspec;

	return EXIT_OK;

check_devspec:
	return check_devspecs(opts);

err_unknown_dev_or_devtype:
	syntax("Unknown device type or device ID specification: %s\n", argv[i]);
	return EXIT_USAGE_ERROR;

err_unknown_dev:
	return check_devspec(opts->select->devtype, opts->select->subtype,
			     argv[i]);

err_unknown_devtype:
	error("Unrecognized device type: %s\n", argv[i]);
	return EXIT_UNKNOWN_DEVTYPE;

err_extra:
	syntax("Unexpected parameter found: %s\n", argv[i]);
	return EXIT_USAGE_ERROR;

err_no_devtype:
	error("Please specify a device type\n"
	      "Use '%s --list-types' to get a list of device types\n",
	      toolname);
	return EXIT_USAGE_ERROR;

err_no_dev:
	syntax("Please specify a device\n");
	return EXIT_USAGE_ERROR;
}

/* Parse command line options. Filter out invalid combinations. */
static exit_code_t parse_options(struct options *opts, int argc, char *argv[])
{
	exit_code_t rc;
	int opt;
	int specified[OPTS_MAX + 1];

	/* Suppress getopt error messages. */
	memset(specified, 0, sizeof(specified));
	rc = EXIT_OK;
	opterr = 0;
	while ((opt = getopt_long(argc, argv, opt_str, opt_list, NULL)) != -1) {
		switch (opt) {
		case OPT_ALL:
			/* --all */
			opts->select->all = 1;
			break;

		case OPT_CONFIGURED:
			/* --configured */
			opts->select->configured = 1;
			break;

		case OPT_EXISTING:
			/* --existing */
			opts->select->existing = 1;
			break;

		case OPT_ONLINE:
			/* --online */
			opts->select->online = 1;
			break;

		case OPT_OFFLINE:
			/* --offline */
			opts->select->offline = 1;
			break;

		case OPT_FAILED:
			/* --failed */
			opts->select->failed = 1;
			break;

		case OPT_BY_PATH:
			/* --by-path MOUNTPOINT */
			strlist_add(&opts->select->by_path, "%s", optarg);
			break;

		case OPT_BY_NODE:
			/* --by-node NODE */
			strlist_add(&opts->select->by_node, "%s", optarg);
			break;

		case OPT_BY_INTERFACE:
			/* --by-interface NAME */
			strlist_add(&opts->select->by_if, "%s", optarg);
			break;

		case OPT_BY_ATTRIB:
			/* --by-attrib NAME */
			if (!strchr(optarg, '=')) {
				syntax("--by-attrib requires argument in "
				       "ATTRIB=VALUE or ATTRIB!=VALUE "
				       "format\n");
				return EXIT_USAGE_ERROR;
			}
			strlist_add(&opts->select->by_attr, "%s", optarg);
			break;

		case OPT_TYPE:
			/* --type */
			opts->type = 1;
			break;

		case OPT_INFO:
			/* --info */
			opts->info++;
			break;

		case OPT_LIST_COLUMNS:
			/* --list-columns */
			opts->list_columns = 1;
			break;

		case OPT_LIST_TYPES:
			/* --list-types */
			opts->list_types = 1;
			break;

		case OPT_HELP:
			/* --help */
			opts->help = 1;
			/* --help has precedence - exit early */
			return EXIT_OK;

		case OPT_VERSION:
			/* --version */
			opts->version = 1;
			/* --version has precedence - exit early */
			return EXIT_OK;

		case OPT_ACTIVE:
			/* --active */
			opts->active = 1;
			break;

		case OPT_PERSISTENT:
			/* --persistent */
			opts->persistent = 1;
			break;

		case OPT_AUTO_CONF:
			/* --auto-conf */
			opts->auto_conf = 1;
			break;

		case OPT_COLUMNS:
			/* --columns COLUMN */
			strlist_add_multi(opts->columns, optarg, ",", 0);
			break;

		case OPT_NO_HEADINGS:
			/* --no-headings */
			opts->no_headings = 1;
			break;

		case OPT_BASE:
			/* --base PATH */
			strlist_add(opts->base, "%s", optarg);
			break;

		case OPT_PAIRS:
			/* --pairs */
			opts->pairs = 1;
			break;

		case OPT_VERBOSE:
			/* --verbose */
			opts->verbose = 1;
			break;

		case OPT_QUIET:
			/* --quiet */
			opts->quiet = 1;
			break;

		case ':':
			/* Missing option argument. */
			syntax("Option '%s' requires an argument\n",
			       argv[optind - 1]);
			return EXIT_USAGE_ERROR;

		case '?':
			/* Unknown option character. */
			if (optopt)
				syntax("Unrecognized option '-%c'\n", optopt);
			else {
				syntax("Unrecognized option '%s'\n",
				       argv[optind - 1]);
			}
			return EXIT_USAGE_ERROR;

		default:
			break;
		}
		if (opt >= 0 && opt <= OPTS_MAX)
			specified[opt] = 1;
		/* Check after each option to report errors in order of
		 * specification. */
		rc = check_options(opts, specified, opt);
		if (rc)
			break;
	}

	if (rc)
		goto out;

	/* Determine configuration set. */
	if (!opts->active && !opts->persistent && !opts->auto_conf) {
		/* Default display targets are active + persistent - note that
		 * autoconf data is still shown when available to make users
		 * aware of this type of data. */
		opts->config = config_active | config_persistent;
	} else {
		opts->config = get_config(opts->active, opts->persistent,
					  opts->auto_conf);
	}

	/* Handle positional parameters. */
	rc = parse_positional(opts, argc, argv, optind);

	/* Set implicit settings. */
	if (!select_opts_dev_specified(opts->select)) {
		switch (get_action(opts)) {
		case ACT_LIST:
		case ACT_INFO:
			/* List(--info without --type or device spec selects all
			 * devices. */
			if (!opts->type) {
				if (opts->config == config_all)
					opts->select->all = 1;
				else if (opts->config == config_active)
					opts->select->existing = 1;
				else if (opts->config == config_persistent)
					opts->select->configured = 1;
			}
			break;
		default:
			break;
		}
	}

out:
	return rc;
}

/* Column IDs for the devices table. */
enum dev_table_id {
	dev_type,
	dev_devid,
	dev_names,
	dev_blockdevs,
	dev_chardevs,
	dev_netdevs,
	dev_exists,
	dev_pers,
	dev_auto,
	dev_online,
	dev_failed,
	dev_modules,
	dev_attr,
	dev_attrpath,
};

/* Definition of output table for device list. */
static struct column *dev_table = COLUMN_ARRAY(
	COLUMN("TYPE",		align_left, dev_type, 1,
	       "Device type"),
	COLUMN("ID",		align_left, dev_devid, 1,
	       "Device identifier"),
	COLUMN("ON",		align_left, dev_online, 1,
	       "Device is online in the active configuration"),
	COLUMN("EXISTS",	align_left, dev_exists, 0,
	       "Device exists in the active configuration"),
	COLUMN("PERS",		align_left, dev_pers, 1,
	       "Device is configured persistently"),
	COLUMN("AUTO",		align_left, dev_auto, 0,
	       "Auto-configuration exists for device"),
	COLUMN("FAILED",	align_left, dev_failed, 0,
	       "Device is in error"),
	COLUMN("NAMES",		align_left, dev_names, 1,
	       "Associated Linux device names"),
	COLUMN("BLOCKDEVS",	align_left, dev_blockdevs, 0,
	       "Associated block devices including partitions"),
	COLUMN("CHARDEVS",	align_left, dev_chardevs, 0,
	       "Associated character devices"),
	COLUMN("NETDEVS",	align_left, dev_netdevs, 0,
	       "Associated network interfaces"),
	COLUMN("MODULES",	align_left, dev_modules, 0,
	       "Required kernel modules"),
	COLUMN("ATTR:",		align_left, dev_attr, 0,
	       "Value of specific attribute, e.g. ATTR:online"),
	COLUMN("ATTRPATH:",	align_left, dev_attrpath, 0,
	       "Path to specific attribute in active configuration")
);

static char *merge_str(const char *act, const char *pers, const char *ac,
		       config_t config)
{
	char *str;
	size_t len;

	act	= act  ? act  : "-";
	pers	= pers ? pers : "-";
	ac	= ac   ? ac   : "-";

	if (strcmp(act, pers) == 0 && strcmp(act, ac) == 0)
		return misc_strdup(act);

	len = strlen(act) + strlen(pers) + strlen(ac) + /* 3 * / + NUL */ 4;
	str = misc_malloc(len);

	if (SCOPE_ACTIVE(config))
		strcat(str, act);
	if (SCOPE_PERSISTENT(config)) {
		if (*str)
			strcat(str, "/");
		strcat(str, pers);
	}
	if (SCOPE_AUTOCONF(config)) {
		if (*str)
			strcat(str, "/");
		strcat(str, ac);
	}

	return str;
}

/* Return string representing online status of device @dev. */
static char *dev_table_get_online(struct device *dev, config_t config)
{
	int online;

	online = subtype_online_get(dev->subtype, dev, config_active);

	return misc_strdup(YESNO(online == 1));
}

/* Return string representing failed status of device @dev. */
static char *dev_table_get_failed(struct device *dev)
{
	struct util_list *errors;

	errors = subtype_get_errors(dev->subtype, dev->id);
	strlist_free(errors);

	return misc_strdup(YESNO(errors != NULL));
}

/* Return string for list of modules required by device @dev. */
static char *dev_table_get_modules(struct device *dev)
{
	struct util_list *names;
	char *str;

	names = strlist_new();
	device_add_modules(names, dev);
	str = strlist_flatten(names, " ");
	strlist_free(names);

	return str;
}

static char *get_attr(struct device *dev, const char *name, config_t config)
{
	struct setting_list *list;
	struct setting *s;

	list = device_get_setting_list(dev, config);
	if (!list)
		return NULL;
	s = setting_list_find(list, name);
	if (!s) {
		if (config == config_active) {
			/* Try reading active attribute directly. */
			return device_read_active_attrib(dev, name);
		}
		return NULL;
	}

	return misc_strdup(s->value);
}

/* Return string for attribute with specified @name for device @dev. */
static char *dev_table_get_attr(struct device *dev, const char *attr,
				config_t config)
{
	char *act = NULL, *pers = NULL, *ac = NULL, *str;
	const char *name;

	name = strchr(attr, ':');
	if (!name)
		return NULL;
	name++;

	if (SCOPE_ACTIVE(config))
		act = get_attr(dev, name, config_active);
	if (SCOPE_PERSISTENT(config))
		pers = get_attr(dev, name, config_persistent);
	if (SCOPE_AUTOCONF(config))
		ac = get_attr(dev, name, config_autoconf);

	str = merge_str(act, pers, ac, config);
	free(act);
	free(pers);
	free(ac);

	return str;
}

/* Return string for path to attribute with specified @name for device @dev
 * in the active configuration. */
static char *dev_table_get_attrpath(struct device *dev, const char *attr)
{
	const char *name;

	name = strchr(attr, ':');
	if (!name)
		return NULL;
	name++;

	return subtype_get_active_attrib_path(dev->subtype, dev, name);
}

/* Retrieve value of a cell for struct device @item in column @id in the
 * devices table. */
static char *dev_table_get_value(void *item, int id, const char *heading,
				 void *data)
{
	struct device *dev = item;
	struct options *opts = data;

	switch (id) {
	case dev_type:
		return misc_strdup(dev->subtype->name);
	case dev_devid:
		return misc_strdup(dev->id);
	case dev_names:
		return subtype_get_devnodes_str(dev->subtype, dev->id, 1, 0, 1,
						1);
	case dev_blockdevs:
		return subtype_get_devnodes_str(dev->subtype, dev->id, 1, 1, 0,
						0);
	case dev_chardevs:
		return subtype_get_devnodes_str(dev->subtype, dev->id, 0, 0, 1,
						0);
	case dev_netdevs:
		return subtype_get_devnodes_str(dev->subtype, dev->id, 0, 0, 0,
						1);
	case dev_exists:
		return misc_strdup(YESNO(dev->active.exists ||
					 dev->active.definable));
	case dev_pers:
		if (!dev->persistent.exists && dev->autoconf.exists)
			return misc_strdup("auto");
		return misc_strdup(YESNO(dev->persistent.exists));
	case dev_auto:
		return misc_strdup(YESNO(dev->autoconf.exists));
	case dev_online:
		return dev_table_get_online(dev, config_active);
	case dev_failed:
		return dev_table_get_failed(dev);
	case dev_modules:
		return dev_table_get_modules(dev);
	case dev_attr:
		return dev_table_get_attr(dev, heading, opts->config);
	case dev_attrpath:
		return dev_table_get_attrpath(dev, heading);
	default:
		break;
	}

	return NULL;
}

/* Determine if we need to read all device information based on command line
 * parameters. */
static read_scope_t get_scope(struct options *opts)
{
	struct strlist_node *s;
	struct column *col;

	util_list_iterate(opts->columns, s) {
		col = table_get_column(dev_table, s->str);
		if (!col)
			return scope_known;
		switch (col->id) {
		case dev_type:
		case dev_devid:
		case dev_names:
		case dev_blockdevs:
		case dev_chardevs:
		case dev_netdevs:
		case dev_exists:
		case dev_pers:
		case dev_auto:
		case dev_online:
		case dev_modules:
			continue;
		default:
			return scope_known;
		}
	}

	return scope_mandatory;
}

/* Build list of items in table from list of selected struct devices. */
static struct util_list *dev_table_build(struct options *opts,
					 exit_code_t *rc_ptr)
{
	struct util_list *selected, *devices = NULL;
	struct selected_dev_node *sel;
	struct device *dev;
	int active, persistent, autoconf;
	read_scope_t scope;
	exit_code_t rc;
	config_t config = opts->config;

	scope = get_scope(opts);
	selected = selected_dev_list_new();

	/* Read auto-config data when no configuration set was specified to
	 * make user aware of auto-config data. */
	if (!opts->active && !opts->persistent && !opts->auto_conf)
		config |= config_autoconf;

	rc = select_devices(opts->select, selected, 1, 0, opts->pairs,
			    config, scope, err_print);
	if (rc)
		goto out;
	devices = ptrlist_new();

	/* Process selected devices. */
	util_list_iterate(selected, sel) {
		if (sel->rc)
			continue;
		if (subtype_read_device(sel->st, sel->id, opts->config, scope,
					&dev))
			continue;

		if (dev->processed)
			continue;
		dev->processed = 1;

		/* Only process existing devices. */
		active = dev->active.exists || dev->active.definable;
		persistent = dev->persistent.exists;
		autoconf = dev->autoconf.exists;
		if (!active && !persistent && !autoconf)
			continue;

		ptrlist_add(devices, dev);
	}

	if (util_list_is_empty(devices)) {
		error("No device was selected!\n");
		ptrlist_free(devices, 0);
		devices = NULL;
		rc = EXIT_EMPTY_SELECTION;
	}

out:
	selected_dev_list_free(selected);
	if (rc_ptr)
		*rc_ptr = rc;

	return devices;
}

/* Perform device list. */
static exit_code_t do_list_devices(struct options *opts)
{
	struct util_list *items;
	exit_code_t rc;

	rc = table_check_columns(dev_table, opts->columns);
	if (rc)
		return rc;

	/* Create table from selection options. */
	items = dev_table_build(opts, &rc);
	if (rc)
		return rc;
	if (!items)
		return EXIT_EMPTY_SELECTION;

	/* Adjust columns visible depending on selected config set. */
	table_set_default(dev_table, dev_online, SCOPE_ACTIVE(opts->config));
	table_set_default(dev_table, dev_pers, SCOPE_PERSISTENT(opts->config));
	table_set_default(dev_table, dev_auto, SCOPE_AUTOCONF(opts->config) &&
					       !SCOPE_PERSISTENT(opts->config));
	table_set_default(dev_table, dev_names, SCOPE_ACTIVE(opts->config));

	/* Display table. */
	rc = table_print(dev_table, dev_table_get_value, opts, items,
			 opts->columns, !opts->no_headings, opts->pairs, 0,
			 util_list_is_empty(opts->columns));
	ptrlist_free(items, 0);

	return rc;
}

static char *devtype_get_modules_str(struct devtype *dt)
{
	struct util_list *modules;
	char *str;

	modules = strlist_new();
	devtype_add_modules(modules, dt, 1);
	str = strlist_flatten(modules, " ");
	strlist_free(modules);

	return str;
}

enum settings_table_id {
	settings_attribute,
	settings_readonly,
	settings_active,
	settings_persistent,
	settings_autoconf,
};

static struct column *settings_table = COLUMN_ARRAY(
	COLUMN("ATTRIBUTE", align_left, settings_attribute, 1, ""),
	COLUMN("READONLY", align_left, settings_readonly, 1, ""),
	COLUMN("ACTIVE", align_left, settings_active, 1, ""),
	COLUMN("PERSISTENT", align_left, settings_persistent, 1, ""),
	COLUMN("AUTOCONF", align_left, settings_autoconf, 0, "")
);

static struct util_list *settings_table_build(struct setting_list *active,
					      struct setting_list *persistent,
					      struct setting_list *autoconf,
					      bool readonly)
{
	struct util_list *names, *items;
	struct setting *s;
	struct strlist_node *str;

	/* Get a list of all configured attributes. */
	names = strlist_new();
	if (active) {
		util_list_iterate(&active->list, s) {
			if ((readonly && !s->readonly) ||
			    (!readonly && s->readonly))
				continue;
			strlist_add(names, s->name);
		}
	}
	if (persistent && !readonly) {
		util_list_iterate(&persistent->list, s)
			strlist_add(names, s->name);
	}
	if (autoconf && !readonly) {
		util_list_iterate(&autoconf->list, s)
			strlist_add(names, s->name);
	}
	strlist_sort_unique(names, str_cmp);

	/* Convert strlist to ptrlist. */
	items = ptrlist_new();
	util_list_iterate(names, str)
		ptrlist_add(items, misc_strdup(str->str));
	strlist_free(names);

	return items;
}

struct settings_table_data {
	struct setting_list *active;
	struct setting_list *persistent;
	struct setting_list *autoconf;
	int pairs;
	bool readonly;
};

static char *settings_table_get_value(void *item, int id, const char *heading,
				      void *data)
{
	char *name = item;
	struct settings_table_data *stdata = data;
	struct setting_list *list = NULL;
	struct setting *s;

	switch (id) {
	case settings_attribute:
	case settings_readonly:
		return misc_strdup(name);
	case settings_active:
		list = stdata->active;
		break;
	case settings_persistent:
		list = stdata->persistent;
		break;
	case settings_autoconf:
		list = stdata->autoconf;
		break;
	default:
		break;
	}
	if (list) {
		s = setting_list_find(list, name);
		if (s &&
		    !((id == settings_persistent || id == settings_autoconf) &&
		      s->derived)) {
			if (stdata->pairs)
				return misc_strdup(s->value);
			else
				return quote_str(s->value, 1);
		}
	}

	return misc_strdup(stdata->pairs ? "" : "-");
}

#define	INFO_INDENT	2
#define	INFO_WIDTH	19

static void settings_table_print(struct setting_list *active,
				 struct setting_list *persistent,
				 struct setting_list *autoconf,
				 struct options *opts, int ind, bool readonly,
				 bool neednl)
{
	struct util_list *items;
	struct settings_table_data data;

	items = settings_table_build(
			SCOPE_ACTIVE(opts->config) ? active : NULL,
			SCOPE_PERSISTENT(opts->config) ? persistent : NULL,
			autoconf, readonly);
	if (util_list_is_empty(items)) {
		if (!opts->pairs && !readonly)
			indent(ind, "%sNo settings found\n",
			       neednl ? "\n" : "");
		goto out;
	}
	if (neednl)
		printf("\n");

	table_set_default(settings_table, settings_attribute, readonly ? 0 : 1);
	table_set_default(settings_table, settings_readonly, readonly ? 1 : 0);
	table_set_default(settings_table, settings_active,
			  SCOPE_ACTIVE(opts->config));
	table_set_default(settings_table, settings_persistent,
			  SCOPE_PERSISTENT(opts->config) && !readonly ? 1 : 0);
	table_set_default(settings_table, settings_autoconf,
			  (SCOPE_AUTOCONF(opts->config) || autoconf) &&
				!readonly ? 1 : 0);
	data.active = active;
	data.persistent = persistent;
	data.autoconf = autoconf;
	data.pairs = opts->pairs;
	table_print(settings_table, settings_table_get_value, &data, items,
		    NULL, 1, opts->pairs, ind, 0);

out:
	ptrlist_free(items, 1);
}

static void print_info(const char *key, const char *value)
{
	if (!*value)
		value = "-";
	printf("%*s%*s%c %s\n", INFO_INDENT, "", -INFO_WIDTH, key,
	       *key ? ':' : ' ', value);
}

static void print_pair(const char *key, const char *value)
{
	char *quoted;

	quoted = quote_str(value, 1);
	printf("%s=%s\n", key, quoted);
	free(quoted);
}

static void print_pair_nonl(const char *key, const char *value)
{
	char *quoted;

	quoted = quote_str(value, 1);
	printf("%s=%s", key, quoted);
	free(quoted);
}

/* Perform device type list/info action. */
static exit_code_t do_info_type(struct options *opts)
{
	struct devtype *dt = opts->select->devtype;
	exit_code_t rc;
	char *modules;
	const char *name, *desc, *active, *persistent;

	/* Get devtype settings. */
	rc = dt->read_settings(dt, opts->config);
	if (rc)
		return rc;

	/* Ensure that settings which are not set are shown as such. */
	if (dt->persistent_settings)
		setting_list_remove_derived(dt->persistent_settings);

	/* Determine values for printing. */
	name		= dt->name;
	desc		= *dt->title ? dt->title : dt->subtypes[0]->title;
	modules		= devtype_get_modules_str(dt);
	active		= YESNO(dt->active_exists);
	persistent	= YESNO(dt->persistent_exists);

	/* Print information. */
	if (opts->pairs) {
		print_pair("TYPE", name);
		print_pair("DESCRIPTION", desc);
		print_pair("MODULES", modules);
		print_pair("ACTV", active);
		print_pair("PERS", persistent);
	} else {
		printf("DEVICE TYPE %s\n", name);
		print_info("Description", desc);
		print_info("Modules", modules);
		print_info("Active", active);
		print_info("Persistent", persistent);
		printf("\n");
	}

	if (!dt->type_attribs[0]) {
		if (!opts->pairs) {
			indent(INFO_INDENT, "Device type does not provide "
			       "type attributes\n");
		}
	} else {
		settings_table_print(dt->active_settings,
				     dt->persistent_settings, NULL, opts,
				     INFO_INDENT, false, false);
	}

	free(modules);

	return rc;
}

/* Perform --list-types. */
static exit_code_t do_list_type(struct options *opts)
{
	return table_types_show(opts->columns, !opts->no_headings, opts->pairs);
}

/* Perform --list-columns. */
static void do_list_columns(struct options *opts)
{
	table_print_columns(dev_table, opts->columns, !opts->no_headings,
			    opts->pairs);
}

/* Perform --info on one device. */
static void do_info_one_device(struct device *dev, struct options *opts)
{
	struct subtype *st = dev->subtype;
	char *names, *bdevs, *cdevs, *ndevs, *modules, *online, *path, *key;
	const char *id, *type, *exists, *persist, *ac, *prefix;
	struct util_list *resources, *errors;
	struct strlist_node *s;
	bool first;
	int i;

	/* Determine values for printing. */
	id	= dev->id;
	type	= st->name;
	names	= subtype_get_devnodes_str(st, dev->id, 1, 0, 1, 1);
	bdevs	= subtype_get_devnodes_str(st, dev->id, 1, 1, 0, 0);
	cdevs	= subtype_get_devnodes_str(st, dev->id, 0, 0, 1, 0);
	ndevs	= subtype_get_devnodes_str(st, dev->id, 0, 0, 0, 1);
	resources = inuse_get_resources(dev);
	modules	= dev_table_get_modules(dev);
	online	= dev_table_get_online(dev, config_active);
	errors	= subtype_get_errors(dev->subtype, dev->id);
	exists	= YESNO(dev->active.exists || dev->active.definable);
	persist	= YESNO(dev->persistent.exists);
	ac	= YESNO(dev->autoconf.exists);

	/* Print information. */
	if (opts->pairs) {
		print_pair("ID", id);
		print_pair("TYPE", type);
		print_pair("NAMES", names);
		print_pair("BLOCKDEVS", bdevs);
		print_pair("CHARDEVS", cdevs);
		print_pair("NETDEVS", ndevs);
		if (resources) {
			util_list_iterate(resources, s)
				print_pair("RESOURCE", s->str);
		}
		print_pair("MODULES", modules);
		print_pair("ONLINE", online);
		print_pair("EXISTS", exists);
		print_pair("PERSISTENT", persist);
		if (SCOPE_AUTOCONF(opts->config) || dev->autoconf.exists)
			print_pair("AUTOCONF", ac);
		if (errors) {
			util_list_iterate(errors, s)
				print_pair("ERROR", s->str);
		}
	} else {
		printf("DEVICE %s %s\n", type, id);
		print_info("Names", names);
		if (bdevs && *bdevs)
			print_info("Block devices", bdevs);
		if (cdevs && *cdevs)
			print_info("Character devices", cdevs);
		if (ndevs && *ndevs)
			print_info("Network interfaces", ndevs);
		if (resources) {
			first = true;
			util_list_iterate(resources, s) {
				print_info(first ? "Resources provided" : "",
					   s->str);
				first = false;
			}
		}
		print_info("Modules", modules);
		print_info("Online", online);
		print_info("Exists", exists);
		print_info("Persistent", persist);
		if (SCOPE_AUTOCONF(opts->config) || dev->autoconf.exists)
			print_info("Auto-configured", ac);
		if (errors) {
			first = true;
			util_list_iterate(errors, s) {
				print_info(first ? "Errors" : "",
					   s->str);
				first = false;
			}
		}
	}

	if (opts->info > 1) {
		/* Print device path. */
		path = subtype_get_active_attrib_path(st, dev, "");
		if (path) {
			if (opts->pairs)
				print_pair("DEVPATH", path);
			else
				print_info("Device path", path);
			free(path);
		}

		/* Print prefix paths. */
		for (i = 0; st->prefixes && (prefix = st->prefixes[i]); i++) {
			path = subtype_get_active_attrib_path(st, dev, prefix);
			if (!path)
				continue;
			if (opts->pairs) {
				print_pair_nonl("PREFIX", prefix);
				printf(" ");
				print_pair("PATH", path);
			} else {
				key = misc_asprintf("%s path", prefix);
				print_info(key, path);
				free(key);
			}
			free(path);
		}
	}

	settings_table_print(dev->active.exists ? dev->active.settings : NULL,
			     dev->persistent.exists ? dev->persistent.settings :
			     NULL,
			     dev->autoconf.exists ? dev->autoconf.settings :
			     NULL,
			     opts, INFO_INDENT, false, !opts->pairs);

	settings_table_print(dev->active.exists ? dev->active.settings : NULL,
			     dev->persistent.exists ? dev->persistent.settings :
			     NULL,
			     dev->autoconf.exists ? dev->autoconf.settings :
			     NULL,
			     opts, INFO_INDENT, true, !opts->pairs);

	strlist_free(errors);
	free(online);
	free(modules);
	strlist_free(resources);
	free(ndevs);
	free(cdevs);
	free(bdevs);
	free(names);
}

/* Perform --info on devices. */
static exit_code_t do_info_devices(struct options *opts)
{
	struct util_list *selected;
	struct selected_dev_node *sel;
	struct device *dev;
	int found, active, persistent, autoconf;
	exit_code_t rc, drc = EXIT_OK;
	read_scope_t scope;
	config_t config = opts->config;

	if (opts->info > 1)
		scope = scope_all;
	else
		scope = scope_known;

	/* Get list of selected devices. */
	selected = selected_dev_list_new();

	/* Read auto-config data when no configuration set was specified to
	 * make user aware of auto-config data. */
	if (!opts->active && !opts->persistent && !opts->auto_conf)
		config |= config_autoconf;

	select_devices(opts->select, selected, 1, 0, opts->pairs, config,
		       scope, err_print);

	/* Process selected devices. */
	found = 0;
	util_list_iterate(selected, sel) {
		if (sel->rc)
			continue;
		rc = subtype_read_device(sel->st, sel->id, opts->config,
					 scope, &dev);
		if (rc) {
			if (!drc)
				drc = rc;
			continue;
		}

		/* Only process existing devices. */
		active = dev->active.exists || dev->active.definable;
		persistent = dev->persistent.exists;
		autoconf = dev->autoconf.exists;
		if (!active && !persistent && !autoconf)
			continue;
		if (found > 0)
			printf("\n");
		do_info_one_device(dev, opts);
		found++;
	}
	selected_dev_list_free(selected);

	if (found == 0) {
		error("No device was selected!\n");
		drc = EXIT_EMPTY_SELECTION;
	}

	return drc;
}

int main(int argc, char *argv[])
{
	exit_code_t rc, drc = EXIT_OK;
	struct options opts;

	debug_init(argc, argv);

	/* Handle command line. */
	toolname = argv[0];
	init_options(&opts);
	devtypes_init();
	rc = parse_options(&opts, argc, argv);
	if (rc) {
		if (!drc)
			drc = rc;
		goto out;
	}

	/* Set globals. */
	verbose		= opts.verbose;
	quiet		= opts.quiet;
	path_set_base(opts.base);
	if (opts.pairs)
		set_stdout_data();

	/* Do not load modules when listing devices. Modules for existing
	 * devices should have been already loaded via udev modalias matching.
	 * Also users don't expect changes to their system when running a
	 * list tool. */
	module_load_suppress(1);

	/* Perform main action. */
	switch (get_action(&opts)) {
	case ACT_LIST:
		if (opts.type)
			rc = do_info_type(&opts);
		else
			rc = do_list_devices(&opts);
		break;
	case ACT_INFO:
		if (opts.type)
			rc = do_info_type(&opts);
		else
			rc = do_info_devices(&opts);
		break;
	case ACT_LIST_COLUMNS:
		do_list_columns(&opts);
		break;
	case ACT_LIST_TYPES:
		rc = do_list_type(&opts);
		break;
	case ACT_HELP:
		print_usage();
		break;
	case ACT_VERSION:
		print_version();
		break;
	}
	if (rc && !drc)
		drc = rc;

out:
	/* Write out any remaining messages. */
	delayed_print(0);

	/* Clean-up. */
	free_options(&opts);

	blkinfo_exit();
	ccw_exit();
	ctc_exit();
	devtypes_exit();
	inuse_exit();
	misc_exit();
	module_exit();
	rc = namespace_exit();
	if (rc && !drc)
		drc = rc;
	path_exit();
	scsi_exit();

	return drc ? drc : rc;
}
