/* $OpenBSD$ */

/*
* Copyright (c) 2010 Nicholas Marriott <[email protected]>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#include <sys/types.h>

#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "tmux.h"

/*
* Manipulate command arguments.
*/

/* List of argument values. */
TAILQ_HEAD(args_values, args_value);

/* Single arguments flag. */
struct args_entry {
       u_char                   flag;
       struct args_values       values;
       u_int                    count;

       int                      flags;
#define ARGS_ENTRY_OPTIONAL_VALUE 0x1

       RB_ENTRY(args_entry)     entry;
};

/* Parsed argument flags and values. */
struct args {
       struct args_tree         tree;
       u_int                    count;
       struct args_value       *values;
};

/* Prepared command state. */
struct args_command_state {
       struct cmd_list         *cmdlist;
       char                    *cmd;
       struct cmd_parse_input   pi;
};

static struct args_entry        *args_find(struct args *, u_char);

static int      args_cmp(struct args_entry *, struct args_entry *);
RB_GENERATE_STATIC(args_tree, args_entry, entry, args_cmp);

/* Arguments tree comparison function. */
static int
args_cmp(struct args_entry *a1, struct args_entry *a2)
{
       return (a1->flag - a2->flag);
}

/* Find a flag in the arguments tree. */
static struct args_entry *
args_find(struct args *args, u_char flag)
{
       struct args_entry       entry;

       entry.flag = flag;
       return (RB_FIND(args_tree, &args->tree, &entry));
}

/* Copy value. */
static void
args_copy_value(struct args_value *to, struct args_value *from)
{
       to->type = from->type;
       switch (from->type) {
       case ARGS_NONE:
               break;
       case ARGS_COMMANDS:
               to->cmdlist = from->cmdlist;
               to->cmdlist->references++;
               break;
       case ARGS_STRING:
               to->string = xstrdup(from->string);
               break;
       }
}

/* Type to string. */
static const char *
args_type_to_string (enum args_type type)
{
       switch (type)
       {
       case ARGS_NONE:
               return "NONE";
       case ARGS_STRING:
               return "STRING";
       case ARGS_COMMANDS:
               return "COMMANDS";
       }
       return "INVALID";
}

/* Get value as string. */
static const char *
args_value_as_string(struct args_value *value)
{
       switch (value->type) {
       case ARGS_NONE:
               return ("");
       case ARGS_COMMANDS:
               if (value->cached == NULL)
                       value->cached = cmd_list_print(value->cmdlist, 0);
               return (value->cached);
       case ARGS_STRING:
               return (value->string);
       }
       fatalx("unexpected argument type");
}

/* Create an empty arguments set. */
struct args *
args_create(void)
{
       struct args      *args;

       args = xcalloc(1, sizeof *args);
       RB_INIT(&args->tree);
       return (args);
}

/* Parse a single flag. */
static int
args_parse_flag_argument(struct args_value *values, u_int count, char **cause,
   struct args *args, u_int *i, const char *string, int flag,
   int optional_argument)
{
       struct args_value       *argument, *new;
       const char              *s;

       new = xcalloc(1, sizeof *new);
       if (*string != '\0') {
               new->type = ARGS_STRING;
               new->string = xstrdup(string);
               goto out;
       }

       if (*i == count)
               argument = NULL;
       else {
               argument = &values[*i];
               if (argument->type != ARGS_STRING) {
                       xasprintf(cause, "-%c argument must be a string", flag);
                       args_free_value(new);
                       free(new);
                       return (-1);
               }
       }
       if (argument == NULL) {
               args_free_value(new);
               free(new);
               if (optional_argument) {
                       log_debug("%s: -%c (optional)", __func__, flag);
                       args_set(args, flag, NULL, ARGS_ENTRY_OPTIONAL_VALUE);
                       return (0); /* either - or end */
               }
               xasprintf(cause, "-%c expects an argument", flag);
               return (-1);
       }
       args_copy_value(new, argument);
       (*i)++;

out:
       s = args_value_as_string(new);
       log_debug("%s: -%c = %s", __func__, flag, s);
       args_set(args, flag, new, 0);
       return (0);
}

