ArgsManager: support command-specific options

This commit is contained in:
Anthony Towns
2023-12-05 22:39:39 +10:00
parent 1a4371cc3d
commit d21e82b7d6
3 changed files with 73 additions and 27 deletions

View File

@@ -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<std::string> 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<std::string, Arg>& 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<std::string>& 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<std::string> TEST_OPTIONS_DOC{

View File

@@ -19,6 +19,7 @@
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
@@ -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<std::string> m_network_only_args GUARDED_BY(cs_args);
std::map<OptionsCategory, std::map<std::string, Arg>> m_available_args GUARDED_BY(cs_args);
std::optional<unsigned int> m_default_flags GUARDED_BY(cs_args){};
std::map<std::string, std::set<std::string>> m_command_args GUARDED_BY(cs_args);
bool m_accept_any_command GUARDED_BY(cs_args){true};
std::list<SectionInfo> m_config_sections GUARDED_BY(cs_args);
std::optional<fs::path> 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<std::string> 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=<user>")
* @param option Option name (e.g. "-rpcuser")
* @param help_param Help parameter (e.g. "=<user>" 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

View File

@@ -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<int>(), random_string_1);