diff --git a/src/common/args.cpp b/src/common/args.cpp index 12e0f26ed21..39dbbce0b26 100644 --- a/src/common/args.cpp +++ b/src/common/args.cpp @@ -598,7 +598,7 @@ void ArgsManager::ForceSetArg(const std::string& strArg, const std::string& strV m_settings.forced_settings[SettingName(strArg)] = strValue; } -void ArgsManager::AddCommand(const std::string& cmd, const std::string& help) +void ArgsManager::AddCommand(const std::string& cmd, const std::string& help, std::set options) { Assert(cmd.find('=') == std::string::npos); Assert(cmd.at(0) != '-'); @@ -607,6 +607,18 @@ void ArgsManager::AddCommand(const std::string& cmd, const std::string& help) m_accept_any_command = false; // latch to false std::map& arg_map = m_available_args[OptionsCategory::COMMANDS]; auto ret = arg_map.emplace(cmd, Arg{"", help, ArgsManager::COMMAND}); + if (!options.empty()) { + auto& cmdopts = m_available_args[OptionsCategory::COMMAND_OPTIONS]; + bool command_has_all_options_defined = true; + for (const auto& opt : options) { + if (!cmdopts.contains(opt)) { + command_has_all_options_defined = false; + } + } + Assert(command_has_all_options_defined); + + m_command_args.try_emplace(cmd, std::move(options)); + } Assert(ret.second); // Fail on duplicate commands } @@ -643,6 +655,7 @@ void ArgsManager::ClearArgs() LOCK(cs_args); m_settings = {}; m_available_args.clear(); + m_command_args.clear(); m_network_only_args.clear(); m_config_sections.clear(); } @@ -670,8 +683,20 @@ std::string ArgsManager::GetHelpMessage() const std::string usage; LOCK(cs_args); - for (const auto& arg_map : m_available_args) { - switch(arg_map.first) { + + const auto command_options = m_available_args.find(OptionsCategory::COMMAND_OPTIONS); + const auto for_matching_cmd_opts = [&](const std::set& select, auto&& fn) EXCLUSIVE_LOCKS_REQUIRED(cs_args) { + if (select.empty()) return; + if (command_options == m_available_args.end()) return; + for (const auto& [name, info] : command_options->second) { + if (!show_debug && (info.m_flags & ArgsManager::DEBUG_ONLY)) continue; + if (!select.contains(name)) continue; + fn(name, info); + } + }; + + for (const auto& [category, category_args] : m_available_args) { + switch(category) { case OptionsCategory::OPTIONS: usage += HelpMessageGroup("Options:"); break; @@ -717,22 +742,27 @@ std::string ArgsManager::GetHelpMessage() const case OptionsCategory::CLI_COMMANDS: usage += HelpMessageGroup("CLI Commands:"); break; + case OptionsCategory::COMMAND_OPTIONS: case OptionsCategory::HIDDEN: break; } // no default case, so the compiler can warn about missing cases - // When we get to the hidden options, stop - if (arg_map.first == OptionsCategory::HIDDEN) break; + if (category == OptionsCategory::COMMAND_OPTIONS) continue; - for (const auto& arg : arg_map.second) { - if (show_debug || !(arg.second.m_flags & ArgsManager::DEBUG_ONLY)) { - std::string name; - if (arg.second.m_help_param.empty()) { - name = arg.first; - } else { - name = arg.first + arg.second.m_help_param; + // When we get to the hidden options, stop + if (category == OptionsCategory::HIDDEN) break; + + for (const auto& [arg_name, arg_info] : category_args) { + if (show_debug || !(arg_info.m_flags & ArgsManager::DEBUG_ONLY)) { + usage += HelpMessageOpt(arg_name, arg_info.m_help_param, arg_info.m_help_text); + + if (category == OptionsCategory::COMMANDS) { + const auto cmd_args = m_command_args.find(arg_name); + if (cmd_args == m_command_args.end()) continue; + for_matching_cmd_opts(cmd_args->second, [&](const auto& cmdopt_name, const auto& cmdopt_info) { + usage += HelpMessageOpt(cmdopt_name, cmdopt_info.m_help_param, cmdopt_info.m_help_text, /*subopt=*/true); + }); } - usage += HelpMessageOpt(name, arg.second.m_help_text); } } } @@ -750,19 +780,26 @@ void SetupHelpOptions(ArgsManager& args) args.AddHiddenArgs({"-h", "-?"}); } -static const int screenWidth = 79; -static const int optIndent = 2; -static const int msgIndent = 7; - std::string HelpMessageGroup(const std::string &message) { return std::string(message) + std::string("\n\n"); } -std::string HelpMessageOpt(const std::string &option, const std::string &message) { - return std::string(optIndent,' ') + std::string(option) + - std::string("\n") + std::string(msgIndent,' ') + - FormatParagraph(message, screenWidth - msgIndent, msgIndent) + - std::string("\n\n"); +std::string HelpMessageOpt(std::string_view option, std::string_view help_param, std::string_view message, bool subopt) +{ + constexpr int screen_width = 79; + int opt_indent = 2; + int msg_indent = 7; + + if (subopt) { + int bump = msg_indent - opt_indent; + opt_indent += bump; // opt_indent now at the old msg_indent level + msg_indent += bump; // indent by the same amount + } + int msg_width = screen_width - msg_indent; + + return strprintf("%*s%s%s\n%*s%s\n\n", + opt_indent, "", option, help_param, + msg_indent, "", FormatParagraph(message, msg_width, msg_indent)); } const std::vector TEST_OPTIONS_DOC{ diff --git a/src/common/args.h b/src/common/args.h index de323d3c490..766b2b229bc 100644 --- a/src/common/args.h +++ b/src/common/args.h @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -67,6 +68,10 @@ enum class OptionsCategory { CLI_COMMANDS, IPC, + // Specific to one or more commands (OptionsCategory::COMMANDS) + // These are only included in help with their associated commands. + COMMAND_OPTIONS, + HIDDEN // Always the last option to avoid printing these in the help }; @@ -142,6 +147,7 @@ private: std::set m_network_only_args GUARDED_BY(cs_args); std::map> m_available_args GUARDED_BY(cs_args); std::optional m_default_flags GUARDED_BY(cs_args){}; + std::map> m_command_args GUARDED_BY(cs_args); bool m_accept_any_command GUARDED_BY(cs_args){true}; std::list m_config_sections GUARDED_BY(cs_args); std::optional m_config_path GUARDED_BY(cs_args); @@ -360,9 +366,9 @@ public: void AddArg(const std::string& name, const std::string& help, unsigned int flags, const OptionsCategory& cat) EXCLUSIVE_LOCKS_REQUIRED(!cs_args); /** - * Add subcommand + * Add command */ - void AddCommand(const std::string& cmd, const std::string& help) EXCLUSIVE_LOCKS_REQUIRED(!cs_args); + void AddCommand(const std::string& cmd, const std::string& help, std::set options = {}) EXCLUSIVE_LOCKS_REQUIRED(!cs_args); /** * Add many hidden arguments @@ -491,10 +497,12 @@ std::string HelpMessageGroup(const std::string& message); /** * Format a string to be used as option description in help messages * - * @param option Option message (e.g. "-rpcuser=") + * @param option Option name (e.g. "-rpcuser") + * @param help_param Help parameter (e.g. "=" or "") * @param message Option description (e.g. "Username for JSON-RPC connections") + * @param subopt True if this is a suboption, instead of a top-level option. * @return the formatted string */ -std::string HelpMessageOpt(const std::string& option, const std::string& message); +std::string HelpMessageOpt(std::string_view option, std::string_view help_param, std::string_view message, bool subopt = false); #endif // BITCOIN_COMMON_ARGS_H diff --git a/src/test/fuzz/string.cpp b/src/test/fuzz/string.cpp index a7f8516f111..02292774c17 100644 --- a/src/test/fuzz/string.cpp +++ b/src/test/fuzz/string.cpp @@ -67,7 +67,8 @@ FUZZ_TARGET(string) (void)HelpExampleCli(random_string_1, random_string_2); (void)HelpExampleRpc(random_string_1, random_string_2); (void)HelpMessageGroup(random_string_1); - (void)HelpMessageOpt(random_string_1, random_string_2); + (void)HelpMessageOpt(random_string_1, "", random_string_2); + (void)HelpMessageOpt(random_string_1, random_string_2, ""); (void)IsDeprecatedRPCEnabled(random_string_1); (void)Join(random_string_vector, random_string_1); (void)JSONRPCError(fuzzed_data_provider.ConsumeIntegral(), random_string_1);