/* Parse flags argument. */
static int
args_parse_flags(const struct args_parse *parse, struct args_value *values,
   u_int count, char **cause, struct args *args, u_int *i)
{
       struct args_value       *value;
       u_char                   flag;
       const char              *found, *string;
       int                      optional_argument;

       value = &values[*i];
       if (value->type != ARGS_STRING)
               return (1);

       string = value->string;
       log_debug("%s: next %s", __func__, string);
       if (*string++ != '-' || *string == '\0')
               return (1);
       (*i)++;
       if (string[0] == '-' && string[1] == '\0')
               return (1);

       for (;;) {
               flag = *string++;
               if (flag == '\0')
                       return (0);
               if (flag == '?')
                       return (-1);
               if (!isalnum(flag)) {
                       xasprintf(cause, "invalid flag -%c", flag);
                       return (-1);
               }

               found = strchr(parse->template, flag);
               if (found == NULL) {
                       xasprintf(cause, "unknown flag -%c", flag);
                       return (-1);
               }
               if (found[1] != ':') {
                       log_debug("%s: -%c", __func__, flag);
                       args_set(args, flag, NULL, 0);
                       continue;
               }
               optional_argument = (found[2] == ':');
               return (args_parse_flag_argument(values, count, cause, args, i,
                   string, flag, optional_argument));
       }
}

/* Parse arguments into a new argument set. */
struct args *
args_parse(const struct args_parse *parse, struct args_value *values,
   u_int count, char **cause)
{
       struct args             *args;
       u_int                    i;
       enum args_parse_type     type;
       struct args_value       *value, *new;
       const char              *s;
       int                      stop;

       if (count == 0)
               return (args_create());

       args = args_create();
       for (i = 1; i < count; /* nothing */) {
               stop = args_parse_flags(parse, values, count, cause, args, &i);
               if (stop == -1) {
                       args_free(args);
                       return (NULL);
               }
               if (stop == 1)
                       break;
       }
       log_debug("%s: flags end at %u of %u", __func__, i, count);
       if (i != count) {
               for (/* nothing */; i < count; i++) {
                       value = &values[i];

                       s = args_value_as_string(value);
                       log_debug("%s: %u = %s (type %s)", __func__, i, s,
                           args_type_to_string (value->type));

                       if (parse->cb != NULL) {
                               type = parse->cb(args, args->count, cause);
                               if (type == ARGS_PARSE_INVALID) {
                                       args_free(args);
                                       return (NULL);
                               }
                       } else
                               type = ARGS_PARSE_STRING;

                       args->values = xrecallocarray(args->values,
                           args->count, args->count + 1, sizeof *args->values);
                       new = &args->values[args->count++];

                       switch (type) {
                       case ARGS_PARSE_INVALID:
                               fatalx("unexpected argument type");
                       case ARGS_PARSE_STRING:
                               if (value->type != ARGS_STRING) {
                                       xasprintf(cause,
                                           "argument %u must be \"string\"",
                                           args->count);
                                       args_free(args);
                                       return (NULL);
                               }
                               args_copy_value(new, value);
                               break;
                       case ARGS_PARSE_COMMANDS_OR_STRING:
                               args_copy_value(new, value);
                               break;
                       case ARGS_PARSE_COMMANDS:
                               if (value->type != ARGS_COMMANDS) {
                                       xasprintf(cause,
                                           "argument %u must be { commands }",
                                           args->count);
                                       args_free(args);
                                       return (NULL);
                               }
                               args_copy_value(new, value);
                               break;
                       }
               }
       }

       if (parse->lower != -1 && args->count < (u_int)parse->lower) {
               xasprintf(cause,
                   "too few arguments (need at least %u)",
                   parse->lower);
               args_free(args);
               return (NULL);
       }
       if (parse->upper != -1 && args->count > (u_int)parse->upper) {
               xasprintf(cause,
                   "too many arguments (need at most %u)",
                   parse->upper);
               args_free(args);
               return (NULL);
       }
       return (args);
}

