view cmd.c @ 31:9be355e742e5

Distinguish between unknown argument and missing value
author Guido Berhoerster <guido+pwm@berhoerster.name>
date Tue, 28 Nov 2017 16:48:45 +0100
parents 2552eec9b913
children b5ebed168e59
line wrap: on
line source

/*
 * Copyright (C) 2017 Guido Berhoerster <guido+pwm@berhoerster.name>
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include "compat.h"

#include <ctype.h>
#ifdef	HAVE_ERR_H
#include <err.h>
#endif /* HAVE_ERR_H */
#include <errno.h>
#include <limits.h>
#include <regex.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "cmd.h"
#include "io.h"
#include "macro.h"
#include "pager.h"
#include "proc.h"
#include "pw.h"
#include "pwfile.h"
#include "tok.h"
#include "util.h"

#define	TIME_FORMAT	"%Y-%m-%dT%TZ"
#define	TIME_SIZE	(4 + 1 + 2 + 1 + 2 + 1 + 8 + 1 + 1)

#define	CHARS_DIGIT	"0123456789"
#define	CHARS_UPPER	"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
#define	CHARS_LOWER	"abcdefghijklmnopqrstuvwxyz"
#define	CHARS_PUNCT	"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"

enum field_type {
	FIELD_UNKNOWN = -1,
	FIELD_GROUP,
	FIELD_TITLE,
	FIELD_USERNAME,
	FIELD_PASSWORD,
	FIELD_NOTES,
	FIELD_URL,
	FIELD_MTIME,
	FIELD_CTIME
};

enum cmd_generatepassword_arg_type {
	CMD_GP_ARG_UNKNOWN = -1,
	CMD_GP_ARG_LEN,
	CMD_GP_ARG_CHARS,
	CMD_GP_ARG_CHARCLASS
};

enum charclass_type {
	CHARCLASS_UNKNOWN = -1,
	CHARCLASS_DIGIT,
	CHARCLASS_UPPER,
	CHARCLASS_LOWER,
	CHARCLASS_PUNCT,
	CHARCLASS_ALPHA,
	CHARCLASS_ALNUM,
	CHARCLASS_XDIGIT,
	CHARCLASS_GRAPH
};

enum option_type {
	OPTION_UNKNOWN = -1,
	OPTION_FILENAME,
	OPTION_PIPECOMMAND
};

