add ability for argparse to validate args

Fixes #4211
This commit is contained in:
Kurtis Rader 2017-07-16 21:10:22 -07:00
parent 2dfb615d7b
commit 4e1303823b
6 changed files with 198 additions and 29 deletions

View file

@ -74,10 +74,28 @@ Each option specification is a string composed of
- `=+` if it requires a value each instance of the flag is saved.
- Optionally a `!` followed by fish script to validate the value. Typically this will be a function to run. If the return status is zero the value for the flag is valid. If non-zero the value is invalid. Any error messages should be written to stdout (not stderr). See the section on <a href="#arparse-validation">Flag Value Validation</a> for more information.
See the <a href="#fish-opt">`fish_opt`</a> command for a friendlier but more verbose way to create option specifications.
In the following examples if a flag is not seen when parsing the arguments then the corresponding _flag_X var(s) will not be set.
\subsection argparse-validation Flag Value Validation
It is common to want to validate the the value provided for an option satisfies some criteria. For example, that it is a valid integer within a specific range. You can always do this after `argparse` returns but you can also request that `argparse` perform the validation by executing arbitrary fish script. To do so simply append an `!` (exclamation-mark) then the fish script to be run. When that code is executed three vars will be defined:
- `_argparse_cmd` will be set to the value of the value of the `argparse --name` value.
- `_flag_name` will be set to the short or long flag that being processed.
- `_flag_value` will be set to the value associated with the flag being processed.
If you do this via a function it should be defined with the `--no-scope-shadowing` flag. Otherwise it won't have access to those variables.
The script should write any error messages to stdout, not stderr. It should return a status of zero if the flag value is valid otherwise a non-zero status to indicate it is invalid.
Fish ships with a `_validate_int` function that accepts a `--min` and `--max` flag. Let's say your command accepts a `-m` or `--max` flag and the minimum allowable value is zero and the maximum is 5. You would define the option like this: `m/max=!_validate_int --min 0 --max 5`. The default if you just call `_validate_int` without those flags is to simply check that the value is a valid integer with no limits on the min or max value allowed.
\subsection argparse-optspec-examples Example OPTION_SPECs
Some OPTION_SPEC examples:

View file

@ -0,0 +1,32 @@
# This function is intended to be used as a validation command for individual option specifications
# given to the `argparse` command. It checks that the argument is a valid integer and optionally
# whether it is in a reasonable range.
function _validate_int --no-scope-shadowing
# Note that we can't use ourself to validate the arguments to the --min and --max flags this
# function recognizes.
set -l options 'm-min=' 'x-max='
argparse -n _argparse_validate_int $options -- $argv
or return
if not string match -qr '^-?\d+$' -- $_flag_value
set -l msg (_ "%s: Value '%s' for flag '%s' is not an integer\n")
printf $msg $_argparse_cmd $_flag_value $_flag_name
return 1
end
if set -q _flag_min
and test $_flag_value -lt $_flag_min
set -l msg (_ "%s: Value '%s' for flag '%s' less than min allowed of '%s'\n")
printf $msg $_argparse_cmd $_flag_value $_flag_name $_flag_min
return 1
end
if set -q _flag_max
and test $_flag_value -gt $_flag_max
set -l msg (_ "%s: Value '%s' for flag '%s' greater than max allowed of '%s'\n")
printf $msg $_argparse_cmd $_flag_value $_flag_name $_flag_max
return 1
end
return 0
end

View file