/* Copy and expand a value. */
static void
args_copy_copy_value(struct args_value *to, struct args_value *from, int argc,
   char **argv)
{
       char    *s, *expanded;
       int      i;

       to->type = from->type;
       switch (from->type) {
       case ARGS_NONE:
               break;
       case ARGS_STRING:
               expanded = xstrdup(from->string);
               for (i = 0; i < argc; i++) {
                       s = cmd_template_replace(expanded, argv[i], i + 1);
                       free(expanded);
                       expanded = s;
               }
               to->string = expanded;
               break;
       case ARGS_COMMANDS:
               to->cmdlist = cmd_list_copy(from->cmdlist, argc, argv);
               break;
       }
}

/* Copy an arguments set. */
struct args *
args_copy(struct args *args, int argc, char **argv)
{
       struct args             *new_args;
       struct args_entry       *entry;
       struct args_value       *value, *new_value;
       u_int                    i;

       cmd_log_argv(argc, argv, "%s", __func__);

       new_args = args_create();
       RB_FOREACH(entry, args_tree, &args->tree) {
               if (TAILQ_EMPTY(&entry->values)) {
                       for (i = 0; i < entry->count; i++)
                               args_set(new_args, entry->flag, NULL, 0);
                       continue;
               }
               TAILQ_FOREACH(value, &entry->values, entry) {
                       new_value = xcalloc(1, sizeof *new_value);
                       args_copy_copy_value(new_value, value, argc, argv);
                       args_set(new_args, entry->flag, new_value, 0);
               }
       }
       if (args->count == 0)
               return (new_args);
       new_args->count = args->count;
       new_args->values = xcalloc(args->count, sizeof *new_args->values);
       for (i = 0; i < args->count; i++) {
               new_value = &new_args->values[i];
               args_copy_copy_value(new_value, &args->values[i], argc, argv);
       }
       return (new_args);
}

/* Free a value. */
void
args_free_value(struct args_value *value)
{
       switch (value->type) {
       case ARGS_NONE:
               break;
       case ARGS_STRING:
               free(value->string);
               break;
       case ARGS_COMMANDS:
               cmd_list_free(value->cmdlist);
               break;
       }
       free(value->cached);
}

/* Free values. */
void
args_free_values(struct args_value *values, u_int count)
{
       u_int   i;

       for (i = 0; i < count; i++)
               args_free_value(&values[i]);
}

/* Free an arguments set. */
void
args_free(struct args *args)
{
       struct args_entry       *entry;
       struct args_entry       *entry1;
       struct args_value       *value;
       struct args_value       *value1;

       args_free_values(args->values, args->count);
       free(args->values);

       RB_FOREACH_SAFE(entry, args_tree, &args->tree, entry1) {
               RB_REMOVE(args_tree, &args->tree, entry);
               TAILQ_FOREACH_SAFE(value, &entry->values, entry, value1) {
                       TAILQ_REMOVE(&entry->values, value, entry);
                       args_free_value(value);
                       free(value);
               }
               free(entry);
       }

       free(args);
}

/* Convert arguments to vector. */
void
args_to_vector(struct args *args, int *argc, char ***argv)
{
       char    *s;
       u_int    i;

       *argc = 0;
       *argv = NULL;

       for (i = 0; i < args->count; i++) {
               switch (args->values[i].type) {
               case ARGS_NONE:
                       break;
               case ARGS_STRING:
                       cmd_append_argv(argc, argv, args->values[i].string);
                       break;
               case ARGS_COMMANDS:
                       s = cmd_list_print(args->values[i].cmdlist, 0);
                       cmd_append_argv(argc, argv, s);
                       free(s);
                       break;
               }
       }
}

/* Convert arguments from vector. */
struct args_value *
args_from_vector(int argc, char **argv)
{
       struct args_value       *values;
       int                      i;

       values = xcalloc(argc, sizeof *values);
       for (i = 0; i < argc; i++) {
               values[i].type = ARGS_STRING;
               values[i].string = xstrdup(argv[i]);
       }
       return (values);
}

