view cmd.c @ 24:eb5ce870eb16

Build compatibility functions as a library that can be reused
author Guido Berhoerster <guido+pwm@berhoerster.name>
date Wed, 20 Sep 2017 23:49:31 +0200
parents 1b89066d992c
children 722a45b4028b
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 "pager.h"
#include "proc.h"
#include "pw.h"
#include "pwfile.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
};

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 struct cmd cmds[] = {
    { "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)
{
	size_t	i;
	size_t	name_len;

	for (i = 0; namev[i] != NULL; i++) {
		name_len = strlen(namev[i]);
		if ((strncmp(namev[i], arg, name_len) == 0) &&
		    (arg[name_len] == sep)) {
			if (valuep != NULL) {
				*valuep = arg + name_len + 1;
			}
			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_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 == FIELD_UNKNOWN) {
			pwm_err(ctx, "bad field name \"%s\"", argv[i]);
			goto out;
		}
		if (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, "bad 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;
		}
	}

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 == FIELD_UNKNOWN) {
			pwm_err(ctx, "bad field assignment \"%s\"", argv[i]);
		}
		if (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, "bad 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 == FIELD_UNKNOWN) {
			pwm_err(ctx, "bad field assignment \"%s\"", argv[i]);
			goto out;
		}
		if (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, "bad 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;
	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++) {
		switch (parse_arg(argv[i], cmd_generatepassword_argv, '=',
		    &value)) {
		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, "bad 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 };
	struct proc	proc = { 0 };

	if (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, "bad field name \"%s\"", argv[2]);
		return (CMD_ERR);
	}
	fields[type] = 1;

	if (proc_open(&proc, argv[3], "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);
}