projects/pwm

changeset 22:ec01c579024a

Add fully interactive mode
author Guido Berhoerster <guido+pwm@berhoerster.name>
date Thu Sep 07 12:40:50 2017 +0200 (2017-09-07)
parents ee4d36c85287
children 1b89066d992c
files cmd.c io.c io.h pwfile.c pwfile.h pwm.1.xml pwm.c pwm.h
line diff
     1.1 --- a/cmd.c	Wed Sep 06 16:41:58 2017 +0200
     1.2 +++ b/cmd.c	Thu Sep 07 12:40:50 2017 +0200
     1.3 @@ -402,18 +402,101 @@
     1.4  	return (retval);
     1.5  }
     1.6  
     1.7 +static int
     1.8 +read_record_fields(struct pwm_ctx *ctx, struct record *record)
     1.9 +{
    1.10 +	char		group_buf[PWM_LINE_MAX] = { '\0' };
    1.11 +	char		title_buf[PWM_LINE_MAX] = { '\0' };
    1.12 +	char		username_buf[PWM_LINE_MAX] = { '\0' };
    1.13 +	char		password_buf[PWM_LINE_MAX] = { '\0' };
    1.14 +	char		notes_buf[PWM_LINE_MAX] = { '\0' };
    1.15 +	char		url_buf[PWM_LINE_MAX] = { '\0' };
    1.16 +
    1.17 +	if (io_get_line(NULL, "Group: ", 0, record->group, -1,
    1.18 +	    sizeof (group_buf), group_buf) == IO_SIGNAL) {
    1.19 +		return (CMD_SIGNAL);
    1.20 +	}
    1.21 +	io_trim_nl(group_buf);
    1.22 +
    1.23 +	if (io_get_line(NULL, "Title: ", 0, record->title, -1,
    1.24 +	    sizeof (title_buf), title_buf) == IO_SIGNAL) {
    1.25 +		return (CMD_SIGNAL);
    1.26 +	}
    1.27 +	io_trim_nl(title_buf);
    1.28 +
    1.29 +	if (io_get_line(NULL, "Username: ", 0, record->username, -1,
    1.30 +	    sizeof (username_buf), username_buf) == IO_SIGNAL) {
    1.31 +		return (CMD_SIGNAL);
    1.32 +	}
    1.33 +	io_trim_nl(username_buf);
    1.34 +
    1.35 +	for (;;) {
    1.36 +		switch (io_get_password("Password: ", "Confirm Password: ",
    1.37 +		    sizeof (password_buf), password_buf)) {
    1.38 +		case IO_OK:		/* FALLTHROUGH */
    1.39 +		case IO_PASSWORD_EMPTY:
    1.40 +			goto password_done;
    1.41 +		case IO_SIGNAL:
    1.42 +			return (CMD_SIGNAL);
    1.43 +		case IO_PASSWORD_MISMATCH:
    1.44 +			pwm_err(ctx, "passwords do not match");
    1.45 +			continue;
    1.46 +		}
    1.47 +	}
    1.48 +
    1.49 +password_done:
    1.50 +	if (io_get_line(NULL, "Notes: ", 0, record->notes, -1,
    1.51 +	    sizeof (notes_buf), notes_buf) == IO_SIGNAL) {
    1.52 +		return (CMD_SIGNAL);
    1.53 +	}
    1.54 +	io_trim_nl(notes_buf);
    1.55 +
    1.56 +	if (io_get_line(NULL, "URL: ", 0, record->url, -1, sizeof (url_buf),
    1.57 +	    url_buf) == IO_SIGNAL) {
    1.58 +		return (CMD_SIGNAL);
    1.59 +	}
    1.60 +	io_trim_nl(url_buf);
    1.61 +
    1.62 +	free(record->group);
    1.63 +	record->group = (group_buf[0] != '\0') ? xstrdup(group_buf) : NULL;
    1.64 +	free(record->title);
    1.65 +	record->title = (title_buf[0] != '\0') ? xstrdup(title_buf) : NULL;
    1.66 +	free(record->username);
    1.67 +	record->username = (username_buf[0] != '\0') ? xstrdup(username_buf) :
    1.68 +	    NULL;
    1.69 +	/*
    1.70 +	 * the current password cannot be edited, keep the current password if
    1.71 +	 * the user pressed return or ^D instead of deleting it like other
    1.72 +	 * fields
    1.73 +	 */
    1.74 +	if (password_buf[0] != '\0') {
    1.75 +		free(record->password);
    1.76 +		record->password = xstrdup(password_buf);
    1.77 +	}
    1.78 +	free(record->notes);
    1.79 +	record->notes = (notes_buf[0] != '\0') ? xstrdup(notes_buf) : NULL;
    1.80 +	free(record->url);
    1.81 +	record->url = (url_buf[0] != '\0') ? xstrdup(url_buf) : NULL;
    1.82 +
    1.83 +	return (CMD_OK);
    1.84 +}
    1.85 +
    1.86  static enum cmd_return
    1.87  cmd_create(struct pwm_ctx *ctx, int argc, char *argv[])
    1.88  {
    1.89 +	enum cmd_return	retval = CMD_ERR;
    1.90  	int		i;
    1.91 -	struct record record = { 0 };
    1.92 +	struct record *record = NULL;
    1.93  	enum field_type	type;
    1.94  	char		*value;
    1.95  
    1.96 -	if (argc < 2) {
    1.97 -		return (CMD_USAGE);
    1.98 +	if (!ctx->is_interactive && (argc < 2)) {
    1.99 +		retval = CMD_USAGE;
   1.100 +		goto out;
   1.101  	}
   1.102  
   1.103 +	record = pwfile_create_record();
   1.104 +
   1.105  	for (i = 1; i < argc; i++) {
   1.106  		type = parse_arg(argv[i], field_namev, '=', &value);
   1.107  		if (type == FIELD_UNKNOWN) {
   1.108 @@ -425,57 +508,76 @@
   1.109  		}
   1.110  		switch (type) {
   1.111  		case FIELD_GROUP:
   1.112 -			record.group = value;
   1.113 +			free(record->group);
   1.114 +			record->group = xstrdup(value);
   1.115  			break;
   1.116  		case FIELD_TITLE:
   1.117 -			record.title = value;
   1.118 +			free(record->title);
   1.119 +			record->title = xstrdup(value);
   1.120  			break;
   1.121  		case FIELD_USERNAME:
   1.122 -			record.username = value;
   1.123 +			free(record->username);
   1.124 +			record->username = xstrdup(value);
   1.125  			break;
   1.126  		case FIELD_PASSWORD:
   1.127 -			record.password = value;
   1.128 +			free(record->password);
   1.129 +			record->password = xstrdup(value);
   1.130  			break;
   1.131  		case FIELD_NOTES:
   1.132 -			record.notes = value;
   1.133 +			free(record->notes);
   1.134 +			record->notes = xstrdup(value);
   1.135  			break;
   1.136  		case FIELD_URL:
   1.137 -			record.url = value;
   1.138 +			free(record->url);
   1.139 +			record->url = xstrdup(value);
   1.140  			break;
   1.141  		default:
   1.142  			pwm_err(ctx, "bad field name \"%s\"", argv[i]);
   1.143 -			return (CMD_ERR);
   1.144 +			goto out;
   1.145  		}
   1.146  	}
   1.147  
   1.148 -	pwfile_create_record(ctx, &record);
   1.149 +	if (ctx->is_interactive && (argc < 2)) {
   1.150 +		if (read_record_fields(ctx, record) != 0) {
   1.151 +			goto out;
   1.152 +		}
   1.153 +	}
   1.154  
   1.155 -	return (CMD_OK);
   1.156 +	pwfile_create_pws_record(ctx, record);
   1.157 +	retval = CMD_OK;
   1.158 +
   1.159 +out:
   1.160 +	pwfile_destroy_record(record);
   1.161 +
   1.162 +	return (retval);
   1.163  }
   1.164  
   1.165  static enum cmd_return
   1.166  cmd_modify(struct pwm_ctx *ctx, int argc, char *argv[])
   1.167  {
   1.168 +	int		retval = CMD_ERR;
   1.169  	unsigned int	id;
   1.170  	int		i;
   1.171 -	struct record	record = { 0 };
   1.172 +	struct record	*record = NULL;
   1.173  	enum field_type	type;
   1.174  	char		*value;
   1.175  
   1.176 -	if (argc < 2) {
   1.177 -		return (CMD_USAGE);
   1.178 +	if (!ctx->is_interactive && (argc < 2)) {
   1.179 +		retval = CMD_USAGE;
   1.180 +		goto out;
   1.181  	}
   1.182  
   1.183  	if (parse_id(argv[1], &id) != 0) {
   1.184  		pwm_err(ctx, "invalid id %s", argv[1]);
   1.185 -		return (CMD_ERR);
   1.186 +		goto out;
   1.187  	}
   1.188 +	record = pwfile_get_record(ctx, id);
   1.189  
   1.190  	for (i = 2; i < argc; i++) {
   1.191  		type = parse_arg(argv[i], field_namev, '=', &value);
   1.192  		if (type == FIELD_UNKNOWN) {
   1.193  			pwm_err(ctx, "bad field assignment \"%s\"", argv[i]);
   1.194 -			return (CMD_ERR);
   1.195 +			goto out;
   1.196  		}
   1.197  		if (value[0] == '\0') {
   1.198  			/* skip empty assignments */
   1.199 @@ -483,32 +585,48 @@
   1.200  		}
   1.201  		switch (type) {
   1.202  		case FIELD_GROUP:
   1.203 -			record.group = value;
   1.204 +			free(record->group);
   1.205 +			record->group = xstrdup(value);
   1.206  			break;
   1.207  		case FIELD_TITLE:
   1.208 -			record.title = value;
   1.209 +			free(record->title);
   1.210 +			record->title = xstrdup(value);
   1.211  			break;
   1.212  		case FIELD_USERNAME:
   1.213 -			record.username = value;
   1.214 +			free(record->username);
   1.215 +			record->username = xstrdup(value);
   1.216  			break;
   1.217  		case FIELD_PASSWORD:
   1.218 -			record.password = value;
   1.219 +			free(record->password);
   1.220 +			record->password = xstrdup(value);
   1.221  			break;
   1.222  		case FIELD_NOTES:
   1.223 -			record.notes = value;
   1.224 +			free(record->notes);
   1.225 +			record->notes = xstrdup(value);
   1.226  			break;
   1.227  		case FIELD_URL:
   1.228 -			record.url = value;
   1.229 +			free(record->url);
   1.230 +			record->url = xstrdup(value);
   1.231  			break;
   1.232  		default:
   1.233  			pwm_err(ctx, "bad field name \"%s\"", argv[i]);
   1.234 -			return (CMD_ERR);
   1.235 +			goto out;
   1.236  		}
   1.237  	}
   1.238  
   1.239 -	pwfile_modify_record(ctx, id, &record);
   1.240 +	if (ctx->is_interactive && (argc < 3)) {
   1.241 +		if (read_record_fields(ctx, record) != 0) {
   1.242 +			goto out;
   1.243 +		}
   1.244 +	}
   1.245  
   1.246 -	return (CMD_OK);
   1.247 +	pwfile_modify_pws_record(ctx, id, record);
   1.248 +	retval = CMD_OK;
   1.249 +
   1.250 +out:
   1.251 +	pwfile_destroy_record(record);
   1.252 +
   1.253 +	return (retval);
   1.254  }
   1.255  
   1.256  static enum cmd_return
   1.257 @@ -633,7 +751,7 @@
   1.258  	}
   1.259  
   1.260  	if (id != 0) {
   1.261 -		if (pwfile_modify_record(ctx, id,
   1.262 +		if (pwfile_modify_pws_record(ctx, id,
   1.263  		    &(struct record){ .password = password }) != 0) {
   1.264  			pwm_err(ctx, "record %u does not exist", id);
   1.265  			goto out;
   1.266 @@ -663,7 +781,7 @@
   1.267  		return (CMD_ERR);
   1.268  	}
   1.269  
   1.270 -	if (pwfile_remove_record(ctx, id) != 0) {
   1.271 +	if (pwfile_remove_pws_record(ctx, id) != 0) {
   1.272  		pwm_err(ctx, "failed to remove record %u", id);
   1.273  		return (CMD_ERR);
   1.274  	}
   1.275 @@ -835,13 +953,27 @@
   1.276  static enum cmd_return
   1.277  cmd_creategroup(struct pwm_ctx *ctx, int argc, char *argv[])
   1.278  {
   1.279 -	if (argc != 2) {
   1.280 +	char		group_buf[PWM_LINE_MAX] = { '\0' };
   1.281 +
   1.282 +	if (!ctx->is_interactive && (argc != 2)) {
   1.283  		return (CMD_USAGE);
   1.284  	}
   1.285  
   1.286 -	if (pwfile_create_group(ctx, argv[1]) != 0) {
   1.287 -		pwm_err(ctx, "group \"%s\" already exists", argv[1]);
   1.288 -		return (CMD_ERR);
   1.289 +	if (ctx->is_interactive && (argc != 2)) {
   1.290 +		if (io_get_line(NULL, "Group: ", 0, NULL, 0,
   1.291 +		    sizeof (group_buf), group_buf) == IO_SIGNAL) {
   1.292 +			return (CMD_SIGNAL);
   1.293 +		}
   1.294 +		io_trim_nl(group_buf);
   1.295 +	} else {
   1.296 +		strcpy(group_buf, argv[1]);
   1.297 +	}
   1.298 +
   1.299 +	if (group_buf[0] != '\0') {
   1.300 +		if (pwfile_create_group(ctx, group_buf) != 0) {
   1.301 +			pwm_err(ctx, "group \"%s\" already exists", group_buf);
   1.302 +			return (CMD_ERR);
   1.303 +		}
   1.304  	}
   1.305  
   1.306  	return (CMD_OK);
     2.1 --- a/io.c	Wed Sep 06 16:41:58 2017 +0200
     2.2 +++ b/io.c	Thu Sep 07 12:40:50 2017 +0200
     2.3 @@ -47,6 +47,17 @@
     2.4  	siglongjmp(signal_env, signal_no);
     2.5  }
     2.6  
     2.7 +void
     2.8 +io_trim_nl(char *s)
     2.9 +{
    2.10 +	size_t	len;
    2.11 +
    2.12 +	len = strlen(s);
    2.13 +	if ((len > 0) && (s[len - 1] == '\n')) {
    2.14 +		s[len - 1] = '\0';
    2.15 +	}
    2.16 +}
    2.17 +
    2.18  int
    2.19  io_gl_complete_nothing(WordCompletion *cpl, void *data, const char *line,
    2.20      int word_end)
     3.1 --- a/io.h	Wed Sep 06 16:41:58 2017 +0200
     3.2 +++ b/io.h	Thu Sep 07 12:40:50 2017 +0200
     3.3 @@ -37,6 +37,7 @@
     3.4  	IO_PASSWORD_MISMATCH = -6
     3.5  };
     3.6  
     3.7 +void		io_trim_nl(char *);
     3.8  int		io_gl_complete_nothing(WordCompletion *, void *, const char *,
     3.9      int);
    3.10  enum io_status	io_get_char(const char *, int *);
     4.1 --- a/pwfile.c	Wed Sep 06 16:41:58 2017 +0200
     4.2 +++ b/pwfile.c	Thu Sep 07 12:40:50 2017 +0200
     4.3 @@ -827,7 +827,7 @@
     4.4  }
     4.5  
     4.6  int
     4.7 -pwfile_create_record(struct pwm_ctx *ctx, struct record *record)
     4.8 +pwfile_create_pws_record(struct pwm_ctx *ctx, struct record *record)
     4.9  {
    4.10  	struct pws3_record *pws3_record;
    4.11  	const unsigned char *uuid;
    4.12 @@ -853,7 +853,7 @@
    4.13  }
    4.14  
    4.15  int
    4.16 -pwfile_modify_record(struct pwm_ctx *ctx, unsigned int id,
    4.17 +pwfile_modify_pws_record(struct pwm_ctx *ctx, unsigned int id,
    4.18      struct record *record)
    4.19  {
    4.20  	const unsigned char *uuid;
    4.21 @@ -871,7 +871,7 @@
    4.22  }
    4.23  
    4.24  int
    4.25 -pwfile_remove_record(struct pwm_ctx *ctx, unsigned int id)
    4.26 +pwfile_remove_pws_record(struct pwm_ctx *ctx, unsigned int id)
    4.27  {
    4.28  	const unsigned char *uuid;
    4.29  	struct record_id_entry *entry;
    4.30 @@ -943,6 +943,24 @@
    4.31  }
    4.32  
    4.33  struct record *
    4.34 +pwfile_create_record(void)
    4.35 +{
    4.36 +	struct record	*record;
    4.37 +
    4.38 +	record = xmalloc(sizeof (struct record));
    4.39 +	record->ctime = (time_t)0;
    4.40 +	record->mtime = (time_t)0;
    4.41 +	record->group = NULL;
    4.42 +	record->title = NULL;
    4.43 +	record->username = NULL;
    4.44 +	record->password = NULL;
    4.45 +	record->notes = NULL;
    4.46 +	record->url = NULL;
    4.47 +
    4.48 +	return (record);
    4.49 +}
    4.50 +
    4.51 +struct record *
    4.52  pwfile_get_record(struct pwm_ctx *ctx, unsigned int id)
    4.53  {
    4.54  	struct record	*record;
     5.1 --- a/pwfile.h	Wed Sep 06 16:41:58 2017 +0200
     5.2 +++ b/pwfile.h	Thu Sep 07 12:40:50 2017 +0200
     5.3 @@ -79,12 +79,13 @@
     5.4  void		pwfile_destroy_list(union list_item **);
     5.5  struct metadata * pwfile_get_metadata(struct pwm_ctx *);
     5.6  void		pwfile_destroy_metadata(struct metadata *);
     5.7 -int		pwfile_create_record(struct pwm_ctx *, struct record *);
     5.8 -int		pwfile_modify_record(struct pwm_ctx *, unsigned int,
     5.9 +int		pwfile_create_pws_record(struct pwm_ctx *, struct record *);
    5.10 +int		pwfile_modify_pws_record(struct pwm_ctx *, unsigned int,
    5.11      struct record *);
    5.12 -int		pwfile_remove_record(struct pwm_ctx *, unsigned int);
    5.13 +int		pwfile_remove_pws_record(struct pwm_ctx *, unsigned int);
    5.14  int		pwfile_create_group(struct pwm_ctx *, const char *);
    5.15  int		pwfile_remove_group(struct pwm_ctx *, const char *);
    5.16 +struct record *	pwfile_create_record(void);
    5.17  struct record *	pwfile_get_record(struct pwm_ctx *, unsigned int);
    5.18  void		pwfile_destroy_record(struct record *);
    5.19  
     6.1 --- a/pwm.1.xml	Wed Sep 06 16:41:58 2017 +0200
     6.2 +++ b/pwm.1.xml	Thu Sep 07 12:40:50 2017 +0200
     6.3 @@ -34,7 +34,7 @@
     6.4        <email>guido+pwm@berhoerster.name</email>
     6.5        <personblurb/>
     6.6      </author>
     6.7 -    <date>6 September, 2017</date>
     6.8 +    <date>7 September, 2017</date>
     6.9    </info>
    6.10    <refmeta>
    6.11      <refentrytitle>pwm</refentrytitle>
    6.12 @@ -240,6 +240,9 @@
    6.13              <para>Create a new entry assigning each given
    6.14              <replaceable>field</replaceable> to the corresponsing
    6.15              <replaceable>value</replaceable>.</para>
    6.16 +            <para>If no fields are specified in interactive mode,
    6.17 +            <command>pwm</command> will prompt the user for the content of
    6.18 +            each field.</para>
    6.19            </listitem>
    6.20          </varlistentry>
    6.21          <varlistentry>
    6.22 @@ -268,6 +271,10 @@
    6.23              <replaceable>id</replaceable> assigning each given
    6.24              <replaceable>field</replaceable> to the corresponsing
    6.25              <replaceable>value</replaceable>.</para>
    6.26 +            <para>If no fields are specified and <command>pwm</command> is
    6.27 +            running in interactive mode, it will prompt the user for the
    6.28 +            content of each field, allowing him to edit any previous
    6.29 +            content.</para>
    6.30            </listitem>
    6.31          </varlistentry>
    6.32          <varlistentry>
    6.33 @@ -374,6 +381,9 @@
    6.34              </cmdsynopsis>
    6.35              <para>Create a new empty group named
    6.36              <replaceable>name</replaceable>.</para>
    6.37 +            <para>In interactive-mode the <replaceable>name</replaceable>
    6.38 +            argument is optional, if it is not specified <command>pwm</command>
    6.39 +            will prompt the user for it.</para>
    6.40            </listitem>
    6.41          </varlistentry>
    6.42          <varlistentry>
     7.1 --- a/pwm.c	Wed Sep 06 16:41:58 2017 +0200
     7.2 +++ b/pwm.c	Thu Sep 07 12:40:50 2017 +0200
     7.3 @@ -101,7 +101,7 @@
     7.4  }
     7.5  
     7.6  static int
     7.7 -run_input_loop(struct pwm_ctx *ctx, int is_interactive)
     7.8 +run_input_loop(struct pwm_ctx *ctx)
     7.9  {
    7.10  	int		retval = -1;
    7.11  	char		prompt[8 + 2 + 1];
    7.12 @@ -141,7 +141,7 @@
    7.13  			fprintf(stderr, "line too long\n");
    7.14  			goto out;
    7.15  		case IO_EOF:
    7.16 -			if (is_interactive) {
    7.17 +			if (ctx->is_interactive) {
    7.18  				/* treat as "q" command */
    7.19  				strcpy(buf, "q\n");
    7.20  				io_retval = IO_OK;
    7.21 @@ -164,13 +164,13 @@
    7.22  			err(1, "tok_tokenize");
    7.23  		case TOK_ERR_UNTERMINATED_QUOTE:
    7.24  			fprintf(stderr, "unterminated quote\n");
    7.25 -			if (!is_interactive) {
    7.26 +			if (!ctx->is_interactive) {
    7.27  				goto out;
    7.28  			}
    7.29  			goto next;
    7.30  		case TOK_ERR_TRAILING_BACKSLASH:
    7.31  			fprintf(stderr, "trailing backslash\n");
    7.32 -			if (!is_interactive) {
    7.33 +			if (!ctx->is_interactive) {
    7.34  				goto out;
    7.35  			}
    7.36  			goto next;
    7.37 @@ -185,7 +185,7 @@
    7.38  		cmd = cmd_match(argv[0]);
    7.39  		if (cmd == NULL) {
    7.40  			pwm_err(ctx, "unknown command: %s", argv[0]);
    7.41 -			if (is_interactive) {
    7.42 +			if (ctx->is_interactive) {
    7.43  				goto next;
    7.44  			} else {
    7.45  				goto out;
    7.46 @@ -198,7 +198,7 @@
    7.47  		case CMD_USAGE:
    7.48  			fprintf(stderr, "usage: %s\n", cmd->usage);
    7.49  		case CMD_ERR:	/* FALLTHROUGH */
    7.50 -			if (!is_interactive) {
    7.51 +			if (!ctx->is_interactive) {
    7.52  				goto out;
    7.53  			}
    7.54  			break;
    7.55 @@ -310,7 +310,6 @@
    7.56  {
    7.57  	int		status = EXIT_FAILURE;
    7.58  	char		*locale;
    7.59 -	int		is_interactive;
    7.60  	int		errflag = 0;
    7.61  	int		c;
    7.62  	const char	*master_password_filename = NULL;
    7.63 @@ -343,7 +342,7 @@
    7.64  		goto out;
    7.65  	}
    7.66  
    7.67 -	is_interactive = isatty(STDIN_FILENO);
    7.68 +	ctx.is_interactive = isatty(STDIN_FILENO);
    7.69  
    7.70  	while (!errflag && (c = getopt(argc, argv, "P:h")) != -1) {
    7.71  		switch (c) {
    7.72 @@ -386,7 +385,7 @@
    7.73  		goto out;
    7.74  	}
    7.75  
    7.76 -	if (is_interactive) {
    7.77 +	if (ctx.is_interactive) {
    7.78  		printf("pwm version %s\n", VERSION);
    7.79  	} else if (master_password_filename == NULL) {
    7.80  		fprintf(stderr, "master password file must be specified when "
    7.81 @@ -419,7 +418,7 @@
    7.82  	}
    7.83  
    7.84  	/* run main input loop */
    7.85 -	status = (run_input_loop(&ctx, is_interactive) != 0);
    7.86 +	status = (run_input_loop(&ctx) != 0);
    7.87  
    7.88  out:
    7.89  	pwfile_destroy(&ctx);
     8.1 --- a/pwm.h	Wed Sep 06 16:41:58 2017 +0200
     8.2 +++ b/pwm.h	Thu Sep 07 12:40:50 2017 +0200
     8.3 @@ -44,6 +44,7 @@
     8.4  #endif /* !PWM_HISTORY_MAX */
     8.5  
     8.6  struct pwm_ctx {
     8.7 +	int		is_interactive;
     8.8  	const char	*prev_cmd;
     8.9  	char		*errmsg;
    8.10  	char		*dirname;