/* Add to string. */
static void printflike(3, 4)
args_print_add(char **buf, size_t *len, const char *fmt, ...)
{
       va_list  ap;
       char    *s;
       size_t   slen;

       va_start(ap, fmt);
       slen = xvasprintf(&s, fmt, ap);
       va_end(ap);

       *len += slen;
       *buf = xrealloc(*buf, *len);

       strlcat(*buf, s, *len);
       free(s);
}

/* Add value to string. */
static void
args_print_add_value(char **buf, size_t *len, struct args_value *value)
{
       char    *expanded = NULL;

       if (**buf != '\0')
               args_print_add(buf, len, " ");

       switch (value->type) {
       case ARGS_NONE:
               break;
       case ARGS_COMMANDS:
               expanded = cmd_list_print(value->cmdlist, 0);
               args_print_add(buf, len, "{ %s }", expanded);
               break;
       case ARGS_STRING:
               expanded = args_escape(value->string);
               args_print_add(buf, len, "%s", expanded);
               break;
       }
       free(expanded);
}

/* Print a set of arguments. */
char *
args_print(struct args *args)
{
       size_t                   len;
       char                    *buf;
       u_int                    i, j;
       struct args_entry       *entry;
       struct args_entry       *last = NULL;
       struct args_value       *value;

       len = 1;
       buf = xcalloc(1, len);

       /* Process the flags first. */
       RB_FOREACH(entry, args_tree, &args->tree) {
               if (entry->flags & ARGS_ENTRY_OPTIONAL_VALUE)
                       continue;
               if (!TAILQ_EMPTY(&entry->values))
                       continue;

               if (*buf == '\0')
                       args_print_add(&buf, &len, "-");
               for (j = 0; j < entry->count; j++)
                       args_print_add(&buf, &len, "%c", entry->flag);
       }

       /* Then the flags with arguments. */
       RB_FOREACH(entry, args_tree, &args->tree) {
               if (entry->flags & ARGS_ENTRY_OPTIONAL_VALUE) {
                       if (*buf != '\0')
                               args_print_add(&buf, &len, " -%c", entry->flag);
                       else
                               args_print_add(&buf, &len, "-%c", entry->flag);
                       last = entry;
                       continue;
               }
               if (TAILQ_EMPTY(&entry->values))
                       continue;
               TAILQ_FOREACH(value, &entry->values, entry) {
                       if (*buf != '\0')
                               args_print_add(&buf, &len, " -%c", entry->flag);
                       else
                               args_print_add(&buf, &len, "-%c", entry->flag);
                       args_print_add_value(&buf, &len, value);
               }
               last = entry;
       }
       if (last && (last->flags & ARGS_ENTRY_OPTIONAL_VALUE))
               args_print_add(&buf, &len, " --");

       /* And finally the argument vector. */
       for (i = 0; i < args->count; i++)
               args_print_add_value(&buf, &len, &args->values[i]);

       return (buf);
}

/* Escape an argument. */
char *
args_escape(const char *s)
{
       static const char        dquoted[] = " #';${}%";
       static const char        squoted[] = " \"";
       char                    *escaped, *result;
       int                      flags, quotes = 0;

       if (*s == '\0') {
               xasprintf(&result, "''");
               return (result);
       }
       if (s[strcspn(s, dquoted)] != '\0')
               quotes = '"';
       else if (s[strcspn(s, squoted)] != '\0')
               quotes = '\'';

       if (s[0] != ' ' &&
           s[1] == '\0' &&
           (quotes != 0 || s[0] == '~')) {
               xasprintf(&escaped, "\\%c", s[0]);
               return (escaped);
       }

       flags = VIS_OCTAL|VIS_CSTYLE|VIS_TAB|VIS_NL;
       if (quotes == '"')
               flags |= VIS_DQ;
       utf8_stravis(&escaped, s, flags);

       if (quotes == '\'')
               xasprintf(&result, "'%s'", escaped);
       else if (quotes == '"') {
               if (*escaped == '~')
                       xasprintf(&result, "\"\\%s\"", escaped);
               else
                       xasprintf(&result, "\"%s\"", escaped);
       } else {
               if (*escaped == '~')
                       xasprintf(&result, "\\%s", escaped);
               else
                       result = xstrdup(escaped);
       }
       free(escaped);
       return (result);
}

