argparse: Make short flag names optional (#7585)

It was always a bit ridiculous that argparse required `X-longflag` if
that "X" short flag was never actually used anywhere.

Since the short letter is for getopt's benefit, we can hack around
this with our old friend: Unicode Private Use Areas.

We have a counter, starting at 0xE000 and going to 0xF8FF, that counts
up for all options that don't have a short flag and provides one. This
gives us up to 6400 long-only options.

6.4K should be enough for everybody.
This commit is contained in:
Fabian Homborg 2021-01-01 11:37:25 +01:00 committed by GitHub
parent c8b400bfad
commit 7ea8e20623
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 114 additions and 22 deletions

View File

@ -18,7 +18,7 @@ This command makes it easy for fish scripts and functions to handle arguments li
Each option specification (``OPTION_SPEC``) is written in the `domain specific language <#option-specifications>`__ described below. All OPTION_SPECs must appear after any argparse flags and before the ``--`` that separates them from the arguments to be parsed.
Each option that is seen in the ARG list will result in a var name of the form ``_flag_X``, where ``X`` is the short flag letter and the long flag name. The OPTION_SPEC always requires a short flag even if it can't be used. So there will always be ``_flag_X`` var set using the short flag letter if the corresponding short or long flag is seen. The long flag name var (e.g., ``_flag_help``) will only be defined, obviously, if the OPTION_SPEC includes a long flag name.
Each option that is seen in the ARG list will result in a var name of the form ``_flag_X``, where ``X`` is the short flag letter and the long flag name. The long flag name var (e.g., ``_flag_help``) will only be defined, obviously, if the OPTION_SPEC includes a long flag name.
For example ``_flag_h`` and ``_flag_help`` if ``-h`` or ``--help`` is seen. The var will be set with local scope (i.e., as if the script had done ``set -l _flag_X``). If the flag is a boolean (that is, it just is passed or not, it doesn't have a value) the values are the short and long flags seen. If the option is not a boolean the values will be zero or more values corresponding to the values collected when the ARG list is processed. If the flag was not seen the flag var will not be set.
@ -74,13 +74,11 @@ Option Specifications
Each option specification consists of:
- A short flag letter (which is mandatory). It must be an alphanumeric or "#". The "#" character is special and means that a flag of the form ``-123`` is valid. The short flag "#" must be followed by "-" (since the short name isn't otherwise valid since ``_flag_#`` is not a valid var name) and must be followed by a long flag name with no modifiers.
- An optional alphanumeric short flag letter, followed by a ``/`` if the short flag can be used by someone invoking your command or a ``-`` if it should not be exposed as a valid short flag and the letter is just for the ``_flag_X`` variable.
- A ``/`` if the short flag can be used by someone invoking your command else ``-`` if it should not be exposed as a valid short flag. If there is no long flag name these characters should be omitted. You can also specify a '#' to indicate the short and long flag names can be used and the value can be specified as an implicit int; i.e., a flag of the form ``-NNN``.
- An optional long flag name. If not present then only the short flag letter can be used, and if that is not present either it's an error.
- A long flag name which is optional. If not present then only the short flag letter can be used.
- Nothing if the flag is a boolean that takes no argument or is an implicit int flag, or
- Nothing if the flag is a boolean that takes no argument or is an integer flag, or
- ``=`` if it requires a value and only the last instance of the flag is saved, or
@ -94,6 +92,17 @@ See the :ref:`fish_opt <cmd-fish_opt>` command for a friendlier but more verbose
If a flag is not seen when parsing the arguments then the corresponding _flag_X var(s) will not be set.
Integer flag
------------
Sometimes commands take numbers directly as options, like ``foo -55``. To allow this one option spec can have the ``#`` modifier so that any integer will be understood as this flag, and the last number will be given as its value (as if ``=`` was used).
The ``#`` must follow the short flag letter (if any), and other modifiers like ``=`` are not allowed, except for ``-``::
m#maximum
This does not read numbers given as ``+NNN``, only those that look like flags - ``-NNN``.
Note: Optional arguments
------------------------
@ -149,6 +158,10 @@ Some OPTION_SPEC examples:
- ``h-help`` means that only ``--help`` is valid. The flag is a boolean and can be used more than once. If the long flag is used then ``_flag_h`` and ``_flag_help`` will be set to the count of how many times the long flag was seen.
- ``help`` means that only ``--help`` is valid and only ``_flag_help`` will be set.
- ``longonly=`` is a flag ``--longonly`` that requires an option, there is no short flag or even short flag variable.
- ``n/name=`` means that both ``-n`` and ``--name`` are valid. It requires a value and can be used at most once. If the flag is seen then ``_flag_n`` and ``_flag_name`` will be set with the single mandatory value associated with the flag.
- ``n/name=?`` means that both ``-n`` and ``--name`` are valid. It accepts an optional value and can be used at most once. If the flag is seen then ``_flag_n`` and ``_flag_name`` will be set with the value associated with the flag if one was provided else it will be set with no values.
@ -165,6 +178,8 @@ Some OPTION_SPEC examples:
- ``n#max`` means that flags matching the regex "^--?\\d+$" are valid. When seen they are assigned to the variables ``_flag_n`` and ``_flag_max``. This allows any valid positive or negative integer to be specified by prefixing it with a single "-". Many commands support this idiom. For example ``head -3 /a/file`` to emit only the first three lines of /a/file. You can also specify the value using either flag: ``-n NNN`` or ``--max NNN`` in this example.
After parsing the arguments the ``argv`` var is set with local scope to any values not already consumed during flag processing. If there are not unbound values the var is set but ``count $argv`` will be zero.
- ``#longonly`` causes the last integer option to be stored in ``_flag_longonly``.
After parsing the arguments the ``argv`` var is set with local scope to any values not already consumed during flag processing. If there are no unbound values the var is set but ``count $argv`` will be zero.
If an error occurs during argparse processing it will exit with a non-zero status and print error messages to stderr.

View File

@ -32,7 +32,7 @@ static const wcstring var_name_prefix = L"_flag_";
#define BUILTIN_ERR_INVALID_OPT_SPEC _(L"%ls: Invalid option spec '%ls' at char '%lc'\n")
struct option_spec_t {
const wchar_t short_flag;
wchar_t short_flag;
wcstring long_flag;
wcstring validation_command;
wcstring_list_t vals;
@ -208,14 +208,14 @@ static bool parse_flag_modifiers(const argparse_cmd_opts_t &opts, const option_s
/// Parse the text following the short flag letter.
static bool parse_option_spec_sep(argparse_cmd_opts_t &opts, const option_spec_ref_t &opt_spec,
const wcstring &option_spec, const wchar_t **opt_spec_str,
io_streams_t &streams) {
wchar_t &counter, io_streams_t &streams) {
const wchar_t *s = *opt_spec_str;
if (*(s - 1) == L'#') {
if (*s != L'-') {
streams.err.append_format(
_(L"%ls: Short flag '#' must be followed by '-' and a long name\n"),
opts.name.c_str());
return false;
// Long-only!
s--;
opt_spec->short_flag = counter;
counter++;
}
if (opts.implicit_int_flag) {
streams.err.append_format(_(L"%ls: Implicit int flag '%lc' already defined\n"),
@ -250,9 +250,17 @@ static bool parse_option_spec_sep(argparse_cmd_opts_t &opts, const option_spec_r
opt_spec->num_allowed = 1; // mandatory arg and can appear only once
s++; // the struct is initialized assuming short_flag_valid should be true
} else {
// Long flag name not allowed if second char isn't '/', '-' or '#' so just check for
// behavior modifier chars.
if (!parse_flag_modifiers(opts, opt_spec, option_spec, &s, streams)) return false;
if (*s != L'!' && *s != L'?' && *s != L'=') {
// No short flag separator and no other modifiers, so this is a long only option.
// Since getopt needs a wchar, we have a counter that we count up.
opt_spec->short_flag_valid = false;
s--;
opt_spec->short_flag = counter;
counter++;
} else {
// Try to parse any other flag modifiers
if (!parse_flag_modifiers(opts, opt_spec, option_spec, &s, streams)) return false;
}
}
*opt_spec_str = s;
@ -261,10 +269,12 @@ static bool parse_option_spec_sep(argparse_cmd_opts_t &opts, const option_spec_r
/// This parses an option spec string into a struct option_spec.
static bool parse_option_spec(argparse_cmd_opts_t &opts, //!OCLINT(high npath complexity)
const wcstring &option_spec, io_streams_t &streams) {
const wcstring &option_spec, wchar_t &counter,
io_streams_t &streams) {
if (option_spec.empty()) {
streams.err.append_format(_(L"%ls: An option spec must have a short flag letter\n"),
opts.name.c_str());
streams.err.append_format(
_(L"%ls: An option spec must have at least a short or a long flag\n"),
opts.name.c_str());
return false;
}
@ -278,7 +288,7 @@ static bool parse_option_spec(argparse_cmd_opts_t &opts, //!OCLINT(high npath c
std::unique_ptr<option_spec_t> opt_spec(new option_spec_t{*s++});
// Try parsing stuff after the short flag.
if (*s && !parse_option_spec_sep(opts, opt_spec, option_spec, &s, streams)) {
if (*s && !parse_option_spec_sep(opts, opt_spec, option_spec, &s, counter, streams)) {
return false;
}
@ -315,22 +325,32 @@ static int collect_option_specs(argparse_cmd_opts_t &opts, int *optind, int argc
io_streams_t &streams) {
wchar_t *cmd = argv[0];
// A counter to give short chars to long-only options because getopt needs that.
// Luckily we have wgetopt so we can use wchars - this is one of the private use areas so we
// have 6400 options available.
wchar_t counter = static_cast<wchar_t>(0xE000);
while (true) {
if (std::wcscmp(L"--", argv[*optind]) == 0) {
++*optind;
break;
}
if (!parse_option_spec(opts, argv[*optind], streams)) {
if (!parse_option_spec(opts, argv[*optind], counter, streams)) {
return STATUS_CMD_ERROR;
}
if (++*optind == argc) {
streams.err.append_format(_(L"%ls: Missing -- separator\n"), cmd);
return STATUS_INVALID_ARGS;
}
}
// Check for counter overreach once at the end because this is very unlikely to ever be reached.
if (counter > static_cast<wchar_t>(0xF8FF)) {
streams.err.append_format(_(L"%ls: Too many long-only options\n"), cmd);
return STATUS_INVALID_ARGS;
}
if (opts.options.empty()) {
streams.err.append_format(_(L"%ls: No option specs were provided\n"), cmd);
return STATUS_INVALID_ARGS;

View File

@ -350,6 +350,54 @@ begin
# CHECK: saved_status 57
end
# long-only flags
begin
argparse installed= foo -- --installed=no --foo
set -l
# CHECK: _flag_a 'alpha' 'aaaa'
# CHECK: _flag_b -b
# CHECK: _flag_break -b
# CHECK: _flag_foo --foo
# CHECK: _flag_installed no
# CHECK: _flag_m 1
# CHECK: _flag_max 1
# CHECK: argv
# CHECK: saved_status 57
end
begin
argparse installed='!_validate_int --max 12' foo -- --installed=5 --foo
set -l
# CHECK: _flag_a 'alpha' 'aaaa'
# CHECK: _flag_b -b
# CHECK: _flag_break -b
# CHECK: _flag_foo --foo
# CHECK: _flag_installed 5
# CHECK: _flag_m 1
# CHECK: _flag_max 1
# CHECK: argv
# CHECK: saved_status 57
end
begin
argparse '#num' installed= -- --installed=5 -5
set -l
# CHECK: _flag_a 'alpha' 'aaaa'
# CHECK: _flag_b -b
# CHECK: _flag_break -b
# CHECK: _flag_installed 5
# CHECK: _flag_m 1
# CHECK: _flag_max 1
# CHECK: _flag_num 5
# CHECK: argv
# CHECK: saved_status 57
end
begin
argparse installed='!_validate_int --max 12' foo -- --foo --installed=error --foo
# CHECKERR: argparse: Value 'error' for flag 'installed' is not an integer
end
# #6483 - error messages for missing arguments
argparse -n foo q r/required= -- foo -qr
# CHECKERR: foo: Expected argument for option r
@ -440,3 +488,12 @@ function wrongargparse
argparse a-b
argparse
end
begin
argparse ''
#CHECKERR: argparse: An option spec must have at least a short or a long flag
#CHECKERR: checks/argparse.fish (line {{\d+}}):
#CHECKERR: argparse ''
#CHECKERR: ^
#CHECKERR: (Type 'help argparse' for related documentation)
end