ArgsManager: support command-specific options
What changed, and why it matters
This commit is a code cleanup and feature addition for Bitcoin Core's command-line help system. It lets individual commands (like 'getblock' or 'send') declare their own specific options, and shows those options indented under the command in help output. There is no direct security bug here; it is a refactor that changes how help text is formatted and how command-specific options are registered.
No security action required. Review as normal code-quality/feature change. If auditing, verify that all command-specific options are registered in OptionsCategory::COMMAND_OPTIONS before AddCommand() is called, because the new Assert() will crash on startup if they are not.
Security signals we found
No memory-unsafe operations introduced
No changes to authentication, authorization, or consensus code
New Assert() added to ensure command-specific options are pre-registered before being attached to a command
Help formatting refactor only; no input parsing changes
Fuzz test updated to cover new HelpMessageOpt signature
Evidence from the diff
The patch extends ArgsManager::AddCommand() to accept an optional set of command-specific option names, stores them in a new m_command_args map, and updates GetHelpMessage() to print those options as indented sub-options under their associated command. It also refactors HelpMessageOpt() to take separate option name, help parameter, and sub-option parameters, and updates a fuzz test accordingly. The changes are guarded by existing Assert() checks and do not alter network consensus, wallet handling, or RPC authorization logic.
Changed components
src/common/args.cppsrc/common/args.hsrc/test/fuzz/string.cppInspect captured patch +73 / −27
diff --git a/src/common/args.cpp b/src/common/args.cpp
index 12e0f26e..39dbbce0 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<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
+ if (category == OptionsCategory::COMMAND_OPTIONS) continue;
+
// When we get to the hidden options, stop
- if (arg_map.first == OptionsCategory::HIDDEN) break;
-
- 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;
+ 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{
diff --git a/src/common/args.h b/src/common/args.h
index de323d3c..766b2b22 100644
--- a/src/common/args.h
+++ b/src/common/args.h
@@ -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
diff --git a/src/test/fuzz/string.cpp b/src/test/fuzz/string.cpp
index a7f8516f..02292774 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<int>(), random_string_1);
Why this scored 17/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.