/* Return if an argument is present. */
int
args_has(struct args *args, u_char flag)
{
       struct args_entry       *entry;

       entry = args_find(args, flag);
       if (entry == NULL)
               return (0);
       return (entry->count);
}

/* Set argument value in the arguments tree. */
void
args_set(struct args *args, u_char flag, struct args_value *value, int flags)
{
       struct args_entry       *entry;

       entry = args_find(args, flag);
       if (entry == NULL) {
               entry = xcalloc(1, sizeof *entry);
               entry->flag = flag;
               entry->count = 1;
               entry->flags = flags;
               TAILQ_INIT(&entry->values);
               RB_INSERT(args_tree, &args->tree, entry);
       } else
               entry->count++;
       if (value != NULL && value->type != ARGS_NONE)
               TAILQ_INSERT_TAIL(&entry->values, value, entry);
       else
               free(value);
}

/* Get argument value. Will be NULL if it isn't present. */
const char *
args_get(struct args *args, u_char flag)
{
       struct args_entry       *entry;

       if ((entry = args_find(args, flag)) == NULL)
               return (NULL);
       if (TAILQ_EMPTY(&entry->values))
               return (NULL);
       return (TAILQ_LAST(&entry->values, args_values)->string);
}

/* Get first argument. */
u_char
args_first(struct args *args, struct args_entry **entry)
{
       *entry = RB_MIN(args_tree, &args->tree);
       if (*entry == NULL)
               return (0);
       return ((*entry)->flag);
}

/* Get next argument. */
u_char
args_next(struct args_entry **entry)
{
       *entry = RB_NEXT(args_tree, &args->tree, *entry);
       if (*entry == NULL)
               return (0);
       return ((*entry)->flag);
}

/* Get argument count. */
u_int
args_count(struct args *args)
{
       return (args->count);
}

/* Get argument values. */
struct args_value *
args_values(struct args *args)
{
       return (args->values);
}

/* Get argument value. */
struct args_value *
args_value(struct args *args, u_int idx)
{
       if (idx >= args->count)
               return (NULL);
       return (&args->values[idx]);
}

/* Return argument as string. */
const char *
args_string(struct args *args, u_int idx)
{
       if (idx >= args->count)
               return (NULL);
       return (args_value_as_string(&args->values[idx]));
}

/* Make a command now. */
struct cmd_list *
args_make_commands_now(struct cmd *self, struct cmdq_item *item, u_int idx,
   int expand)
{
       struct args_command_state       *state;
       char                            *error;
       struct cmd_list                 *cmdlist;

       state = args_make_commands_prepare(self, item, idx, NULL, 0, expand);
       cmdlist = args_make_commands(state, 0, NULL, &error);
       if (cmdlist == NULL) {
               cmdq_error(item, "%s", error);
               free(error);
       }
       else
               cmdlist->references++;
       args_make_commands_free(state);
       return (cmdlist);
}

/* Save bits to make a command later. */
struct args_command_state *
args_make_commands_prepare(struct cmd *self, struct cmdq_item *item, u_int idx,
   const char *default_command, int wait, int expand)
{
       struct args                     *args = cmd_get_args(self);
       struct cmd_find_state           *target = cmdq_get_target(item);
       struct client                   *tc = cmdq_get_target_client(item);
       struct args_value               *value;
       struct args_command_state       *state;
       const char                      *cmd;
       const char                      *file;

       state = xcalloc(1, sizeof *state);

       if (idx < args->count) {
               value = &args->values[idx];
               if (value->type == ARGS_COMMANDS) {
                       state->cmdlist = value->cmdlist;
                       state->cmdlist->references++;
                       return (state);
               }
               cmd = value->string;
       } else {
               if (default_command == NULL)
                       fatalx("argument out of range");
               cmd = default_command;
       }


       if (expand)
               state->cmd = format_single_from_target(item, cmd);
       else
               state->cmd = xstrdup(cmd);
       log_debug("%s: %s", __func__, state->cmd);

       if (wait)
               state->pi.item = item;
       cmd_get_source(self, &file, &state->pi.line);
       if (file != NULL)
               state->pi.file = xstrdup(file);
       state->pi.c = tc;
       if (state->pi.c != NULL)
               state->pi.c->references++;
       cmd_find_copy_state(&state->pi.fs, target);

       return (state);
}

