From 277999adef945ab7d93558bd6924086b0d8fc850 Mon Sep 17 00:00:00 2001 From: Kurtis Rader Date: Fri, 7 Jul 2017 14:32:41 -0700 Subject: [PATCH] implement `argparse` builtin We've needed a fishy way to parse flags and arguments given to scripts and functions for a very long time. In particular a manner that provides the same behavior implemented by builtin commands. The long term goal is to support DocOpt. But since it is unclear when that will happen so this implements a `argparse` command. So named as homage to the excellent Python module of the same name. Fixes #4190 --- CHANGELOG.md | 1 + Makefile.in | 89 +++++---- fish.xcodeproj/project.pbxproj | 4 + src/builtin.cpp | 2 + src/builtin_argparse.cpp | 354 +++++++++++++++++++++++++++++++++ src/builtin_argparse.h | 9 + 6 files changed, 417 insertions(+), 42 deletions(-) create mode 100644 src/builtin_argparse.cpp create mode 100644 src/builtin_argparse.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 534e45193..b8e14bd1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # fish 2.7b1 ## Notable fixes and improvements +- A new `argparse` command is available to allow fish script to parse arguments with the same behavior as builtin commands. This also includes the `fish_opt` helper command. (#4190). - The `COLUMNS` and `LINES` env vars are now correctly set the first time `fish_prompt` is run (#4141). - New `status is-breakpoint` command that is true when a prompt is displayed in response to a `breakpoint` command (#1310). - Invalid array indexes are now silently ignored (#826, #4127). diff --git a/Makefile.in b/Makefile.in index 2a64ad00c..08aaa35b3 100644 --- a/Makefile.in +++ b/Makefile.in @@ -99,21 +99,24 @@ HAVE_DOXYGEN=@HAVE_DOXYGEN@ # All objects that the system needs to build fish, except fish.o # FISH_OBJS := obj/autoload.o obj/builtin.o obj/builtin_bg.o obj/builtin_bind.o obj/builtin_block.o \ - obj/builtin_builtin.o obj/builtin_cd.o obj/builtin_command.o obj/builtin_commandline.o \ - obj/builtin_complete.o obj/builtin_contains.o obj/builtin_disown.o obj/builtin_echo.o \ - obj/builtin_emit.o obj/builtin_exit.o obj/builtin_fg.o obj/builtin_function.o \ - obj/builtin_functions.o obj/builtin_history.o obj/builtin_jobs.o obj/builtin_printf.o \ - obj/builtin_pwd.o obj/builtin_random.o obj/builtin_read.o obj/builtin_realpath.o \ - obj/builtin_return.o obj/builtin_set.o obj/builtin_set_color.o obj/builtin_source.o \ - obj/builtin_status.o obj/builtin_string.o obj/builtin_test.o obj/builtin_ulimit.o \ - obj/color.o obj/common.o obj/complete.o obj/env.o obj/env_universal_common.o obj/event.o \ - obj/exec.o obj/expand.o obj/fallback.o obj/fish_version.o obj/function.o obj/highlight.o \ - obj/history.o obj/input.o obj/input_common.o obj/intern.o obj/io.o obj/iothread.o \ - obj/kill.o obj/output.o obj/pager.o obj/parse_execution.o obj/parse_productions.o \ - obj/parse_tree.o obj/parse_util.o obj/parser.o obj/parser_keywords.o obj/path.o \ - obj/postfork.o obj/proc.o obj/reader.o obj/sanity.o obj/screen.o obj/signal.o \ - obj/tokenizer.o obj/utf8.o obj/util.o obj/wcstringutil.o obj/wgetopt.o obj/wildcard.o \ - obj/wutil.o + obj/builtin_builtin.o obj/builtin_cd.o obj/builtin_command.o \ + obj/builtin_commandline.o obj/builtin_complete.o obj/builtin_contains.o \ + obj/builtin_disown.o obj/builtin_echo.o obj/builtin_emit.o \ + obj/builtin_exit.o obj/builtin_fg.o obj/builtin_function.o \ + obj/builtin_functions.o obj/builtin_argparse.o obj/builtin_history.o \ + obj/builtin_jobs.o obj/builtin_printf.o obj/builtin_pwd.o \ + obj/builtin_random.o obj/builtin_read.o obj/builtin_realpath.o \ + obj/builtin_return.o obj/builtin_set.o obj/builtin_set_color.o \ + obj/builtin_source.o obj/builtin_status.o obj/builtin_string.o \ + obj/builtin_test.o obj/builtin_ulimit.o obj/color.o obj/common.o \ + obj/complete.o obj/env.o obj/env_universal_common.o obj/event.o obj/exec.o \ + obj/expand.o obj/fallback.o obj/fish_version.o obj/function.o obj/highlight.o \ + obj/history.o obj/input.o obj/input_common.o obj/intern.o obj/io.o \ + obj/iothread.o obj/kill.o obj/output.o obj/pager.o obj/parse_execution.o \ + obj/parse_productions.o obj/parse_tree.o obj/parse_util.o obj/parser.o \ + obj/parser_keywords.o obj/path.o obj/postfork.o obj/proc.o obj/reader.o \ + obj/sanity.o obj/screen.o obj/signal.o obj/tokenizer.o obj/utf8.o obj/util.o \ + obj/wcstringutil.o obj/wgetopt.o obj/wildcard.o obj/wutil.o FISH_INDENT_OBJS := obj/fish_indent.o obj/print_help.o $(FISH_OBJS) @@ -956,10 +959,10 @@ obj/builtin.o: src/builtin_command.h src/builtin_commandline.h obj/builtin.o: src/builtin_complete.h src/builtin_contains.h obj/builtin.o: src/builtin_disown.h src/builtin_echo.h src/builtin_emit.h obj/builtin.o: src/builtin_exit.h src/builtin_fg.h src/builtin_functions.h -obj/builtin.o: src/builtin_history.h src/builtin_jobs.h src/builtin_printf.h -obj/builtin.o: src/builtin_pwd.h src/builtin_random.h src/builtin_read.h -obj/builtin.o: src/builtin_realpath.h src/builtin_return.h src/builtin_set.h -obj/builtin.o: src/builtin_set_color.h src/builtin_source.h +obj/builtin.o: src/builtin_argparse.h src/builtin_history.h src/builtin_jobs.h +obj/builtin.o: src/builtin_printf.h src/builtin_pwd.h src/builtin_random.h +obj/builtin.o: src/builtin_read.h src/builtin_realpath.h src/builtin_return.h +obj/builtin.o: src/builtin_set.h src/builtin_set_color.h src/builtin_source.h obj/builtin.o: src/builtin_status.h src/builtin_string.h src/builtin_test.h obj/builtin.o: src/builtin_ulimit.h src/complete.h src/exec.h src/intern.h obj/builtin.o: src/io.h src/parse_constants.h src/parse_util.h @@ -985,7 +988,7 @@ obj/builtin_cd.o: config.h src/builtin.h src/common.h src/fallback.h obj/builtin_cd.o: src/signal.h src/builtin_cd.h src/env.h src/io.h obj/builtin_cd.o: src/parser.h src/event.h src/expand.h src/parse_constants.h obj/builtin_cd.o: src/parse_tree.h src/tokenizer.h src/proc.h src/path.h -obj/builtin_cd.o: src/wgetopt.h src/wutil.h +obj/builtin_cd.o: src/wutil.h obj/builtin_command.o: config.h src/builtin.h src/common.h src/fallback.h obj/builtin_command.o: src/signal.h src/builtin_command.h src/io.h src/path.h obj/builtin_command.o: src/env.h src/wgetopt.h src/wutil.h @@ -1009,13 +1012,13 @@ obj/builtin_disown.o: config.h src/signal.h src/builtin.h src/common.h obj/builtin_disown.o: src/fallback.h src/builtin_disown.h src/io.h obj/builtin_disown.o: src/parser.h src/event.h src/expand.h obj/builtin_disown.o: src/parse_constants.h src/parse_tree.h src/tokenizer.h -obj/builtin_disown.o: src/proc.h src/wgetopt.h src/wutil.h +obj/builtin_disown.o: src/proc.h src/wutil.h obj/builtin_echo.o: config.h src/builtin.h src/common.h src/fallback.h obj/builtin_echo.o: src/signal.h src/builtin_echo.h src/io.h src/wgetopt.h obj/builtin_echo.o: src/wutil.h obj/builtin_emit.o: config.h src/builtin.h src/common.h src/fallback.h obj/builtin_emit.o: src/signal.h src/builtin_emit.h src/event.h src/io.h -obj/builtin_emit.o: src/wgetopt.h src/wutil.h +obj/builtin_emit.o: src/wutil.h obj/builtin_exit.o: config.h src/builtin.h src/common.h src/fallback.h obj/builtin_exit.o: src/signal.h src/builtin_exit.h src/io.h src/proc.h obj/builtin_exit.o: src/parse_tree.h src/parse_constants.h src/tokenizer.h @@ -1038,6 +1041,10 @@ obj/builtin_functions.o: src/event.h src/function.h src/io.h obj/builtin_functions.o: src/parser_keywords.h src/proc.h src/parse_tree.h obj/builtin_functions.o: src/parse_constants.h src/tokenizer.h src/wgetopt.h obj/builtin_functions.o: src/wutil.h +obj/builtin_argparse.o: config.h src/builtin.h src/common.h src/fallback.h +obj/builtin_argparse.o: src/signal.h src/builtin_argparse.h src/env.h +obj/builtin_argparse.o: src/expand.h src/parse_constants.h src/io.h +obj/builtin_argparse.o: src/wgetopt.h src/wutil.h obj/builtin_history.o: config.h src/builtin.h src/common.h src/fallback.h obj/builtin_history.o: src/signal.h src/builtin_history.h src/history.h obj/builtin_history.o: src/wutil.h src/io.h src/reader.h src/complete.h @@ -1050,17 +1057,15 @@ obj/builtin_jobs.o: src/wutil.h obj/builtin_printf.o: config.h src/builtin.h src/common.h src/fallback.h obj/builtin_printf.o: src/signal.h src/io.h src/wutil.h obj/builtin_pwd.o: config.h src/builtin.h src/common.h src/fallback.h -obj/builtin_pwd.o: src/signal.h src/builtin_pwd.h src/event.h src/io.h -obj/builtin_pwd.o: src/wgetopt.h src/wutil.h +obj/builtin_pwd.o: src/signal.h src/builtin_pwd.h src/io.h src/wutil.h obj/builtin_random.o: config.h src/builtin.h src/common.h src/fallback.h -obj/builtin_random.o: src/signal.h src/builtin_random.h src/io.h -obj/builtin_random.o: src/wgetopt.h src/wutil.h +obj/builtin_random.o: src/signal.h src/builtin_random.h src/io.h src/wutil.h obj/builtin_read.o: config.h src/builtin.h src/common.h src/fallback.h obj/builtin_read.o: src/signal.h src/builtin_read.h src/complete.h src/env.h obj/builtin_read.o: src/event.h src/expand.h src/parse_constants.h -obj/builtin_read.o: src/highlight.h src/color.h src/io.h src/proc.h -obj/builtin_read.o: src/parse_tree.h src/tokenizer.h src/reader.h -obj/builtin_read.o: src/wcstringutil.h src/wgetopt.h src/wutil.h +obj/builtin_read.o: src/highlight.h src/color.h src/history.h src/wutil.h +obj/builtin_read.o: src/io.h src/proc.h src/parse_tree.h src/tokenizer.h +obj/builtin_read.o: src/reader.h src/wcstringutil.h src/wgetopt.h obj/builtin_realpath.o: config.h src/builtin.h src/common.h src/fallback.h obj/builtin_realpath.o: src/signal.h src/builtin_realpath.h src/io.h obj/builtin_realpath.o: src/wutil.h @@ -1081,7 +1086,7 @@ obj/builtin_source.o: src/signal.h src/builtin_source.h src/env.h obj/builtin_source.o: src/intern.h src/io.h src/parser.h src/event.h obj/builtin_source.o: src/expand.h src/parse_constants.h src/parse_tree.h obj/builtin_source.o: src/tokenizer.h src/proc.h src/reader.h src/complete.h -obj/builtin_source.o: src/highlight.h src/color.h src/wgetopt.h src/wutil.h +obj/builtin_source.o: src/highlight.h src/color.h src/wutil.h obj/builtin_status.o: config.h src/builtin.h src/common.h src/fallback.h obj/builtin_status.o: src/signal.h src/builtin_status.h src/io.h src/parser.h obj/builtin_status.o: src/event.h src/expand.h src/parse_constants.h @@ -1143,11 +1148,11 @@ obj/fish_indent.o: src/signal.h src/env.h src/fish_version.h src/highlight.h obj/fish_indent.o: src/output.h src/parse_constants.h src/parse_tree.h obj/fish_indent.o: src/tokenizer.h src/print_help.h src/wutil.h obj/fish_key_reader.o: config.h src/signal.h src/common.h src/fallback.h -obj/fish_key_reader.o: src/env.h src/input.h src/builtin_bind.h -obj/fish_key_reader.o: src/input_common.h src/print_help.h src/proc.h -obj/fish_key_reader.o: src/io.h src/parse_tree.h src/parse_constants.h -obj/fish_key_reader.o: src/tokenizer.h src/reader.h src/complete.h -obj/fish_key_reader.o: src/highlight.h src/color.h src/wutil.h +obj/fish_key_reader.o: src/env.h src/fish_version.h src/input.h +obj/fish_key_reader.o: src/builtin_bind.h src/input_common.h src/print_help.h +obj/fish_key_reader.o: src/proc.h src/io.h src/parse_tree.h +obj/fish_key_reader.o: src/parse_constants.h src/tokenizer.h src/reader.h +obj/fish_key_reader.o: src/complete.h src/highlight.h src/color.h src/wutil.h obj/fish_tests.o: config.h src/signal.h src/builtin.h src/common.h obj/fish_tests.o: src/fallback.h src/color.h src/complete.h src/env.h obj/fish_tests.o: src/env_universal_common.h src/wutil.h src/event.h @@ -1195,13 +1200,13 @@ obj/pager.o: config.h src/common.h src/fallback.h src/signal.h src/complete.h obj/pager.o: src/highlight.h src/color.h src/env.h src/pager.h src/reader.h obj/pager.o: src/parse_constants.h src/screen.h src/util.h src/wutil.h obj/parse_execution.o: config.h src/builtin.h src/common.h src/fallback.h -obj/parse_execution.o: src/signal.h src/complete.h src/env.h src/event.h -obj/parse_execution.o: src/exec.h src/expand.h src/parse_constants.h -obj/parse_execution.o: src/function.h src/io.h src/parse_execution.h -obj/parse_execution.o: src/parse_tree.h src/tokenizer.h src/proc.h -obj/parse_execution.o: src/parse_util.h src/parser.h src/path.h src/reader.h -obj/parse_execution.o: src/highlight.h src/color.h src/util.h src/wildcard.h -obj/parse_execution.o: src/wutil.h +obj/parse_execution.o: src/signal.h src/builtin_function.h src/complete.h +obj/parse_execution.o: src/env.h src/event.h src/exec.h src/expand.h +obj/parse_execution.o: src/parse_constants.h src/function.h src/io.h +obj/parse_execution.o: src/parse_execution.h src/parse_tree.h src/tokenizer.h +obj/parse_execution.o: src/proc.h src/parse_util.h src/parser.h src/path.h +obj/parse_execution.o: src/reader.h src/highlight.h src/color.h src/util.h +obj/parse_execution.o: src/wildcard.h src/wutil.h obj/parse_productions.o: config.h src/common.h src/fallback.h src/signal.h obj/parse_productions.o: src/parse_constants.h src/parse_productions.h obj/parse_productions.o: src/parse_tree.h src/tokenizer.h diff --git a/fish.xcodeproj/project.pbxproj b/fish.xcodeproj/project.pbxproj index 2c24938b5..43fef7032 100644 --- a/fish.xcodeproj/project.pbxproj +++ b/fish.xcodeproj/project.pbxproj @@ -647,6 +647,8 @@ 63A2C0E81CC5F9FB00973404 /* pcre2_find_bracket.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pcre2_find_bracket.c; sourceTree = ""; }; 9C7A55721DCD71330049C25D /* fish_key_reader */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = fish_key_reader; sourceTree = BUILT_PRODUCTS_DIR; }; 9C7A557C1DCD717C0049C25D /* fish_key_reader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = fish_key_reader.cpp; sourceTree = ""; }; + CBB772591F11F93F00780A21 /* builtin_argparse.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = builtin_argparse.cpp; sourceTree = ""; }; + CBB7725A1F11F93F00780A21 /* builtin_argparse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = builtin_argparse.h; sourceTree = ""; }; D00769421990137800CA4627 /* fish_tests */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = fish_tests; sourceTree = BUILT_PRODUCTS_DIR; }; D00F63F019137E9D00FCCDEC /* fish_version.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = fish_version.cpp; sourceTree = ""; }; D01A2D23169B730A00767098 /* man1 */ = {isa = PBXFileReference; lastKnownFileType = text; name = man1; path = pages_for_manpath/man1; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -984,6 +986,8 @@ D0D02A91159845EF008E62BD /* Sources */ = { isa = PBXGroup; children = ( + CBB772591F11F93F00780A21 /* builtin_argparse.cpp */, + CBB7725A1F11F93F00780A21 /* builtin_argparse.h */, 9C7A557C1DCD717C0049C25D /* fish_key_reader.cpp */, 4E142D731B56B5D7008783C8 /* config.h */, D0C6FCCB14CFA4B7004CE8AD /* autoload.h */, diff --git a/src/builtin.cpp b/src/builtin.cpp index 2ffc017e3..4cb9da31f 100644 --- a/src/builtin.cpp +++ b/src/builtin.cpp @@ -28,6 +28,7 @@ #include #include "builtin.h" +#include "builtin_argparse.h" #include "builtin_bg.h" #include "builtin_bind.h" #include "builtin_block.h" @@ -406,6 +407,7 @@ int builtin_false(parser_t &parser, io_streams_t &streams, wchar_t **argv) { static const builtin_data_t builtin_datas[] = { {L"[", &builtin_test, N_(L"Test a condition")}, {L"and", &builtin_generic, N_(L"Execute command if previous command suceeded")}, + {L"argparse", &builtin_argparse, N_(L"Parse options in fish script")}, {L"begin", &builtin_generic, N_(L"Create a block of code")}, {L"bg", &builtin_bg, N_(L"Send job to background")}, {L"bind", &builtin_bind, N_(L"Handle fish key bindings")}, diff --git a/src/builtin_argparse.cpp b/src/builtin_argparse.cpp new file mode 100644 index 000000000..f6147700c --- /dev/null +++ b/src/builtin_argparse.cpp @@ -0,0 +1,354 @@ +// Implementation of the argparse builtin. +// +// See issue #4190 for the rationale behind the original behavior of this builtin. +#include "config.h" // IWYU pragma: keep + +#if 0 +#include +#endif + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "builtin.h" +#include "builtin_argparse.h" +#include "common.h" +#include "env.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; + +#define BUILTIN_ERR_INVALID_OPT_SPEC _(L"%ls: Invalid option spec '%ls' at char '%lc'\n") + +class option_spec_t { + public: + wchar_t short_flag; + wcstring long_flag; + 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) {} +}; + +class argparse_cmd_opts_t { + public: + bool print_help = false; + bool require_order = false; + wcstring name = L"argparse"; + wcstring_list_t argv; + struct std::map options; + + ~argparse_cmd_opts_t() { + for (auto it : options) { + delete it.second; + } + } +}; + +static const wchar_t *short_options = L"+:hn:r"; +static const struct woption long_options[] = {{L"require-order", no_argument, NULL, 'r'}, + {L"name", required_argument, NULL, 'n'}, + {L"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0}}; + +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 (*s == '+') { + opt_spec->num_allowed = 2; // mandatory arg and can appear more than once + s++; + } else if (*s == ':') { + s++; + if (*s == ':') { + opt_spec->num_allowed = -1; // optional arg + s++; + } else { + opt_spec->num_allowed = 1; // mandatory arg and can appear only once + } + } + + if (*s) { + streams.err.append_format(BUILTIN_ERR_INVALID_OPT_SPEC, opts.name.c_str(), + option_spec.c_str(), *s); + return false; + } + + opts.options.emplace(opt_spec->short_flag, opt_spec); + return true; +} + +// This parses an option spec string into a struct option_spec. +static bool parse_option_spec(argparse_cmd_opts_t &opts, wcstring option_spec, + io_streams_t &streams) { + if (option_spec.empty()) { + streams.err.append_format(_(L"%s: An option spec must have a short flag letter\n"), + opts.name.c_str()); + return false; + } + + const wchar_t *s = option_spec.c_str(); + option_spec_t *opt_spec = new option_spec_t(*s++); + + if (*s == '/') { + s++; // the struct is initialized assuming short_flag_valid should be true + } else if (*s == '-') { + opt_spec->short_flag_valid = false; + s++; + } else { + // Long flag name not allowed if second char isn't '/' or '-' so just check for + // behavior modifier chars. + return parse_flag_modifiers(opts, opt_spec, option_spec, s, streams); + } + + const wchar_t *e = s; + while (*e && *e != '+' && *e != ':') e++; + if (e == s) { + streams.err.append_format(BUILTIN_ERR_INVALID_OPT_SPEC, opts.name.c_str(), + option_spec.c_str(), *(s - 1)); + return false; + } + + opt_spec->long_flag = wcstring(s, e - s); + return parse_flag_modifiers(opts, opt_spec, option_spec, e, streams); +} + +static int parse_cmd_opts(argparse_cmd_opts_t &opts, int *optind, //!OCLINT(high ncss method) + int argc, wchar_t **argv, parser_t &parser, io_streams_t &streams) { + wchar_t *cmd = argv[0]; + int opt; + wgetopter_t w; + while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, NULL)) != -1) { + switch (opt) { + case 'n': { + opts.name = w.woptarg; + break; + } + case 'r': { + opts.require_order = true; + break; + } + case 'h': { + opts.print_help = true; + break; + } + case ':': { + builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]); + return STATUS_INVALID_ARGS; + } + case '?': { + builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]); + return STATUS_INVALID_ARGS; + } + default: { + DIE("unexpected retval from wgetopt_long"); + break; + } + } + } + + *optind = w.woptind; + return STATUS_CMD_OK; +} + +static void populate_option_strings( + argparse_cmd_opts_t &opts, wcstring &short_options, + std::unique_ptr> &long_options) { + int i = 0; + for (auto it : opts.options) { + option_spec_t *opt_spec = it.second; + if (opt_spec->short_flag_valid) short_options.push_back(opt_spec->short_flag); + + int arg_type = no_argument; + if (opt_spec->num_allowed == -1) { + arg_type = optional_argument; + if (opt_spec->short_flag_valid) short_options.append(L"::"); + } else if (opt_spec->num_allowed > 0) { + arg_type = required_argument; + if (opt_spec->short_flag_valid) short_options.append(L":"); + } + + if (!opt_spec->long_flag.empty()) { + long_options.get()[i++] = {opt_spec->long_flag.c_str(), arg_type, NULL, + opt_spec->short_flag}; + } + } + long_options.get()[i] = {NULL, 0, NULL, 0}; +} + +// This function mimics the `wgetopt_long()` usage found elsewhere in our other builtin commands. +// It's different in that the short and long option structures are constructed dynamically based on +// arguments provided to the `argparse` command. +static int argparse_parse_args(argparse_cmd_opts_t &opts, const wcstring_list_t &args, + parser_t &parser, io_streams_t &streams) { + if (args.empty()) return STATUS_CMD_OK; + + wcstring short_options = opts.require_order ? L"+:" : L":"; + int nflags = opts.options.size(); + // This assumes every option has a long flag. Which is the worst case and isn't worth optimizing + // since the number of options is always quite small. Thus the size of the array will never be + // much larger than the minimum size required for all the long options. + auto long_options = std::unique_ptr>( + new woption[nflags + 1], [](woption *p) { delete[] p; }); + populate_option_strings(opts, short_options, long_options); + + const wchar_t *cmd = opts.name.c_str(); + int argc = args.size(); + + // This is awful but we need to convert our wcstring_list_t to a that can be passed + // to w.wgetopt_long(). Furthermore, because we're dynamically allocating the array of pointers + // we need to ensure the memory for the data structure is freed when we leave this scope. + null_terminated_array_t argv_container(args); + auto argv = (wchar_t **)argv_container.get(); + + int opt; + wgetopter_t w; + auto long_opts = long_options.get(); + while ((opt = w.wgetopt_long(argc, argv, short_options.c_str(), long_opts, NULL)) != -1) { + switch (opt) { + case ':': { + builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]); + return STATUS_INVALID_ARGS; + } + case '?': { + builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]); + return STATUS_INVALID_ARGS; + } + default: { + auto found = opts.options.find(opt); + assert(found != opts.options.end()); + + option_spec_t *opt_spec = found->second; + 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) { + // This is subtle. 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. + // Otherwise we can treat the optional and mandatory arg cases the same. That + // is, store the arg as the only value for the flag. Even if we've seen earlier + // instances of the flag. + opt_spec->vals.clear(); + if (w.woptarg) { + opt_spec->vals.push_back(w.woptarg); + } + } else { + assert(w.woptarg); + opt_spec->vals.push_back(w.woptarg); + } + break; + } + } + } + + // Add a count for how many times we saw each boolean flag but only if we saw the flag at least + // once. + for (auto it : opts.options) { + auto opt_spec = it.second; + if (opt_spec->num_allowed != 0 || opt_spec->num_seen == 0) continue; + wchar_t count[20]; + swprintf(count, sizeof count / sizeof count[0], L"%d", opt_spec->num_seen); + opt_spec->vals.push_back(wcstring(count)); + } + + for (int i = w.woptind; argv[i]; i++) opts.argv.push_back(argv[i]); + return STATUS_CMD_OK; +} + +/// The argparse builtin. This is explicitly not compatible with the BSD or GNU version of this +/// command. That's because fish doesn't have the weird quoting problems of POSIX shells. So we +/// don't need to support flags like `--unquoted`. Similarly we don't want to support introducing +/// long options with a single dash so we don't support the `--alternative` flag. That `getopt` is +/// an external command also means its output has to be in a form that can be eval'd. Because our +/// version is a builtin it can directly set variables local to the current scope (e.g., a +/// function). It doesn't need to write anything to stdout that then needs to be eval'd. +int builtin_argparse(parser_t &parser, io_streams_t &streams, wchar_t **argv) { + const wchar_t *cmd = argv[0]; + int argc = builtin_count_args(argv); + argparse_cmd_opts_t opts; + + int optind; + int retval = parse_cmd_opts(opts, &optind, argc, argv, parser, streams); + if (retval != STATUS_CMD_OK) return retval; + + if (opts.print_help) { + builtin_print_help(parser, streams, cmd, streams.out); + return STATUS_CMD_OK; + } + + if (optind == argc) { + streams.err.append_format(BUILTIN_ERR_ARG_COUNT1, cmd, 2, 0); + return STATUS_INVALID_ARGS; + } + + while (true) { + if (wcscmp(L"--", argv[optind]) == 0) { + optind++; + break; + } + + if (!parse_option_spec(opts, argv[optind], streams)) { + return STATUS_CMD_ERROR; + } + + if (++optind == argc) { + streams.err.append_format(BUILTIN_ERR_ARG_COUNT1, cmd, 2, 0); + return STATUS_INVALID_ARGS; + } + } + +#if 0 + auto stats = mstats(); + fwprintf(stderr, L"WTF %d bytes_total %lu chunks_used %lu bytes_used %lu chunks_free %lu bytes_free %lu\n", __LINE__, stats.bytes_total, stats.chunks_used, stats.bytes_used, stats.chunks_free); +#endif + + wcstring_list_t args; + args.push_back(opts.name); + while (optind < argc) args.push_back(argv[optind++]); + + retval = argparse_parse_args(opts, args, parser, streams); + if (retval != STATUS_CMD_OK) return retval; + + for (auto it : opts.options) { + 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); + env_set(var_name_prefix + opt_spec->short_flag, *val == ENV_NULL ? NULL : val->c_str(), + ENV_LOCAL); + if (!opt_spec->long_flag.empty()) { + // We do a simple replacement of all non alphanum chars rather than calling + // escape_string(long_flag, 0, STRING_STYLE_VAR). + wcstring long_flag = opt_spec->long_flag; + for (size_t pos = 0; pos < long_flag.size(); pos++) { + if (!iswalnum(long_flag[pos])) long_flag[pos] = L'_'; + } + env_set(var_name_prefix + long_flag, *val == ENV_NULL ? NULL : val->c_str(), + ENV_LOCAL); + } + } + + auto val = list_to_array_val(opts.argv); + env_set(L"argv", *val == ENV_NULL ? NULL : val->c_str(), ENV_LOCAL); + +#if 0 + stats = mstats(); + fwprintf(stderr, L"WTF %d bytes_total %lu chunks_used %lu bytes_used %lu chunks_free %lu bytes_free %lu\n", __LINE__, stats.bytes_total, stats.chunks_used, stats.bytes_used, stats.chunks_free); +#endif + + return retval; +} diff --git a/src/builtin_argparse.h b/src/builtin_argparse.h new file mode 100644 index 000000000..b515e0bce --- /dev/null +++ b/src/builtin_argparse.h @@ -0,0 +1,9 @@ +// Prototypes for executing builtin_getopt function. +#ifndef FISH_BUILTIN_ARGPARSE_H +#define FISH_BUILTIN_ARGPARSE_H + +class parser_t; +struct io_streams_t; + +int builtin_argparse(parser_t &parser, io_streams_t &streams, wchar_t **argv); +#endif