static enum cmd_return	cmd_set(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_define(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_status(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_info(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_list(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_create(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_modify(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_generatepassword(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_remove(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_show(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_pipe(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_creategroup(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_removegroup(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_changepassword(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_help(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_write(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_quit(struct pwm_ctx *, int, char *[]);

static const char *field_namev[] = {
    "group",
    "title",
    "username",
    "password",
    "notes",
    "url",
    "ctime",
    "mtime",
    NULL
};

static const char *field_labels[] = {
    "Group:    ",
    "Title:    ",
    "Username: ",
    "Password: ",
    "Notes:    ",
    "URL:      ",
    "Created:  ",
    "Modified: "
};

static const char *cmd_generatepassword_argv[] = {
    "len",
    "char",
    "charclass",
    NULL
};

static const char *charclass_namev[] = {
    "digit",
    "upper",
    "lower",
    "punct",
    "alpha",
    "alnum",
    "xdigit",
    "graph",
    NULL
};

static const char *charclass_values[] = {
    CHARS_DIGIT,
    CHARS_UPPER,
    CHARS_LOWER,
    CHARS_PUNCT,
    CHARS_UPPER CHARS_LOWER,
    CHARS_DIGIT CHARS_UPPER CHARS_LOWER,
    CHARS_DIGIT "abcdef",
    CHARS_DIGIT CHARS_UPPER CHARS_LOWER CHARS_PUNCT
};

static const char *optionv[] = {
    "filename",
    "pipecommand",
    NULL
};

static struct cmd cmds[] = {
    { "S", "set", "set [option=value]", "Set an option or show option values",
    cmd_set },
    { "D", "define", "define name=value", "Define a macro", cmd_define },
    { "t", "status", "status", "Redisplay an error message of the previous "
    "command and unsaved changes", cmd_status },
    { "i", "info", "info", "Show metadata information about the current file",
    cmd_info },
    { "ls", "list", "list [field~regex ...]", "List entries", cmd_list },
    { "c", "create", "create field=value ...", "Create entry", cmd_create },
    { "m", "modify", "modify id field=value ...", "Modify entry", cmd_modify },
    { "gp", "generatepassword", "generatepassword [id] [len=n] [chars=n:chars] "
    "[charclass=n:class] ...", "Randomly generate a password",
    cmd_generatepassword },
    { "rm", "remove", "remove id", "Delete entry", cmd_remove },
    { "s", "show", "show id field", "Show entry", cmd_show },
    { "p", "pipe", "pipe id field command", "Pipe entry to external command",
    cmd_pipe },
    { "cg", "creategroup", "creategroup name", "Create empty group",
    cmd_creategroup },
    { "rg", "removegroup", "removegroup name", "Delete empty group",
    cmd_removegroup },
    { "ch", "changepassword", "changepassword", "Change password",
    cmd_changepassword },
    { "h", "help", "help [command]", "Show help text", cmd_help },
    { "w", "write", "write", "Write the database", cmd_write },
    { "q", "quit", "quit", "Quit", cmd_quit },
    { "Q", "Quit", "Quit", "Quit without checking", cmd_quit },
    { 0 }
};

static int
parse_arg(char *arg, const char *namev[], int sep, char **valuep)
{
	char	*p;
	size_t	i;

	if ((sep != '\0') && ((p = strchr(arg, sep)) != NULL)) {
		*p++ = '\0';
	}
	if (valuep != NULL) {
		*valuep = p;
	}

	for (i = 0; namev[i] != NULL; i++) {
		if (strcmp(namev[i], arg) == 0) {
			return (i);
		}
	}

	return (-1);
}

static int
parse_id(const char *arg, unsigned int *idp)
{
	long	x;
	char	*p;

	errno = 0;
	x = strtol(arg, &p, 10);
	if ((errno != 0) || (*arg == '\0') || (*p != '\0') || (x > UINT_MAX) ||
	    (x <= 0)) {
		return (-1);
	}
	*idp = (unsigned int)x;

	return (0);
}

static enum cmd_return
cmd_set(struct pwm_ctx *ctx, int argc, char *argv[])
{
	enum option_type type;
	char		*value;

	if (argc == 1) {
		/* show options */
		if ((io_printf("%s: %s\n", optionv[OPTION_FILENAME],
		    ctx->filename) == IO_SIGNAL) ||
		    (io_printf("%s: %s\n", optionv[OPTION_PIPECOMMAND],
		    (ctx->pipecmd != NULL) ? ctx->pipecmd : "") == IO_SIGNAL)) {
			return (CMD_SIGNAL);
		}

		return (CMD_OK);
	} else if (argc != 2) {
		return (CMD_USAGE);
	}

	type = parse_arg(argv[1], optionv, '=', &value);
	if ((type >= 0) && (value == NULL)) {
		pwm_err(ctx, "missing value for \"%s\"", argv[1]);
		return (CMD_ERR);
	}
	switch (type) {
	case OPTION_FILENAME:
		free(ctx->filename);
		ctx->filename = (value[0] != '\0') ? xstrdup(value) :
		    xasprintf(&ctx->filename, "%s/pwm.psafe3", ctx->dirname);
		break;
	case OPTION_PIPECOMMAND:
		free(ctx->pipecmd);
		ctx->pipecmd = (value[0] != '\0') ? xstrdup(value) : NULL;
		break;
	default:
		pwm_err(ctx, "unknown option \"%s\"", argv[1]);
		return (CMD_ERR);
	}

	return (CMD_OK);
}

static enum cmd_return
cmd_define(struct pwm_ctx *ctx, int argc, char *argv[])
{
	int		retval = CMD_ERR;
	const char	*value;
	char		*name = NULL;
	size_t		tokenc = 0;
	union tok	**tokenv = NULL;
	struct macro_entry *macro_entry;

	if (argc != 2) {
		return (CMD_USAGE);
	}

	/* split into name and value */
	value = strchr(argv[1], '=');
	if (value == NULL) {
		pwm_err(ctx, "bad macro definition \"%s\"", argv[1]);
		goto out;
	}
	xasprintf(&name, "%.*s", value - argv[1], argv[1]);
	value++;

	/* tokenize macro value */
	switch (tok_tokenize(value, &tokenc, &tokenv)) {
	case TOK_ERR_UNTERMINATED_QUOTE:
		pwm_err(ctx, "unterminated quote in macro");
		goto out;
	case TOK_ERR_TRAILING_BACKSLASH:
		pwm_err(ctx, "trailing backslash in macro");
		goto out;
	case TOK_ERR_INVALID_MACRO_NAME:
		pwm_err(ctx, "invalid macro name referenced in macro");
		goto out;
	case TOK_ERR_OK:
		break;
	}

	/* parse macro definition */
	switch (macro_parse(name, tokenc, tokenv, ctx->macro_head,
	    &macro_entry)) {
	case MACRO_ERR_INVALID_NAME:
		pwm_err(ctx, "invalid macro name \"%s\"", name);
		goto out;
	case MACRO_ERR_UNDEFINED_MACRO:
		pwm_err(ctx, "macro definition references undefined macro");
		goto out;
	case MACRO_ERR_RECURSIVE:
		pwm_err(ctx, "macro definition must not be recursive");
		goto out;
	case MACRO_ERR_OK:
		break;
	}

	macro_add(ctx->macro_head, macro_entry);
	retval = CMD_OK;

out:
	tok_free(tokenv);
	free(name);

	return (retval);
}

static enum cmd_return
cmd_status(struct pwm_ctx *ctx, int argc, char *argv[])
{
	if (argc != 1) {
		return (CMD_USAGE);
	}

	if (ctx->errmsg != NULL) {
		if (io_printf("%s\n", ctx->errmsg) == IO_SIGNAL) {
			return (CMD_SIGNAL);
		}
	}
	if (ctx->is_readonly) {
		if (io_printf("Read-only mode\n") == IO_SIGNAL) {
			return (CMD_SIGNAL);
		}
	} else {
		if (io_printf("There are%sunsaved changes\n",
		    ctx->unsaved_changes ? " " : " no ") == IO_SIGNAL) {
			return (CMD_SIGNAL);
		}
	}

	return (CMD_STATUS);
}

static enum cmd_return
cmd_info(struct pwm_ctx *ctx, int argc, char *argv[])
{
	enum cmd_return	retval;
	struct metadata	*metadata;
	struct pager	*pager;
	struct tm	*tm;
	char		timebuf[TIME_SIZE];

	if (argc != 1) {
		return (CMD_USAGE);
	}

	metadata = pwfile_get_metadata(ctx);

	pager = pager_create(STDOUT_FILENO);
	pager_printf(pager, "Format:      0x%04x\n", metadata->version);
	if (metadata->user != NULL) {
		pager_printf(pager, "User:        %s\n", metadata->user);
	}
	if (metadata->user != NULL) {
		pager_printf(pager, "Host:        %s\n", metadata->host);
	}
	if (metadata->user != NULL) {
		pager_printf(pager, "Application: %s\n", metadata->application);
	}
	tm = gmtime(&metadata->timestamp);
	strftime(timebuf, sizeof (timebuf), TIME_FORMAT, tm);
	pager_printf(pager, "Last Saved:  %s\n", timebuf);
	retval = (pager_show(pager) != IO_SIGNAL) ? CMD_OK : CMD_SIGNAL;
	pager_destroy(pager);

	pwfile_destroy_metadata(metadata);

	return (retval);
}

static enum cmd_return
cmd_list(struct pwm_ctx *ctx, int argc, char *argv[])
{
	int		retval = CMD_ERR;
	int		i;
	regex_t		*group_re = NULL;
	regex_t		*title_re = NULL;
	regex_t		*username_re = NULL;
	regex_t		*notes_re = NULL;
	regex_t		*url_re = NULL;
	enum field_type	type;
	char		*value;
	regex_t		**repp;
	int		errcode;
	char		*errbuf;
	size_t		errbuf_size;
	struct pager	*pager = NULL;
	union list_item	**list = NULL;
	size_t		j;
	struct record	*record;

	for (i = 1; i < argc; i++) {
		type = parse_arg(argv[i], field_namev, '~', &value);
		if ((type >= 0) && ((value == NULL) || (value[0] == '\0'))) {
			/* skip empty expressions */
			continue;
		}
		switch (type) {
		case FIELD_GROUP:
			repp = &group_re;
			break;
		case FIELD_TITLE:
			repp = &title_re;
			break;
		case FIELD_USERNAME:
			repp = &username_re;
			break;
		case FIELD_NOTES:
			repp = &notes_re;
			break;
		case FIELD_URL:
			repp = &url_re;
			break;
		default:
			pwm_err(ctx, "unknown field name \"%s\"", argv[i]);
			goto out;
		}

		if (*repp == NULL) {
			*repp = xmalloc(sizeof (regex_t));
		} else {
			regfree(*repp);
		}
		errcode = regcomp(*repp, value, REG_EXTENDED | REG_NOSUB);
		if (errcode != 0) {
			errbuf_size = regerror(errcode, *repp, "", 0);
			errbuf = xmalloc(errbuf_size);
			regerror(errcode, *repp, errbuf, errbuf_size);
			pwm_err(ctx, "bad regular expression \"%s\"", errbuf);
			free(errbuf);

			free(*repp);
			*repp = NULL;

			goto out;
		}
	}

	pager = pager_create(STDOUT_FILENO);
	list = pwfile_create_list(ctx);
	for (j = 0; list[j] != NULL; j++) {
		if (list[j]->any.type == ITEM_TYPE_GROUP) {
			pager_printf(pager, "[%s]\n", list[j]->group.group);
		} else {
			record = pwfile_get_record(ctx, list[j]->record.id);
			if (((group_re == NULL) || (regexec(group_re,
			    record->group, 0, NULL, 0) == 0)) &&
			    ((title_re == NULL) || (regexec(title_re,
			    record->title, 0, NULL, 0) == 0)) &&
			    ((username_re == NULL) || (regexec(username_re,
			    record->username, 0, NULL, 0) == 0)) &&
			    ((notes_re == NULL) || (regexec(notes_re,
			    record->notes, 0, NULL, 0) == 0)) &&
			    ((url_re == NULL) || (regexec(url_re,
			    record->url, 0, NULL, 0) == 0))) {
				pager_printf(pager, "%4u %s\n",
				    list[j]->record.id,
				    (list[j]->record.title != NULL) ?
				    list[j]->record.title : "");
			}
			pwfile_destroy_record(record);
		}
	}
	retval = (pager_show(pager) != IO_SIGNAL) ? CMD_OK : CMD_SIGNAL;

out:
	pager_destroy(pager);

	if (group_re != NULL) {
		regfree(group_re);
		free(group_re);
	}
	if (title_re != NULL) {
		regfree(title_re);
		free(title_re);
	}
	if (username_re != NULL) {
		regfree(username_re);
		free(username_re);
	}
	if (notes_re != NULL) {
		regfree(notes_re);
		free(notes_re);
	}
	if (url_re != NULL) {
		regfree(url_re);
		free(url_re);
	}

	pwfile_destroy_list(list);


	return (retval);
}

static int
read_record_fields(struct pwm_ctx *ctx, struct record *record)
{
	char		group_buf[PWM_LINE_MAX] = { '\0' };
	char		title_buf[PWM_LINE_MAX] = { '\0' };
	char		username_buf[PWM_LINE_MAX] = { '\0' };
	char		password_buf[PWM_LINE_MAX] = { '\0' };
	char		notes_buf[PWM_LINE_MAX] = { '\0' };
	char		url_buf[PWM_LINE_MAX] = { '\0' };

	if (io_get_line(NULL, "Group: ", 0, record->group, -1,
	    sizeof (group_buf), group_buf) == IO_SIGNAL) {
		return (CMD_SIGNAL);
	}
	io_trim_nl(group_buf);

	if (io_get_line(NULL, "Title: ", 0, record->title, -1,
	    sizeof (title_buf), title_buf) == IO_SIGNAL) {
		return (CMD_SIGNAL);
	}
	io_trim_nl(title_buf);

	if (io_get_line(NULL, "Username: ", 0, record->username, -1,
	    sizeof (username_buf), username_buf) == IO_SIGNAL) {
		return (CMD_SIGNAL);
	}
	io_trim_nl(username_buf);

	for (;;) {
		switch (io_get_password("Password: ", "Confirm Password: ",
		    sizeof (password_buf), password_buf)) {
		case IO_OK:		/* FALLTHROUGH */
		case IO_PASSWORD_EMPTY:
			goto password_done;
		case IO_SIGNAL:
			return (CMD_SIGNAL);
		case IO_PASSWORD_MISMATCH:
			pwm_err(ctx, "passwords do not match");
			continue;
		default:
			break;
		}
	}

password_done:
	if (io_get_line(NULL, "Notes: ", 0, record->notes, -1,
	    sizeof (notes_buf), notes_buf) == IO_SIGNAL) {
		return (CMD_SIGNAL);
	}
	io_trim_nl(notes_buf);

	if (io_get_line(NULL, "URL: ", 0, record->url, -1, sizeof (url_buf),
	    url_buf) == IO_SIGNAL) {
		return (CMD_SIGNAL);
	}
	io_trim_nl(url_buf);

	free(record->group);
	record->group = (group_buf[0] != '\0') ? xstrdup(group_buf) : NULL;
	free(record->title);
	record->title = (title_buf[0] != '\0') ? xstrdup(title_buf) : NULL;
	free(record->username);
	record->username = (username_buf[0] != '\0') ? xstrdup(username_buf) :
	    NULL;
	/*
	 * the current password cannot be edited, keep the current password if
	 * the user pressed return or ^D instead of deleting it like other
	 * fields
	 */
	if (password_buf[0] != '\0') {
		free(record->password);
		record->password = xstrdup(password_buf);
	}
	free(record->notes);
	record->notes = (notes_buf[0] != '\0') ? xstrdup(notes_buf) : NULL;
	free(record->url);
	record->url = (url_buf[0] != '\0') ? xstrdup(url_buf) : NULL;

	return (CMD_OK);
}

static enum cmd_return
cmd_create(struct pwm_ctx *ctx, int argc, char *argv[])
{
	enum cmd_return	retval = CMD_ERR;
	int		i;
	struct record *record = NULL;
	enum field_type	type;
	char		*value;

	if (!ctx->is_interactive && (argc < 2)) {
		retval = CMD_USAGE;
		goto out;
	}

	if (ctx->is_readonly) {
		pwm_err(ctx, "cannot create new entries in read-only mode");
		goto out;
	}

	record = pwfile_create_record();

	for (i = 1; i < argc; i++) {
		type = parse_arg(argv[i], field_namev, '=', &value);
		if ((type >= 0) && ((value == NULL) || (value[0] == '\0'))) {
			/* skip empty assignments */
			continue;
		}
		switch (type) {
		case FIELD_GROUP:
			free(record->group);
			record->group = xstrdup(value);
			break;
		case FIELD_TITLE:
			free(record->title);
			record->title = xstrdup(value);
			break;
		case FIELD_USERNAME:
			free(record->username);
			record->username = xstrdup(value);
			break;
		case FIELD_PASSWORD:
			free(record->password);
			record->password = xstrdup(value);
			break;
		case FIELD_NOTES:
			free(record->notes);
			record->notes = xstrdup(value);
			break;
		case FIELD_URL:
			free(record->url);
			record->url = xstrdup(value);
			break;
		default:
			pwm_err(ctx, "unknown field name \"%s\"", argv[i]);
			goto out;
		}
	}

	if (ctx->is_interactive && (argc < 2)) {
		if (read_record_fields(ctx, record) != 0) {
			goto out;
		}
	}

	pwfile_create_pws_record(ctx, record);
	retval = CMD_OK;

out:
	pwfile_destroy_record(record);

	return (retval);
}

static enum cmd_return
cmd_modify(struct pwm_ctx *ctx, int argc, char *argv[])
{
	int		retval = CMD_ERR;
	unsigned int	id;
	int		i;
	struct record	*record = NULL;
	enum field_type	type;
	char		*value;

	if (!ctx->is_interactive && (argc < 2)) {
		retval = CMD_USAGE;
		goto out;
	}

	if (parse_id(argv[1], &id) != 0) {
		pwm_err(ctx, "invalid id %s", argv[1]);
		goto out;
	}

	if (ctx->is_readonly) {
		pwm_err(ctx, "cannot modify entries in read-only mode");
		goto out;
	}

	record = pwfile_get_record(ctx, id);

	for (i = 2; i < argc; i++) {
		type = parse_arg(argv[i], field_namev, '=', &value);
		if ((type >= 0) && ((value == NULL) || (value[0] == '\0'))) {
			/* skip empty assignments */
			continue;
		}
		switch (type) {
		case FIELD_GROUP:
			free(record->group);
			record->group = xstrdup(value);
			break;
		case FIELD_TITLE:
			free(record->title);
			record->title = xstrdup(value);
			break;
		case FIELD_USERNAME:
			free(record->username);
			record->username = xstrdup(value);
			break;
		case FIELD_PASSWORD:
			free(record->password);
			record->password = xstrdup(value);
			break;
		case FIELD_NOTES:
			free(record->notes);
			record->notes = xstrdup(value);
			break;
		case FIELD_URL:
			free(record->url);
			record->url = xstrdup(value);
			break;
		default:
			pwm_err(ctx, "unknown field name \"%s\"", argv[i]);
			goto out;
		}
	}

	if (ctx->is_interactive && (argc < 3)) {
		if (read_record_fields(ctx, record) != 0) {
			goto out;
		}
	}

	pwfile_modify_pws_record(ctx, id, record);
	retval = CMD_OK;

out:
	pwfile_destroy_record(record);

	return (retval);
}

static enum cmd_return
cmd_generatepassword(struct pwm_ctx *ctx, int argc, char *argv[])
{
	enum cmd_return	retval = CMD_ERR;
	unsigned int	id = 0;
	int		i = 1;
	enum cmd_generatepassword_arg_type type;
	char		*value = NULL;
	long		x;
	char		*p;
	size_t		password_len = 16;
	size_t		chars_min;
	const char	*chars;
	struct pw_char_group *char_groupv = NULL;
	size_t		char_groupv_len = 0;
	int		charclass;
	size_t		j;
	char		password[PWS3_MAX_PASSWORD_LEN + 1] = { 0 };

	/* check if first argument is an id */
	if ((argc > 1) && (parse_id(argv[1], &id) == 0)) {
		i++;
		if (ctx->is_readonly) {
			pwm_err(ctx, "cannot modify entries in read-only mode");
			goto out;
		}
	}

	for (; i < argc; i++) {
		type = parse_arg(argv[i], cmd_generatepassword_argv, '=',
		    &value);
		if ((type >= 0) && ((value == NULL) || (value[0] == '\0'))) {
			pwm_err(ctx, "invalid value for \"%s\"", argv[i]);
			retval = CMD_USAGE;
			goto out;
		}
		switch (type) {
		case CMD_GP_ARG_LEN:
			errno = 0;
			x = strtol(value, &p, 10);
			if ((errno != 0) || (*value == '\0') || (*p != '\0') ||
			    (x > PWS3_MAX_PASSWORD_LEN) || (x <= 0)) {
				pwm_err(ctx, "invalid password length \"%s\"",
				    argv[i]);
				goto out;
			}
			password_len = x;
			break;
		case CMD_GP_ARG_CHARS:
			errno = 0;
			x = strtol(value, &p, 10);
			if ((errno != 0) || (*value == '\0') || (*p != ':') ||
			    (x < 0) || (x > PWS3_MAX_PASSWORD_LEN)) {
				pwm_err(ctx, "invalid minimum number of "
				    "characters \"%s\"", argv[i]);
				goto out;
			}
			chars_min = x;

			chars = ++p;
			while (*p != '\0') {
				if (!isascii(*p) || !isprint(*p)) {
					pwm_err(ctx, "invalid character in "
					    "character group \"%s\"", argv[i]);
					goto out;
				}
				p++;
			}

			char_groupv = xrealloc(char_groupv,
			    sizeof (struct pw_char_group) * (char_groupv_len +
			    1));
			char_groupv[char_groupv_len].chars = chars;
			char_groupv[char_groupv_len].chars_min = chars_min;
			char_groupv_len++;
			break;
		case CMD_GP_ARG_CHARCLASS:
			errno = 0;
			x = strtol(value, &p, 10);
			if ((errno != 0) || (*value == '\0') || (*p != ':') ||
			    (x < 0) || (x > PWS3_MAX_PASSWORD_LEN)) {
				pwm_err(ctx, "invalid minimum number of "
				    "characters \"%s\"", argv[i]);
				goto out;
			}
			chars_min = x;

			charclass = parse_arg(++p, charclass_namev, '\0', NULL);
			if (charclass < 0) {
				pwm_err(ctx, "unknown character class \"%s\"",
				    argv[i]);
				goto out;
			}
			chars = charclass_values[charclass];
			char_groupv = xrealloc(char_groupv,
			    sizeof (struct pw_char_group) * (char_groupv_len +
			    1));
			char_groupv[char_groupv_len].chars = chars;
			char_groupv[char_groupv_len].chars_min = chars_min;
			char_groupv_len++;
			break;
		default:
			pwm_err(ctx, "invalid argument \"%s\"", argv[i]);
			retval = CMD_USAGE;
			goto out;
		}
	}

	for (j = 0; j < char_groupv_len; j++) {
		if (char_groupv[j].chars_min > password_len) {
			pwm_err(ctx, "invalid minimum number of characters "
			    "\"%zu:%s\"", char_groupv[j].chars_min,
			    char_groupv[j].chars);
			goto out;
		}
	}

	if (char_groupv_len == 0) {
		/* use defaults */
		char_groupv = xmalloc(sizeof (struct pw_char_group));
		char_groupv[0].chars = charclass_values[CHARCLASS_GRAPH];
		char_groupv[0].chars_min = 0;
		char_groupv_len++;
	}

	if (pw_genrandom(char_groupv, char_groupv_len, password,
	    password_len) != 0) {
		pwm_err(ctx, "failed to generate password that meets the given "
		    "constraints");
		goto out;
	}

	if (id != 0) {
		if (pwfile_modify_pws_record(ctx, id,
		    &(struct record){ .password = password }) != 0) {
			pwm_err(ctx, "record %u does not exist", id);
			goto out;
		}
		retval = CMD_OK;
	} else {
		retval = io_printf("%s\n", password);
	}

out:
	free(char_groupv);

	return (retval);
}

static enum cmd_return
cmd_remove(struct pwm_ctx *ctx, int argc, char *argv[])
{
	unsigned int	id;

	if (argc != 2) {
		return (CMD_USAGE);
	}

	if (parse_id(argv[1], &id) != 0) {
		pwm_err(ctx, "invalid id %s", argv[1]);
		return (CMD_ERR);
	}

	if (ctx->is_readonly) {
		pwm_err(ctx, "cannot remove entries in read-only mode");
		return (CMD_ERR);
	}

	if (pwfile_remove_pws_record(ctx, id) != 0) {
		pwm_err(ctx, "failed to remove record %u", id);
		return (CMD_ERR);
	}

	return (CMD_OK);
}

static int
print_record(struct record *record, int fields[], int show_labels, int fd)
{
	struct pager	*pager;
	struct tm	*tm;
	char		timebuf[TIME_SIZE];
	int		retval;

	pager = pager_create(fd);
	if (fields[FIELD_TITLE]) {
		pager_printf(pager, "%s%s\n", show_labels ?
		    field_labels[FIELD_TITLE] : "", (record->title != NULL) ?
		    record->title : "");
	}
	if (fields[FIELD_GROUP]) {
		pager_printf(pager, "%s%s\n", show_labels ?
		    field_labels[FIELD_GROUP] : "", (record->group != NULL) ?
		    record->group : "");
	}
	if (fields[FIELD_USERNAME]) {
		pager_printf(pager, "%s%s\n", show_labels ?
		    field_labels[FIELD_USERNAME] : "",
		    (record->username != NULL) ?  record->username : "");
	}
	if (fields[FIELD_PASSWORD]) {
		pager_printf(pager, "%s%s\n", show_labels ?
		    field_labels[FIELD_PASSWORD] : "",
		    (record->password != NULL) ?  record->password : "");
	}
	if (fields[FIELD_NOTES]) {
		pager_printf(pager, "%s%s\n", show_labels ?
		    field_labels[FIELD_NOTES] : "", (record->notes != NULL) ?
		    record->notes : "");
	}
	if (fields[FIELD_URL]) {
		pager_printf(pager, "%s%s\n", show_labels ?
		    field_labels[FIELD_URL] : "", (record->url != NULL) ?
		    record->url : "");
	}
	if (fields[FIELD_CTIME]) {
		tm = gmtime(&record->ctime);
		strftime(timebuf, sizeof (timebuf), TIME_FORMAT, tm);
		pager_printf(pager, "%s%s\n", show_labels ?
		    field_labels[FIELD_CTIME] : "", timebuf);
	}
	if (fields[FIELD_MTIME]) {
		tm = gmtime(&record->mtime);
		strftime(timebuf, sizeof (timebuf), TIME_FORMAT, tm);
		pager_printf(pager, "%s%s\n", show_labels ?
		    field_labels[FIELD_MTIME] : "", timebuf);
	}
	retval = pager_show(pager);
	pager_destroy(pager);

	return (retval);
}

static enum cmd_return
cmd_show(struct pwm_ctx *ctx, int argc, char *argv[])
{
	enum cmd_return	retval;
	unsigned int	id;
	struct record	*record;
	int		i;
	enum field_type	type;
	int		fields[COUNTOF(field_namev) - 1] = {
		[FIELD_GROUP] = 1,
		[FIELD_TITLE] = 1,
		[FIELD_USERNAME] = 1,
		[FIELD_PASSWORD] = 0,
		[FIELD_NOTES] = 1,
		[FIELD_URL] = 1,
		[FIELD_MTIME] = 1,
		[FIELD_CTIME] = 1
	};

	if (argc < 2) {
		return (CMD_USAGE);
	}

	if (parse_id(argv[1], &id) != 0) {
		pwm_err(ctx, "invalid id %s", argv[1]);
		return (CMD_ERR);
	}

	if (argc > 2) {
		/* show only explicitly given field names */
		memset(fields, 0, sizeof (fields));
	}

	for (i = 2; i < argc; i++) {
		type = parse_arg(argv[i], field_namev, '\0', NULL);
		if (type < 0) {
			pwm_err(ctx, "unknown field name \"%s\"", argv[i]);
			return (CMD_ERR);
		}
		fields[type] = 1;
	}

	record = pwfile_get_record(ctx, id);
	if (record == NULL) {
		pwm_err(ctx, "record %u does not exist", id);
		return (CMD_ERR);
	}
	retval = (print_record(record, fields, 1, STDOUT_FILENO) != IO_SIGNAL) ?
	    CMD_OK : CMD_SIGNAL;
	pwfile_destroy_record(record);

	return (retval);
}

static enum cmd_return
cmd_pipe(struct pwm_ctx *ctx, int argc, char *argv[])
{
	enum cmd_return	retval = CMD_ERR;
	unsigned int	id;
	struct record	*record = NULL;
	enum field_type	type;
	int		fields[COUNTOF(field_namev) - 1] = { 0 };
	char		*cmd;
	struct proc	proc = { 0 };	

	/* if pipecommand is set, the last argument is optional */
	if (((ctx->pipecmd == NULL) && (argc != 4)) ||
	    ((ctx->pipecmd != NULL) && ((argc < 3) || (argc > 4)))) {
		return (CMD_USAGE);
	}

	if (parse_id(argv[1], &id) != 0) {
		pwm_err(ctx, "invalid id %s", argv[1]);
		return (CMD_ERR);
	}

	type = parse_arg(argv[2], field_namev, '\0', NULL);
	if (type < 0) {
		pwm_err(ctx, "unknown field name \"%s\"", argv[2]);
		return (CMD_ERR);
	}
	fields[type] = 1;

	cmd = (argc == 4) ? argv[3] : ctx->pipecmd;
	if (proc_open(&proc, cmd, "w") != IO_OK) {
		goto out;
	}


	record = pwfile_get_record(ctx, id);
	if (record == NULL) {
		pwm_err(ctx, "record %u does not exist", id);
		goto out;
	}

	retval = (print_record(record, fields, 0, proc.fd) != IO_SIGNAL) ?
	    CMD_OK : CMD_SIGNAL;

out:
	pwfile_destroy_record(record);
	if (proc.pid != 0) {
		if (proc_close(&proc) == IO_SIGNAL) {
			retval = CMD_SIGNAL;
		}
	}

	return (retval);
}

static enum cmd_return
cmd_creategroup(struct pwm_ctx *ctx, int argc, char *argv[])
{
	char		group_buf[PWM_LINE_MAX] = { '\0' };

	if (!ctx->is_interactive && (argc != 2)) {
		return (CMD_USAGE);
	}

	if (ctx->is_readonly) {
		pwm_err(ctx, "cannot create groups in read-only mode");
		return (CMD_ERR);
	}

	if (ctx->is_interactive && (argc != 2)) {
		if (io_get_line(NULL, "Group: ", 0, NULL, 0,
		    sizeof (group_buf), group_buf) == IO_SIGNAL) {
			return (CMD_SIGNAL);
		}
		io_trim_nl(group_buf);
	} else {
		strcpy(group_buf, argv[1]);
	}

	if (group_buf[0] != '\0') {
		if (pwfile_create_group(ctx, group_buf) != 0) {
			pwm_err(ctx, "group \"%s\" already exists", group_buf);
			return (CMD_ERR);
		}
	}

	return (CMD_OK);
}

static enum cmd_return
cmd_removegroup(struct pwm_ctx *ctx, int argc, char *argv[])
{
	if (argc != 2) {
		return (CMD_USAGE);
	}

	if (ctx->is_readonly) {
		pwm_err(ctx, "cannot remove groups in read-only mode");
		return (CMD_ERR);
	}

	if (pwfile_remove_group(ctx, argv[1]) != 0) {
		pwm_err(ctx, "empty group \"%s\" does not exist", argv[1]);
		return (CMD_ERR);
	}

	return (CMD_OK);
}

static enum cmd_return
cmd_changepassword(struct pwm_ctx *ctx, int argc, char *argv[])
{
	size_t	len;

	if (argc > 2) {
		return (CMD_USAGE);
	}

	if (ctx->is_readonly) {
		pwm_err(ctx, "cannot modify entries in read-only mode");
		return (CMD_ERR);
	}

	if (argc == 2) {
		len = strlen(argv[1]);
		if (len == 0) {
			pwm_err(ctx, "password must not be empty");
			return (CMD_ERR);
		} else if (len + 1 > sizeof (ctx->password)) {
			pwm_err(ctx, "password too long");
			return (CMD_ERR);
		}
		memcpy(ctx->password, argv[1], len + 1);
	} else {
		if (pwm_read_password(ctx, 1) != 0) {
			return (CMD_ERR);
		}
	}

	return (CMD_OK);
}

static enum cmd_return
cmd_help(struct pwm_ctx *ctx, int argc, char *argv[])
{
	enum cmd_return	retval = CMD_OK;
	struct pager	*pager;
	struct cmd	*cmd;

	if (argc > 2) {
		return (CMD_USAGE);
	}

	pager = pager_create(STDOUT_FILENO);
	if (argc == 2) {
		for (cmd = cmds; cmd->cmd_func != NULL; cmd++) {
			if ((strcmp(argv[1], cmd->abbrev_cmd) == 0) ||
			    (strcmp(argv[1], cmd->full_cmd) == 0)) {
				pager_printf(pager, "%s\n", cmd->usage);
				break;
			}
		}
	} else {
		printf("Commands:\n");
		for (cmd = cmds; cmd->cmd_func != NULL; cmd++) {
			pager_printf(pager, "%-2s %-16s %s\n", cmd->abbrev_cmd,
			    cmd->full_cmd, cmd->description);
		}
	}
	retval = (pager_show(pager) != IO_SIGNAL) ? CMD_OK : CMD_SIGNAL;
	pager_destroy(pager);

	return (retval);
}

static enum cmd_return
cmd_write(struct pwm_ctx *ctx, int argc, char *argv[])
{
	if (argc != 1) {
		return (CMD_USAGE);
	}

	if (ctx->is_readonly) {
		pwm_err(ctx, "cannot write changes in read-only mode");
		return (CMD_ERR);
	}

	return ((pwfile_write_file(ctx) == 0) ? CMD_OK : CMD_ERR);
}

static enum cmd_return
cmd_quit(struct pwm_ctx *ctx, int argc, char *argv[])
{
	if (argc != 1) {
		return (CMD_USAGE);
	}

	if ((argv[0][0] == 'q') && ctx->unsaved_changes &&
	    (ctx->prev_cmd != NULL) && (strcmp(ctx->prev_cmd, "quit") != 0)) {
		printf("Warning: There are unsaved changes\n");
		return (CMD_OK);
	}

	return (CMD_QUIT);
}

struct cmd *
cmd_match(const char *cmd_name)
{
	size_t	i;

	for (i = 0; cmds[i].cmd_func != NULL; i++) {
		if ((strcmp(cmds[i].abbrev_cmd, cmd_name) == 0) ||
		    (strcmp(cmds[i].full_cmd, cmd_name) == 0)) {
			return (&cmds[i]);
		}
	}

	return (NULL);
}