diff --git a/Userland/Libraries/LibCore/ArgsParser.cpp b/Userland/Libraries/LibCore/ArgsParser.cpp index 4a331453d1..a4f56bba20 100644 --- a/Userland/Libraries/LibCore/ArgsParser.cpp +++ b/Userland/Libraries/LibCore/ArgsParser.cpp @@ -5,6 +5,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include "AK/JsonObject.h" #include #include #include @@ -30,6 +31,7 @@ ArgsParser::ArgsParser() { add_option(m_show_help, "Display help message and exit", "help", 0); add_option(m_show_version, "Print version", "version", 0); + add_option(m_perform_autocomplete, "Perform autocompletion", "complete", 0); } bool ArgsParser::parse(int argc, char* const* argv, FailureBehavior failure_behavior) @@ -110,7 +112,7 @@ bool ArgsParser::parse(int argc, char* const* argv, FailureBehavior failure_beha } // We're done processing options. - // Now let's show version or help if requested. + // Now let's show version or help if requested, or perform autocompletion if needed. if (m_show_version) { print_version(stdout); @@ -118,6 +120,7 @@ bool ArgsParser::parse(int argc, char* const* argv, FailureBehavior failure_beha exit(0); return false; } + if (m_show_help) { print_usage(stdout, argv[0]); if (failure_behavior == FailureBehavior::Exit || failure_behavior == FailureBehavior::PrintUsageAndExit) @@ -125,6 +128,13 @@ bool ArgsParser::parse(int argc, char* const* argv, FailureBehavior failure_beha return false; } + if (m_perform_autocomplete) { + autocomplete(stdout, argv[0], Span { argv + optind, static_cast(argc - optind) }); + if (failure_behavior == FailureBehavior::Exit || failure_behavior == FailureBehavior::PrintUsageAndExit) + exit(0); + return false; + } + // Now let's parse positional arguments. int values_left = argc - optind; @@ -628,4 +638,110 @@ void ArgsParser::add_positional_argument(Vector& values, char const* add_positional_argument(move(arg)); } +void ArgsParser::autocomplete(FILE* file, StringView program_name, Span remaining_arguments) +{ + // We expect the full invocation of the program to be available as positional args, + // e.g. `foo --bar arg -b` (program invoked as `foo --complete -- foo --bar arg -b`) + auto first = true; + auto seen_all_options = false; + auto skip_next = false; + + StringView argument_to_complete; + StringView option_to_complete; + auto completing_option = false; + + for (auto& arg : remaining_arguments) { + StringView argument { arg }; + + completing_option = false; + if (skip_next) { + argument_to_complete = argument; + skip_next = false; + continue; + } + + // Skip over the program name. + if (first && program_name == argument) { + first = false; + continue; + } + + if (seen_all_options) { + argument_to_complete = argument; + continue; + } + + if (argument.starts_with("--")) { + option_to_complete = argument; + completing_option = true; + + if (argument == "--") { + seen_all_options = true; + continue; + } + + // Look for a long option + auto option_pattern = argument.substring_view(2); + auto it = m_options.find_if([&](auto& option) { return StringView(option.long_name) == option_pattern; }); + if (it.is_end()) + continue; + + if (it->requires_argument) + skip_next = true; + continue; + } + + if (argument.starts_with("-")) { + option_to_complete = argument; + completing_option = true; + + if (argument == "-") { + option_to_complete = argument; + continue; + } + + // Look for a short option + auto option_pattern = argument[argument.length() - 1]; + auto it = m_options.find_if([&](auto& option) { return option.short_name == option_pattern; }); + if (it.is_end()) + continue; + + if (it->requires_argument) + skip_next = true; + continue; + } + } + + // We don't know how to complete arguments quite yet. + if (!completing_option) + return; + + auto write_completion = [&](auto format, auto... args) { + JsonObject object; + object.set("completion", String::formatted(format, args...)); + object.set("static_offset", 0); + object.set("invariant_offset", option_to_complete.length()); + outln(file, "{}", object.to_string()); + }; + + if (option_to_complete.starts_with("--")) { + // Complete a long option. + auto option_pattern = option_to_complete.substring_view(2); + for (auto& option : m_options) { + StringView option_string = option.long_name; + if (option_string.starts_with(option_pattern)) { + write_completion("--{}", option_string); + } + } + } else { + // Complete a short option, note that we're not going to attempt to 'match' anything here. + for (auto& option : m_options) { + if (option.short_name == 0) + continue; + + write_completion("-{}", option.short_name); + } + } +} + } diff --git a/Userland/Libraries/LibCore/ArgsParser.h b/Userland/Libraries/LibCore/ArgsParser.h index bdc2d63a84..e7a7a30777 100644 --- a/Userland/Libraries/LibCore/ArgsParser.h +++ b/Userland/Libraries/LibCore/ArgsParser.h @@ -32,11 +32,11 @@ public: struct Option { bool requires_argument { true }; - const char* help_string { nullptr }; - const char* long_name { nullptr }; + char const* help_string { nullptr }; + char const* long_name { nullptr }; char short_name { 0 }; - const char* value_name { nullptr }; - Function accept_value; + char const* value_name { nullptr }; + Function accept_value; String name_for_display() const { @@ -47,11 +47,11 @@ public: }; struct Arg { - const char* help_string { nullptr }; - const char* name { nullptr }; + char const* help_string { nullptr }; + char const* name { nullptr }; int min_values { 0 }; int max_values { 1 }; - Function accept_value; + Function accept_value; }; bool parse(int argc, char* const* argv, FailureBehavior failure_behavior = FailureBehavior::PrintUsageAndExit); @@ -61,42 +61,45 @@ public: } // *Without* trailing newline! - void set_general_help(const char* help_string) { m_general_help = help_string; }; + void set_general_help(char const* help_string) { m_general_help = help_string; }; void set_stop_on_first_non_option(bool stop_on_first_non_option) { m_stop_on_first_non_option = stop_on_first_non_option; } - void print_usage(FILE*, const char* argv0); - void print_usage_terminal(FILE*, const char* argv0); - void print_usage_markdown(FILE*, const char* argv0); + void print_usage(FILE*, char const* argv0); + void print_usage_terminal(FILE*, char const* argv0); + void print_usage_markdown(FILE*, char const* argv0); void print_version(FILE*); void add_option(Option&&); - void add_ignored(const char* long_name, char short_name); - void add_option(bool& value, const char* help_string, const char* long_name, char short_name); - void add_option(const char*& value, const char* help_string, const char* long_name, char short_name, const char* value_name); - void add_option(String& value, const char* help_string, const char* long_name, char short_name, const char* value_name); + void add_ignored(char const* long_name, char short_name); + void add_option(bool& value, char const* help_string, char const* long_name, char short_name); + void add_option(char const*& value, char const* help_string, char const* long_name, char short_name, char const* value_name); + void add_option(String& value, char const* help_string, char const* long_name, char short_name, char const* value_name); void add_option(StringView& value, char const* help_string, char const* long_name, char short_name, char const* value_name); - void add_option(int& value, const char* help_string, const char* long_name, char short_name, const char* value_name); - void add_option(unsigned& value, const char* help_string, const char* long_name, char short_name, const char* value_name); - void add_option(double& value, const char* help_string, const char* long_name, char short_name, const char* value_name); - void add_option(Optional& value, const char* help_string, const char* long_name, char short_name, const char* value_name); - void add_option(Optional& value, const char* help_string, const char* long_name, char short_name, const char* value_name); + void add_option(int& value, char const* help_string, char const* long_name, char short_name, char const* value_name); + void add_option(unsigned& value, char const* help_string, char const* long_name, char short_name, char const* value_name); + void add_option(double& value, char const* help_string, char const* long_name, char short_name, char const* value_name); + void add_option(Optional& value, char const* help_string, char const* long_name, char short_name, char const* value_name); + void add_option(Optional& value, char const* help_string, char const* long_name, char short_name, char const* value_name); void add_positional_argument(Arg&&); - void add_positional_argument(const char*& value, const char* help_string, const char* name, Required required = Required::Yes); - void add_positional_argument(String& value, const char* help_string, const char* name, Required required = Required::Yes); + void add_positional_argument(char const*& value, char const* help_string, char const* name, Required required = Required::Yes); + void add_positional_argument(String& value, char const* help_string, char const* name, Required required = Required::Yes); void add_positional_argument(StringView& value, char const* help_string, char const* name, Required required = Required::Yes); - void add_positional_argument(int& value, const char* help_string, const char* name, Required required = Required::Yes); - void add_positional_argument(unsigned& value, const char* help_string, const char* name, Required required = Required::Yes); - void add_positional_argument(double& value, const char* help_string, const char* name, Required required = Required::Yes); - void add_positional_argument(Vector& value, const char* help_string, const char* name, Required required = Required::Yes); + void add_positional_argument(int& value, char const* help_string, char const* name, Required required = Required::Yes); + void add_positional_argument(unsigned& value, char const* help_string, char const* name, Required required = Required::Yes); + void add_positional_argument(double& value, char const* help_string, char const* name, Required required = Required::Yes); + void add_positional_argument(Vector& value, char const* help_string, char const* name, Required required = Required::Yes); void add_positional_argument(Vector& value, char const* help_string, char const* name, Required required = Required::Yes); private: + void autocomplete(FILE*, StringView program_name, Span remaining_arguments); + Vector