@ -24,12 +24,14 @@
#include "builtin_argparse.h"
#include "common.h"
#include "env.h"
#include "exec.h"
#include "fallback.h" // IWYU pragma: keep
#include "io.h"
#include "wgetopt.h" // IWYU pragma: keep
#include "wutil.h" // IWYU pragma: keep
class parser_t;
const wcstring var_name_prefix = L"_flag_";
#define BUILTIN_ERR_INVALID_OPT_SPEC _(L"%ls: Invalid option spec '%ls' at char '%lc'\n")
@ -37,13 +39,20 @@ class option_spec_t {
public:
wchar_t short_flag;
wcstring long_flag;
wcstring validation_command;
wcstring_list_t vals;
bool short_flag_valid;
int num_allowed;
int num_seen;
option_spec_t(wchar_t s)
: short_flag(s), long_flag(), vals(), short_flag_valid(true), num_allowed(0), num_seen(0) {}
: short_flag(s),
long_flag(),
validation_command(),
vals(),
short_flag_valid(true),
num_allowed(0),
num_seen(0) {}
};
class argparse_cmd_opts_t {
@ -174,9 +183,10 @@ static int parse_exclusive_args(argparse_cmd_opts_t &opts, io_streams_t &streams
static bool parse_flag_modifiers(argparse_cmd_opts_t &opts, option_spec_t *opt_spec,
const wcstring &option_spec, const wchar_t *s,
io_streams_t &streams) {
if (opt_spec->short_flag == opts.implicit_int_flag && *s) {
streams.err.append_format(_(L"%ls: Implicit int short flag '%lc' does not allow modifiers like '%lc'\n"),
opts.name.c_str(), opt_spec->short_flag, *s);
if (opt_spec->short_flag == opts.implicit_int_flag && *s && *s != L'!') {
streams.err.append_format(
_(L"%ls: Implicit int short flag '%lc' does not allow modifiers like '%lc'\n"),
opts.name.c_str(), opt_spec->short_flag, *s);
return false;
}
@ -193,12 +203,20 @@ static bool parse_flag_modifiers(argparse_cmd_opts_t &opts, option_spec_t *opt_s
}
}
if (*s) {
if (*s == L'!') {
s++;
opt_spec->validation_command = wcstring(s);
} else if (*s) {
streams.err.append_format(BUILTIN_ERR_INVALID_OPT_SPEC, opts.name.c_str(),
option_spec.c_str(), *s);
return false;
}
// Make sure we have some validation for implicit int flags.
if (opt_spec->short_flag == opts.implicit_int_flag && opt_spec->validation_command.empty()) {
opt_spec->validation_command = L"_validate_int";
}
if (opts.options.find(opt_spec->short_flag) != opts.options.end()) {
streams.err.append_format(L"%ls: Short flag '%lc' already defined\n", opts.name.c_str(),
opt_spec->short_flag);
@ -234,8 +252,8 @@ static bool parse_option_spec(argparse_cmd_opts_t &opts, wcstring option_spec,
return false;
}
if (opts.implicit_int_flag) {
streams.err.append_format(
_(L"%ls: Implicit int flag '%lc' already defined\n"), opts.name.c_str(), opts.implicit_int_flag);
streams.err.append_format(_(L"%ls: Implicit int flag '%lc' already defined\n"),
opts.name.c_str(), opts.implicit_int_flag);
return false;
}
opts.implicit_int_flag = opt_spec->short_flag;
@ -248,8 +266,8 @@ static bool parse_option_spec(argparse_cmd_opts_t &opts, wcstring option_spec,
s++; // the struct is initialized assuming short_flag_valid should be true
} else if (*s == L'#') {
if (opts.implicit_int_flag) {
streams.err.append_format(
_(L"%ls: Implicit int flag '%lc' already defined\n"), opts.name.c_str(), opts.implicit_int_flag);
streams.err.append_format(_(L"%ls: Implicit int flag '%lc' already defined\n"),
opts.name.c_str(), opts.implicit_int_flag);
return false;
}
opts.implicit_int_flag = opt_spec->short_flag;
@ -417,37 +435,91 @@ static void update_bool_flag_counts(argparse_cmd_opts_t &opts) {
}
}
static int validate_arg(argparse_cmd_opts_t &opts, option_spec_t *opt_spec, bool is_long_flag,
const wchar_t *woptarg, io_streams_t &streams) {
// Obviously if there is no arg validation command we assume the arg is okay.
if (opt_spec->validation_command.empty()) return STATUS_CMD_OK;
wcstring_list_t cmd_output;
env_push(true);
env_set(L"_argparse_cmd", opts.name.c_str(), ENV_LOCAL);
if (is_long_flag) {
env_set(var_name_prefix + L"name", opt_spec->long_flag.c_str(), ENV_LOCAL);
} else {
env_set(var_name_prefix + L"name", wcstring(1, opt_spec->short_flag).c_str(), ENV_LOCAL);
}
env_set(var_name_prefix + L"value", woptarg, ENV_LOCAL);
int retval = exec_subshell(opt_spec->validation_command, cmd_output, false);
for (auto it : cmd_output) {
streams.err.append(it);
streams.err.push_back(L'\n');
}
env_pop();
return retval;
}
// Check if it is an implicit integer option. Try to parse and validate it as a valid integer. The
// code that parses option specs has already ensured that implicit integers have either an explicit
// or implicit validation function.
static int check_for_implicit_int(argparse_cmd_opts_t &opts, const wchar_t *val, const wchar_t *cmd,
const wchar_t *const *argv, wgetopter_t &w, int opt, int long_idx,
parser_t &parser, io_streams_t &streams) {
if (opts.implicit_int_flag == L'\0') {
// There is no implicit integer option so report an error.
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
return STATUS_INVALID_ARGS;
}
if (opt == '?') {
// It's a flag that might be an implicit integer flag. Try to parse it as an integer to
// decide if it is in fact something like `-NNN` or an invalid flag.
(void)fish_wcstol(val);
if (errno) {
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
return STATUS_INVALID_ARGS;
}
}
// Okay, it looks like an integer. See if it passes the validation checks.
auto found = opts.options.find(opts.implicit_int_flag);
assert(found != opts.options.end());
auto opt_spec = found->second;
int retval = validate_arg(opts, opt_spec, long_idx != -1, val, streams);
if (retval != STATUS_CMD_OK) return retval;
// It's a valid integer so store it and return success.
opt_spec->vals.clear();
opt_spec->vals.push_back(wcstring(val));
opt_spec->num_seen++;
w.nextchar = NULL;
return STATUS_CMD_OK;
}
static int argparse_parse_flags(argparse_cmd_opts_t &opts, const wchar_t *short_options,
const woption *long_options, const wchar_t *cmd, int argc,
wchar_t **argv, int *optind, parser_t &parser,
io_streams_t &streams) {
int opt;
int long_idx = -1;
wgetopter_t w;
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, NULL)) != -1) {
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, &long_idx)) != -1) {
if (opt == ':') {
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
return STATUS_INVALID_ARGS;
}
if (opt == '?') {
auto found = opts.options.find(opts.implicit_int_flag);
if (found != opts.options.end()) {
// Try to parse it as a number; e.g., "-123".
long x = fish_wcstol(argv[w.woptind - 1] + 1);
if (!errno) {
wchar_t val[20];
swprintf(val, sizeof val / sizeof val[0], L"%ld", x);
auto opt_spec = found->second;
opt_spec->vals.clear();
opt_spec->vals.push_back(wcstring(val));
opt_spec->num_seen++;
w.nextchar = NULL;
continue;
}
}
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
return STATUS_INVALID_ARGS;
// It's not a recognized flag. See if it's an implicit int flag.
int retval = check_for_implicit_int(opts, argv[w.woptind - 1] + 1, cmd, argv, w, opt,
long_idx, parser, streams);
if (retval != STATUS_CMD_OK) return retval;
long_idx = -1;
continue;
}
// It's a recognized flag.
auto found = opts.options.find(opt);
assert(found != opts.options.end());
@ -455,7 +527,16 @@ static int argparse_parse_flags(argparse_cmd_opts_t &opts, const wchar_t *short_
opt_spec->num_seen++;
if (opt_spec->num_allowed == 0) {
assert(!w.woptarg);
} else if (opt_spec->num_allowed == -1 || opt_spec->num_allowed == 1) {
long_idx = -1;
continue;
}
if (w.woptarg) {
int retval = validate_arg(opts, opt_spec, long_idx != -1, w.woptarg, streams);
if (retval != STATUS_CMD_OK) return retval;
}
if (opt_spec->num_allowed == -1 || opt_spec->num_allowed == 1) {
// We're depending on `wgetopt_long()` to report that a mandatory value is missing if
// `opt_spec->num_allowed == 1` and thus return ':' so that we don't take this branch if
// the mandatory arg is missing.
@ -467,6 +548,8 @@ static int argparse_parse_flags(argparse_cmd_opts_t &opts, const wchar_t *short_
assert(w.woptarg);
opt_spec->vals.push_back(w.woptarg);
}
long_idx = -1;
}
*optind = w.woptind;
@ -533,7 +616,6 @@ static void set_argparse_result_vars(argparse_cmd_opts_t &opts) {
option_spec_t *opt_spec = it.second;
if (!opt_spec->num_seen) continue;
wcstring var_name_prefix = L"_flag_";
auto val = list_to_array_val(opt_spec->vals);
if (opt_spec->short_flag_valid) {
env_set(var_name_prefix + opt_spec->short_flag, *val == ENV_NULL ? NULL : val->c_str(),

View file

@ -29,3 +29,9 @@ argparse: Long flag 'short' already defined
argparse: Implicit int flag '#' already defined
# Defining an implicit int flag with modifiers
argparse: Implicit int short flag 'v' does not allow modifiers like '='
# Implicit int flag validation fails
argparse: Value '765x' for flag 'max' is not an integer
argparse: Value 'a1' for flag 'm' is not an integer
# Explicit int flag validation
argparse: Value '2' for flag 'm' greater than max allowed of '1'
argparse: Value '-1' for flag 'max' less than min allowed of '0'

View file

@ -98,3 +98,30 @@ echo '# Should be set to 765'
for v in (set -l -n); set -e $v; end
argparse 'm#max' -- argle -987 bargle --max 765
set -l
##########
# Verify that flag value validation works.
echo '# Implicit int flag validation fails' >&2
argparse 'm#max' -- argle --max 765x bargle
and echo unxpected argparse return status >&2
argparse 'm#max' -- argle -ma1 bargle
and echo unxpected argparse return status >&2
echo '# Check the exit status from argparse validation'
argparse 'm#max!set | grep _flag_; function x; return 57; end; x' -- argle --max=83 bargle 2>&1
set -l saved_status $status
test $saved_status -eq 57
and echo expected argparse return status $saved_status
echo '# Explicit int flag validation' >&2
# These should fail
argparse 'm#max!_validate_int --min 1 --max 1' -- argle -m2 bargle
and echo unexpected argparse return status $status >&2
argparse 'm#max!_validate_int --min 0 --max 1' -- argle --max=-1 bargle
and echo unexpected argparse return status $status >&2
# These should succeed
argparse 'm/max=!_validate_int --min 0 --max 1' -- argle --max=0 bargle
or echo unexpected argparse return status $status >&2
argparse 'm/max=!_validate_int --min 0 --max 1' -- argle --max=1 bargle
or echo unexpected argparse return status $status >&2

View file

@ -51,3 +51,7 @@ argv 'argle' 'bargle'
_flag_m 765
_flag_max 765
argv 'argle' 'bargle'
# Check the exit status from argparse validation
_flag_name max
_flag_value 83
expected argparse return status 57