mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-26 12:53:13 +00:00
add argparse
unit tests and fix some bugs
This implements some unit tests for the new `argparse` command and fixes a couple of bugs those tests brought to light. Fixes #4190
This commit is contained in:
parent
3a782003ed
commit
c149f4f301
5 changed files with 159 additions and 13 deletions
|
@ -173,12 +173,12 @@ 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,
|
static bool parse_flag_modifiers(argparse_cmd_opts_t &opts, option_spec_t *opt_spec,
|
||||||
const wcstring &option_spec, const wchar_t *s,
|
const wcstring &option_spec, const wchar_t *s,
|
||||||
io_streams_t &streams) {
|
io_streams_t &streams) {
|
||||||
if (*s == '=') {
|
if (*s == L'=') {
|
||||||
s++;
|
s++;
|
||||||
if (*s == '?') {
|
if (*s == L'?') {
|
||||||
opt_spec->num_allowed = -1; // optional arg
|
opt_spec->num_allowed = -1; // optional arg
|
||||||
s++;
|
s++;
|
||||||
} else if (*s == '+') {
|
} else if (*s == L'+') {
|
||||||
opt_spec->num_allowed = 2; // mandatory arg and can appear more than once
|
opt_spec->num_allowed = 2; // mandatory arg and can appear more than once
|
||||||
s++;
|
s++;
|
||||||
} else {
|
} else {
|
||||||
|
@ -208,9 +208,9 @@ static bool parse_option_spec(argparse_cmd_opts_t &opts, wcstring option_spec,
|
||||||
const wchar_t *s = option_spec.c_str();
|
const wchar_t *s = option_spec.c_str();
|
||||||
option_spec_t *opt_spec = new option_spec_t(*s++);
|
option_spec_t *opt_spec = new option_spec_t(*s++);
|
||||||
|
|
||||||
if (*s == '/') {
|
if (*s == L'/') {
|
||||||
s++; // the struct is initialized assuming short_flag_valid should be true
|
s++; // the struct is initialized assuming short_flag_valid should be true
|
||||||
} else if (*s == '-') {
|
} else if (*s == L'-') {
|
||||||
opt_spec->short_flag_valid = false;
|
opt_spec->short_flag_valid = false;
|
||||||
s++;
|
s++;
|
||||||
} else {
|
} else {
|
||||||
|
@ -219,16 +219,17 @@ static bool parse_option_spec(argparse_cmd_opts_t &opts, wcstring option_spec,
|
||||||
return parse_flag_modifiers(opts, opt_spec, option_spec, s, streams);
|
return parse_flag_modifiers(opts, opt_spec, option_spec, s, streams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collect the long flag name.
|
||||||
const wchar_t *e = s;
|
const wchar_t *e = s;
|
||||||
while (*e && *e != '+' && *e != '=') e++;
|
while (*e && (*e == L'-' || *e == L'_' || iswalnum(*e))) e++;
|
||||||
if (e == s) {
|
if (e == s) {
|
||||||
streams.err.append_format(BUILTIN_ERR_INVALID_OPT_SPEC, opts.name.c_str(),
|
streams.err.append_format(BUILTIN_ERR_INVALID_OPT_SPEC, opts.name.c_str(),
|
||||||
option_spec.c_str(), *(s - 1));
|
option_spec.c_str(), *(s - 1));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
opt_spec->long_flag = wcstring(s, e - s);
|
opt_spec->long_flag = wcstring(s, e - s);
|
||||||
opts.long_to_short_flag.emplace(opt_spec->long_flag, opt_spec->short_flag);
|
opts.long_to_short_flag.emplace(opt_spec->long_flag, opt_spec->short_flag);
|
||||||
|
|
||||||
return parse_flag_modifiers(opts, opt_spec, option_spec, e, streams);
|
return parse_flag_modifiers(opts, opt_spec, option_spec, e, streams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,7 +237,7 @@ static int collect_option_specs(argparse_cmd_opts_t &opts, int *optind, int argc
|
||||||
io_streams_t &streams) {
|
io_streams_t &streams) {
|
||||||
wchar_t *cmd = argv[0];
|
wchar_t *cmd = argv[0];
|
||||||
|
|
||||||
while (*optind < argc) {
|
while (true) {
|
||||||
if (wcscmp(L"--", argv[*optind]) == 0) {
|
if (wcscmp(L"--", argv[*optind]) == 0) {
|
||||||
++*optind;
|
++*optind;
|
||||||
break;
|
break;
|
||||||
|
@ -246,7 +247,10 @@ static int collect_option_specs(argparse_cmd_opts_t &opts, int *optind, int argc
|
||||||
return STATUS_CMD_ERROR;
|
return STATUS_CMD_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
++*optind;
|
if (++*optind == argc) {
|
||||||
|
streams.err.append_format(_(L"%ls: Missing -- separator\n"), cmd);
|
||||||
|
return STATUS_INVALID_ARGS;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.options.empty()) {
|
if (opts.options.empty()) {
|
||||||
|
@ -317,6 +321,12 @@ static int parse_cmd_opts(argparse_cmd_opts_t &opts, int *optind, //!OCLINT(hig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (argc == w.woptind || wcscmp(L"--", argv[w.woptind - 1]) == 0) {
|
||||||
|
// The user didn't specify any option specs.
|
||||||
|
streams.err.append_format(_(L"%ls: No option specs were provided\n"), cmd);
|
||||||
|
return STATUS_INVALID_ARGS;
|
||||||
|
}
|
||||||
|
|
||||||
*optind = w.woptind;
|
*optind = w.woptind;
|
||||||
return collect_option_specs(opts, optind, argc, argv, streams);
|
return collect_option_specs(opts, optind, argc, argv, streams);
|
||||||
}
|
}
|
||||||
|
@ -417,9 +427,8 @@ static int argparse_parse_args(argparse_cmd_opts_t &opts, const wcstring_list_t
|
||||||
const wchar_t *cmd = opts.name.c_str();
|
const wchar_t *cmd = opts.name.c_str();
|
||||||
int argc = static_cast<int>(args.size());
|
int argc = static_cast<int>(args.size());
|
||||||
|
|
||||||
// This is awful but we need to convert our wcstring_list_t to a <wchar_t **> that can be passed
|
// We need to convert our wcstring_list_t to a <wchar_t **> that can be used by wgetopt_long().
|
||||||
// to w.wgetopt_long(). Furthermore, because we're dynamically allocating the array of pointers
|
// This ensures the memory for the data structure is freed when we leave this scope.
|
||||||
// we need to ensure the memory for the data structure is freed when we leave this scope.
|
|
||||||
null_terminated_array_t<wchar_t> argv_container(args);
|
null_terminated_array_t<wchar_t> argv_container(args);
|
||||||
auto argv = (wchar_t **)argv_container.get();
|
auto argv = (wchar_t **)argv_container.get();
|
||||||
|
|
||||||
|
@ -433,7 +442,17 @@ static int argparse_parse_args(argparse_cmd_opts_t &opts, const wcstring_list_t
|
||||||
update_bool_flag_counts(opts);
|
update_bool_flag_counts(opts);
|
||||||
|
|
||||||
for (int i = optind; argv[i]; i++) opts.argv.push_back(argv[i]);
|
for (int i = optind; argv[i]; i++) opts.argv.push_back(argv[i]);
|
||||||
if (opts.min_args > 0 && opts.argv.size() < opts.min_args) {
|
|
||||||
|
return STATUS_CMD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int check_min_max_args_constraints(argparse_cmd_opts_t &opts, const wcstring_list_t &args,
|
||||||
|
parser_t &parser, io_streams_t &streams) {
|
||||||
|
UNUSED(parser);
|
||||||
|
const wchar_t *cmd = opts.name.c_str();
|
||||||
|
int argc = static_cast<int>(args.size());
|
||||||
|
|
||||||
|
if (opts.argv.size() < opts.min_args) {
|
||||||
streams.err.append_format(BUILTIN_ERR_MIN_ARG_COUNT1, cmd, opts.min_args, opts.argv.size());
|
streams.err.append_format(BUILTIN_ERR_MIN_ARG_COUNT1, cmd, opts.min_args, opts.argv.size());
|
||||||
return STATUS_CMD_ERROR;
|
return STATUS_CMD_ERROR;
|
||||||
}
|
}
|
||||||
|
@ -466,11 +485,13 @@ int builtin_argparse(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
|
||||||
return STATUS_CMD_OK;
|
return STATUS_CMD_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if 0
|
||||||
if (optind == argc) {
|
if (optind == argc) {
|
||||||
// Apparently we weren't handed any arguments to be parsed according to the option specs we
|
// Apparently we weren't handed any arguments to be parsed according to the option specs we
|
||||||
// just collected. So there isn't anything for us to do.
|
// just collected. So there isn't anything for us to do.
|
||||||
return STATUS_CMD_OK;
|
return STATUS_CMD_OK;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
wcstring_list_t args;
|
wcstring_list_t args;
|
||||||
args.push_back(opts.name);
|
args.push_back(opts.name);
|
||||||
|
@ -482,6 +503,9 @@ int builtin_argparse(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
|
||||||
retval = argparse_parse_args(opts, args, parser, streams);
|
retval = argparse_parse_args(opts, args, parser, streams);
|
||||||
if (retval != STATUS_CMD_OK) return retval;
|
if (retval != STATUS_CMD_OK) return retval;
|
||||||
|
|
||||||
|
retval = check_min_max_args_constraints(opts, args, parser, streams);
|
||||||
|
if (retval != STATUS_CMD_OK) return retval;
|
||||||
|
|
||||||
for (auto it : opts.options) {
|
for (auto it : opts.options) {
|
||||||
option_spec_t *opt_spec = it.second;
|
option_spec_t *opt_spec = it.second;
|
||||||
if (!opt_spec->num_seen) continue;
|
if (!opt_spec->num_seen) continue;
|
||||||
|
|
16
tests/argparse.err
Normal file
16
tests/argparse.err
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# No args is an error
|
||||||
|
argparse: No option specs were provided
|
||||||
|
# Missing -- is an error
|
||||||
|
argparse: Missing -- separator
|
||||||
|
# Flags but no option specs is an error
|
||||||
|
argparse: No option specs were provided
|
||||||
|
# Invalid option specs
|
||||||
|
argparse: Invalid option spec 'h-' at char '-'
|
||||||
|
argparse: Invalid option spec '+help' at char 'h'
|
||||||
|
argparse: Invalid option spec 'h/help:' at char ':'
|
||||||
|
argparse: Invalid option spec 'h-help::' at char ':'
|
||||||
|
argparse: Invalid option spec 'h-help=x' at char 'x'
|
||||||
|
# --max-args and --min-args work
|
||||||
|
min-max: Expected at least 1 args, got only 0
|
||||||
|
min-max: Expected at most 3 args, got 4
|
||||||
|
min-max: Expected at most 1 args, got 2
|
66
tests/argparse.in
Normal file
66
tests/argparse.in
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
# Test the `argparse` builtin.
|
||||||
|
|
||||||
|
##########
|
||||||
|
# Start by verifying a bunch of error conditions.
|
||||||
|
|
||||||
|
echo '# No args is an error' >&2
|
||||||
|
argparse
|
||||||
|
|
||||||
|
echo '# Missing -- is an error' >&2
|
||||||
|
argparse h/help
|
||||||
|
|
||||||
|
echo '# Flags but no option specs is an error' >&2
|
||||||
|
argparse -s -- hello
|
||||||
|
|
||||||
|
echo '# Invalid option specs' >&2
|
||||||
|
argparse h-
|
||||||
|
argparse +help
|
||||||
|
argparse h/help:
|
||||||
|
argparse h-help::
|
||||||
|
argparse h-help=x
|
||||||
|
|
||||||
|
echo '# --max-args and --min-args work' >&2
|
||||||
|
begin
|
||||||
|
argparse --name min-max --min-args 1 h/help --
|
||||||
|
argparse --name min-max --min-args 1 --max-args 3 h/help -- arg1
|
||||||
|
argparse --name min-max --min-args 1 --max-args 3 h/help -- arg1 arg2
|
||||||
|
argparse --name min-max --min-args 1 --max-args 3 h/help -- --help arg1 arg2 arg3
|
||||||
|
argparse --name min-max --min-args 1 --max-args 3 h/help -- arg1 arg2 -h arg3 arg4
|
||||||
|
argparse --name min-max --max-args 1 h/help --
|
||||||
|
argparse --name min-max --max-args 1 h/help -- arg1
|
||||||
|
argparse --name min-max --max-args 1 h/help -- arg1 arg2
|
||||||
|
end
|
||||||
|
|
||||||
|
##########
|
||||||
|
# Now verify that validly formed invocations work as expected.
|
||||||
|
|
||||||
|
echo '# No args'
|
||||||
|
argparse h/help --
|
||||||
|
|
||||||
|
echo '# One arg and no matching flags'
|
||||||
|
begin
|
||||||
|
argparse h/help -- help
|
||||||
|
set -l
|
||||||
|
show $argv
|
||||||
|
end
|
||||||
|
|
||||||
|
echo '# Five args with two matching a flag'
|
||||||
|
begin
|
||||||
|
argparse h/help -- help --help me -h 'a lot more'
|
||||||
|
set -l
|
||||||
|
show $argv
|
||||||
|
end
|
||||||
|
|
||||||
|
echo '# Required, optional, and multiple flags'
|
||||||
|
begin
|
||||||
|
argparse 'h/help' 'a/abc=' 'd/def=?' 'g/ghk=+' -- help --help me --ghk=g1 --abc=ABC --ghk g2 --d -g g3
|
||||||
|
set -l
|
||||||
|
show $argv
|
||||||
|
end
|
||||||
|
|
||||||
|
echo '# --stop-nonopt works'
|
||||||
|
begin
|
||||||
|
argparse --stop-nonopt 'h/help' 'a/abc=' -- -a A1 -h --abc A2 non-opt 'second non-opt' --help
|
||||||
|
set -l
|
||||||
|
show $argv
|
||||||
|
end
|
36
tests/argparse.out
Normal file
36
tests/argparse.out
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# No args
|
||||||
|
# One arg and no matching flags
|
||||||
|
argv help
|
||||||
|
count=1
|
||||||
|
|help|
|
||||||
|
# Five args with two matching a flag
|
||||||
|
_flag_h 2
|
||||||
|
_flag_help 2
|
||||||
|
argv 'help' 'me' 'a lot more'
|
||||||
|
count=3
|
||||||
|
|help|
|
||||||
|
|me|
|
||||||
|
|a lot more|
|
||||||
|
# Required, optional, and multiple flags
|
||||||
|
_flag_a ABC
|
||||||
|
_flag_abc ABC
|
||||||
|
_flag_d
|
||||||
|
_flag_def
|
||||||
|
_flag_g 'g1' 'g2' 'g3'
|
||||||
|
_flag_ghk 'g1' 'g2' 'g3'
|
||||||
|
_flag_h 1
|
||||||
|
_flag_help 1
|
||||||
|
argv 'help' 'me'
|
||||||
|
count=2
|
||||||
|
|help|
|
||||||
|
|me|
|
||||||
|
# --stop-nonopt works
|
||||||
|
_flag_a A2
|
||||||
|
_flag_abc A2
|
||||||
|
_flag_h 1
|
||||||
|
_flag_help 1
|
||||||
|
argv 'non-opt' 'second non-opt' '--help'
|
||||||
|
count=3
|
||||||
|
|non-opt|
|
||||||
|
|second non-opt|
|
||||||
|
|--help|
|
4
tests/test_functions/show.fish
Normal file
4
tests/test_functions/show.fish
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
function show
|
||||||
|
echo count=(count $argv)
|
||||||
|
printf '|%s|\n' $argv
|
||||||
|
end
|
Loading…
Reference in a new issue