/* Return argument as command. */
struct cmd_list *
args_make_commands(struct args_command_state *state, int argc, char **argv,
   char **error)
{
       struct cmd_parse_result *pr;
       char                    *cmd, *new_cmd;
       int                      i;

       if (state->cmdlist != NULL) {
               if (argc == 0)
                       return (state->cmdlist);
               return (cmd_list_copy(state->cmdlist, argc, argv));
       }

       cmd = xstrdup(state->cmd);
       log_debug("%s: %s", __func__, cmd);
       cmd_log_argv(argc, argv, __func__);
       for (i = 0; i < argc; i++) {
               new_cmd = cmd_template_replace(cmd, argv[i], i + 1);
               log_debug("%s: %%%u %s: %s", __func__, i + 1, argv[i], new_cmd);
               free(cmd);
               cmd = new_cmd;
       }
       log_debug("%s: %s", __func__, cmd);

       pr = cmd_parse_from_string(cmd, &state->pi);
       free(cmd);
       switch (pr->status) {
       case CMD_PARSE_ERROR:
               *error = pr->error;
               return (NULL);
       case CMD_PARSE_SUCCESS:
               return (pr->cmdlist);
       }
       fatalx("invalid parse return state");
}

/* Free commands state. */
void
args_make_commands_free(struct args_command_state *state)
{
       if (state->cmdlist != NULL)
               cmd_list_free(state->cmdlist);
       if (state->pi.c != NULL)
               server_client_unref(state->pi.c);
       free(__UNCONST(state->pi.file));
       free(state->cmd);
       free(state);
}

/* Get prepared command. */
char *
args_make_commands_get_command(struct args_command_state *state)
{
       struct cmd      *first;
       int              n;
       char            *s;

       if (state->cmdlist != NULL) {
               first = cmd_list_first(state->cmdlist);
               if (first == NULL)
                       return (xstrdup(""));
               return (xstrdup(cmd_get_entry(first)->name));
       }
       n = strcspn(state->cmd, " ,");
       xasprintf(&s, "%.*s", n, state->cmd);
       return (s);
}

/* Get first value in argument. */
struct args_value *
args_first_value(struct args *args, u_char flag)
{
       struct args_entry       *entry;

       if ((entry = args_find(args, flag)) == NULL)
               return (NULL);
       return (TAILQ_FIRST(&entry->values));
}

/* Get next value in argument. */
struct args_value *
args_next_value(struct args_value *value)
{
       return (TAILQ_NEXT(value, entry));
}

/* Convert an argument value to a number. */
long long
args_strtonum(struct args *args, u_char flag, long long minval,
   long long maxval, char **cause)
{
       const char              *errstr;
       long long                ll;
       struct args_entry       *entry;
       struct args_value       *value;

       if ((entry = args_find(args, flag)) == NULL) {
               *cause = xstrdup("missing");
               return (0);
       }
       value = TAILQ_LAST(&entry->values, args_values);
       if (value == NULL ||
           value->type != ARGS_STRING ||
           value->string == NULL) {
               *cause = xstrdup("missing");
               return (0);
       }

       ll = strtonum(value->string, minval, maxval, &errstr);
       if (errstr != NULL) {
               *cause = xstrdup(errstr);
               return (0);
       }

       *cause = NULL;
       return (ll);
}

/* Convert an argument value to a number, and expand formats. */
long long
args_strtonum_and_expand(struct args *args, u_char flag, long long minval,
   long long maxval, struct cmdq_item *item, char **cause)
{
       const char              *errstr;
       char                    *formatted;
       long long                ll;
       struct args_entry       *entry;
       struct args_value       *value;

       if ((entry = args_find(args, flag)) == NULL) {
               *cause = xstrdup("missing");
               return (0);
       }
       value = TAILQ_LAST(&entry->values, args_values);
       if (value == NULL ||
           value->type != ARGS_STRING ||
           value->string == NULL) {
               *cause = xstrdup("missing");
               return (0);
       }

       formatted = format_single_from_target(item, value->string);
       ll = strtonum(formatted, minval, maxval, &errstr);
       free(formatted);
       if (errstr != NULL) {
               *cause = xstrdup(errstr);
               return (0);
       }

       *cause = NULL;
       return (ll);
}

/* Convert an argument to a number which may be a percentage. */
long long
args_percentage(struct args *args, u_char flag, long long minval,
   long long maxval, long long curval, char **cause)
{
       const char              *value;
       struct args_entry       *entry;

       if ((entry = args_find(args, flag)) == NULL) {
               *cause = xstrdup("missing");
               return (0);
       }
       if (TAILQ_EMPTY(&entry->values)) {
               *cause = xstrdup("empty");
               return (0);
       }
       value = TAILQ_LAST(&entry->values, args_values)->string;
       return (args_string_percentage(value, minval, maxval, curval, cause));
}

/* Convert a string to a number which may be a percentage. */
long long
args_string_percentage(const char *value, long long minval, long long maxval,
   long long curval, char **cause)
{
       const char      *errstr;
       long long        ll;
       size_t           valuelen = strlen(value);
       char            *copy;

       if (valuelen == 0) {
               *cause = xstrdup("empty");
               return (0);
       }
       if (value[valuelen - 1] == '%') {
               copy = xstrdup(value);
               copy[valuelen - 1] = '\0';

               ll = strtonum(copy, 0, 100, &errstr);
               free(copy);
               if (errstr != NULL) {
                       *cause = xstrdup(errstr);
                       return (0);
               }
               ll = (curval * ll) / 100;
               if (ll < minval) {
                       *cause = xstrdup("too small");
                       return (0);
               }
               if (ll > maxval) {
                       *cause = xstrdup("too large");
                       return (0);
               }
       } else {
               ll = strtonum(value, minval, maxval, &errstr);
               if (errstr != NULL) {
                       *cause = xstrdup(errstr);
                       return (0);
               }
       }

       *cause = NULL;
       return (ll);
}

/*
* Convert an argument to a number which may be a percentage, and expand
* formats.
*/
long long
args_percentage_and_expand(struct args *args, u_char flag, long long minval,
   long long maxval, long long curval, struct cmdq_item *item, char **cause)
{
       const char              *value;
       struct args_entry       *entry;

       if ((entry = args_find(args, flag)) == NULL) {
               *cause = xstrdup("missing");
               return (0);
       }
       if (TAILQ_EMPTY(&entry->values)) {
               *cause = xstrdup("empty");
               return (0);
       }
       value = TAILQ_LAST(&entry->values, args_values)->string;
       return (args_string_percentage_and_expand(value, minval, maxval, curval,
                   item, cause));
}

/*
* Convert a string to a number which may be a percentage, and expand formats.
*/
long long
args_string_percentage_and_expand(const char *value, long long minval,
   long long maxval, long long curval, struct cmdq_item *item, char **cause)
{
       const char      *errstr;
       long long        ll;
       size_t           valuelen = strlen(value);
       char            *copy, *f;

       if (value[valuelen - 1] == '%') {
               copy = xstrdup(value);
               copy[valuelen - 1] = '\0';

               f = format_single_from_target(item, copy);
               ll = strtonum(f, 0, 100, &errstr);
               free(f);
               free(copy);
               if (errstr != NULL) {
                       *cause = xstrdup(errstr);
                       return (0);
               }
               ll = (curval * ll) / 100;
               if (ll < minval) {
                       *cause = xstrdup("too small");
                       return (0);
               }
               if (ll > maxval) {
                       *cause = xstrdup("too large");
                       return (0);
               }
       } else {
               f = format_single_from_target(item, value);
               ll = strtonum(f, minval, maxval, &errstr);
               free(f);
               if (errstr != NULL) {
                       *cause = xstrdup(errstr);
                       return (0);
               }
       }

       *cause = NULL;
       return (ll);
}