From d21e82b7d64a88e7a771ea21694d8e33e0c35867 Mon Sep 17 00:00:00 2001 From: Anthony Towns Date: Tue, 5 Dec 2023 22:39:39 +1000 Subject: [PATCH 1/5] ArgsManager: support command-specific options --- src/common/args.cpp | 81 +++++++++++++++++++++++++++++----------- src/common/args.h | 16 ++++++-- src/test/fuzz/string.cpp | 3 +- 3 files changed, 73 insertions(+), 27 deletions(-) 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); From 186354a0d8a69dd7603ae1630c4f4441f9d36279 Mon Sep 17 00:00:00 2001 From: Anthony Towns Date: Mon, 6 Nov 2023 12:50:38 +1000 Subject: [PATCH 2/5] bitcoin-wallet: use command-specific options --- src/bitcoin-wallet.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bitcoin-wallet.cpp b/src/bitcoin-wallet.cpp index 9b0136da4c4..f72d33b0bc5 100644 --- a/src/bitcoin-wallet.cpp +++ b/src/bitcoin-wallet.cpp @@ -37,14 +37,14 @@ static void SetupWalletToolArgs(ArgsManager& argsman) argsman.AddArg("-version", "Print version and exit", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-datadir=", "Specify data directory", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS); argsman.AddArg("-wallet=", "Specify wallet name", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::OPTIONS); - argsman.AddArg("-dumpfile=", "When used with 'dump', writes out the records to this file. When used with 'createfromdump', loads the records into a new wallet.", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS); + argsman.AddArg("-dumpfile=", "When used with 'dump', writes out the records to this file. When used with 'createfromdump', loads the records into a new wallet.", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::COMMAND_OPTIONS); argsman.AddArg("-debug=", "Output debugging information (default: 0).", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-printtoconsole", "Send trace/debug info to console (default: 1 when no -debug is true, 0 otherwise).", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST); argsman.AddCommand("info", "Get wallet info"); argsman.AddCommand("create", "Create a new descriptor wallet file"); - argsman.AddCommand("dump", "Print out all of the wallet key-value records"); - argsman.AddCommand("createfromdump", "Create new wallet file from dumped records"); + argsman.AddCommand("dump", "Print out all of the wallet key-value records", {"-dumpfile"}); + argsman.AddCommand("createfromdump", "Create new wallet file from dumped records", {"-dumpfile"}); } static std::optional WalletAppInit(ArgsManager& args, int argc, char* argv[]) From 33c8090be95f10f582a9f73402434810602daf60 Mon Sep 17 00:00:00 2001 From: Anthony Towns Date: Mon, 6 Nov 2023 18:58:01 +1000 Subject: [PATCH 3/5] ArgsManager: automate checking for correct command options --- src/common/args.cpp | 34 ++++++++++++++++++++++++++++++++++ src/common/args.h | 10 ++++++++++ src/wallet/wallettool.cpp | 9 ++++++--- test/functional/tool_wallet.py | 1 + 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/common/args.cpp b/src/common/args.cpp index 39dbbce0b26..e153d886f9c 100644 --- a/src/common/args.cpp +++ b/src/common/args.cpp @@ -387,6 +387,40 @@ std::optional ArgsManager::GetCommand() const return ret; } +bool ArgsManager::CheckCommandOptions(const std::string& command, std::vector* errors) const +{ + LOCK(cs_args); + + auto command_options = m_available_args.find(OptionsCategory::COMMAND_OPTIONS); + if (command_options == m_available_args.end()) { + // There are no command-specific options at all, so everything is fine + return true; + } + + const auto command_args = m_command_args.find(command); + auto is_valid_opt = [&](const auto& opt) EXCLUSIVE_LOCKS_REQUIRED(cs_args) -> bool { + if (command_args == m_command_args.end()) { + // Caller may not have checked that command actually exists + // before calling this function. In that case, treat it as + // having no valid command-specific options. + return false; + } else { + return command_args->second.contains(opt); + } + }; + + bool ok = true; + for (const auto& [arg, _] : command_options->second) { + if (!GetSetting_(arg).isNull() && !is_valid_opt(arg)) { + ok = false; + if (errors != nullptr) { + errors->emplace_back(strprintf("The %s option cannot be used with the '%s' command.", arg, command)); + } + } + } + return ok; +} + std::vector ArgsManager::GetArgs(const std::string& strArg) const { std::vector result; diff --git a/src/common/args.h b/src/common/args.h index 766b2b229bc..0a3195f89d6 100644 --- a/src/common/args.h +++ b/src/common/args.h @@ -225,6 +225,16 @@ public: */ std::optional GetCommand() const EXCLUSIVE_LOCKS_REQUIRED(!cs_args); + /** + * Check that any command-specific options the user specified are valid + * for the given command. + * + * @param[in] command The command being run. + * @param[out] errors If non-null, populated with a message for each invalid option. + * @return false if any command-specific options were specified that are not valid for this command + */ + bool CheckCommandOptions(const std::string& command, std::vector* errors = nullptr) const EXCLUSIVE_LOCKS_REQUIRED(!cs_args); + /** * Get blocks directory path * diff --git a/src/wallet/wallettool.cpp b/src/wallet/wallettool.cpp index 7c24c0e1c6a..6baab1f3f18 100644 --- a/src/wallet/wallettool.cpp +++ b/src/wallet/wallettool.cpp @@ -93,9 +93,12 @@ static void WalletShowInfo(CWallet* wallet_instance) bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command) { - if (args.IsArgSet("-dumpfile") && command != "dump" && command != "createfromdump") { - tfm::format(std::cerr, "The -dumpfile option can only be used with the \"dump\" and \"createfromdump\" commands.\n"); - return false; + { + std::vector details; + if (!args.CheckCommandOptions(command, &details)) { + tfm::format(std::cerr, "Error: Invalid arguments provided:\n%s\n", util::MakeUnorderedList(details)); + return false; + } } if ((command == "create" || command == "createfromdump") && !args.IsArgSet("-wallet")) { tfm::format(std::cerr, "Wallet name must be provided when creating a new wallet.\n"); diff --git a/test/functional/tool_wallet.py b/test/functional/tool_wallet.py index 737aaf54d16..845d7a9a236 100755 --- a/test/functional/tool_wallet.py +++ b/test/functional/tool_wallet.py @@ -421,6 +421,7 @@ class ToolWalletTest(BitcoinTestFramework): assert not (self.nodes[0].wallets_path / "legacy").exists() self.assert_raises_tool_error("Invalid parameter -descriptors", "-wallet=legacy", "-descriptors=false", "create") assert not (self.nodes[0].wallets_path / "legacy").exists() + self.assert_raises_tool_error("The -dumpfile option cannot be used with the 'create' command.", "-wallet=legacy", "-dumpfile=wallet.dump", "create") def test_no_create_unnamed(self): self.log.info("Test that unnamed (default) wallets cannot be created") From 92df78585928772bad2ffadee8635428246a79af Mon Sep 17 00:00:00 2001 From: Anthony Towns Date: Tue, 18 Nov 2025 15:25:13 +1000 Subject: [PATCH 4/5] tests: Add some test coverage for ArgsManager::AddCommand Co-Authored-By: l0rinc --- src/test/argsman_tests.cpp | 76 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/test/argsman_tests.cpp b/src/test/argsman_tests.cpp index c8802d3641c..da0d6840504 100644 --- a/src/test/argsman_tests.cpp +++ b/src/test/argsman_tests.cpp @@ -634,6 +634,82 @@ BOOST_AUTO_TEST_CASE(util_GetArg) BOOST_CHECK_EQUAL(testArgs.GetArg("pritest4", "default"), "b"); } +BOOST_AUTO_TEST_CASE(util_AddCommand) +{ + enum TestFail { SUCCESS, + PARSE_FAIL, + PARSE_ERROR, + NO_COMMAND, + COMMAND_OPTS_BAD_DETAILS, + COMMAND_OPTS }; + + auto testfn = [&](const auto& argv) -> TestFail { + TestArgsManager test_args; + test_args.AddArg("-opt1=", "Opt 1", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::COMMAND_OPTIONS); + test_args.AddArg("-opt2=", "Opt 2", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::COMMAND_OPTIONS); + test_args.AddArg("-opt3=", "Opt 3", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS); + + test_args.AddCommand("cmd1", "No specific options"); + test_args.AddCommand("cmd2", "Opt 1", {"-opt1"}); + test_args.AddCommand("cmd3", "Opt 1 or 2", {"-opt1", "-opt2"}); + + std::string error; + if (!test_args.ParseParameters(argv.size(), argv.data(), error)) return PARSE_FAIL; + if (!error.empty()) return PARSE_ERROR; + const auto command = test_args.GetCommand(); + if (!command) return NO_COMMAND; + std::vector details; + if (!test_args.CheckCommandOptions(command->command, &details)) { + if (details.empty()) return COMMAND_OPTS_BAD_DETAILS; + return COMMAND_OPTS; + } else if (!details.empty()) { + return COMMAND_OPTS_BAD_DETAILS; + } + return SUCCESS; + }; + + BOOST_CHECK_EQUAL(COMMAND_OPTS, testfn(std::array{"x", "-opt1=foo", "cmd1"})); + BOOST_CHECK_EQUAL(SUCCESS, testfn(std::array{"x", "cmd1", "-opt1=foo"})); // things after the command are "args" and left unparsed, not options + + BOOST_CHECK_EQUAL(SUCCESS, testfn(std::array{"x", "-opt1=foo", "cmd2"})); + BOOST_CHECK_EQUAL(SUCCESS, testfn(std::array{"x", "-opt1=foo", "cmd3"})); + BOOST_CHECK_EQUAL(COMMAND_OPTS, testfn(std::array{"x", "-opt2=foo", "cmd1"})); + BOOST_CHECK_EQUAL(COMMAND_OPTS, testfn(std::array{"x", "-opt2=foo", "cmd2"})); + BOOST_CHECK_EQUAL(SUCCESS, testfn(std::array{"x", "-opt2=foo", "cmd3"})); + BOOST_CHECK_EQUAL(SUCCESS, testfn(std::array{"x", "-opt3=foo", "cmd1"})); + BOOST_CHECK_EQUAL(SUCCESS, testfn(std::array{"x", "-opt3=foo", "cmd2"})); + BOOST_CHECK_EQUAL(SUCCESS, testfn(std::array{"x", "-opt3=foo", "cmd3"})); + + BOOST_CHECK_EQUAL(COMMAND_OPTS, testfn(std::array{"x", "-opt1=foo", "-opt3=bar", "-opt2=baz", "cmd1"})); + BOOST_CHECK_EQUAL(COMMAND_OPTS, testfn(std::array{"x", "-opt1=foo", "-opt3=bar", "-opt2=baz", "cmd2"})); + BOOST_CHECK_EQUAL(SUCCESS, testfn(std::array{"x", "-opt1=foo", "-opt3=bar", "-opt2=baz", "cmd3"})); + + BOOST_CHECK_EQUAL(PARSE_FAIL, testfn(std::array{"x", "cmd4"})); + BOOST_CHECK_EQUAL(NO_COMMAND, testfn(std::array{"x", "-opt3=foo"})); + BOOST_CHECK_EQUAL(PARSE_FAIL, testfn(std::array{"x", "-opt4=foo"})); +} + +BOOST_AUTO_TEST_CASE(util_AddCommand_clearargs_replaces_command_options) +{ + const auto add_command{[&](TestArgsManager& test_args, const std::string& option) { + test_args.AddArg(option, "option", ArgsManager::ALLOW_ANY, OptionsCategory::COMMAND_OPTIONS); + test_args.AddCommand("cmd", "cmd", {option}); + }}; + + TestArgsManager test_args; + add_command(test_args, "-opt1"); + test_args.ClearArgs(); + add_command(test_args, "-opt2"); + + const auto help{test_args.GetHelpMessage()}; + BOOST_CHECK(help.find("-opt2") != std::string::npos); + + test_args.ForceSetArg("-opt2", "1"); + std::vector details; + BOOST_CHECK(test_args.CheckCommandOptions("cmd", &details)); + BOOST_CHECK(details.empty()); +} + BOOST_AUTO_TEST_CASE(util_GetChainTypeString) { TestArgsManager test_args; From 89af67d79f297fd874d7dc12c5fb860e719560f5 Mon Sep 17 00:00:00 2001 From: Anthony Towns Date: Wed, 22 Apr 2026 17:58:25 +1000 Subject: [PATCH 5/5] tests: Add some fuzz test coverage for command-specific args --- src/test/fuzz/system.cpp | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/test/fuzz/system.cpp b/src/test/fuzz/system.cpp index 12c70c7a10b..9d0978ce780 100644 --- a/src/test/fuzz/system.cpp +++ b/src/test/fuzz/system.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -31,6 +32,7 @@ FUZZ_TARGET(system, .init = initialize_system) { FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); ArgsManager args_manager{}; + std::vector command_option_names; if (fuzzed_data_provider.ConsumeBool()) { SetupHelpOptions(args_manager); @@ -59,7 +61,7 @@ FUZZ_TARGET(system, .init = initialize_system) args_manager.SoftSetBoolArg(str_arg, f_value); }, [&] { - const OptionsCategory options_category = fuzzed_data_provider.PickValueInArray({OptionsCategory::OPTIONS, OptionsCategory::CONNECTION, OptionsCategory::WALLET, OptionsCategory::WALLET_DEBUG_TEST, OptionsCategory::ZMQ, OptionsCategory::DEBUG_TEST, OptionsCategory::CHAINPARAMS, OptionsCategory::NODE_RELAY, OptionsCategory::BLOCK_CREATION, OptionsCategory::RPC, OptionsCategory::GUI, OptionsCategory::COMMANDS, OptionsCategory::REGISTER_COMMANDS, OptionsCategory::CLI_COMMANDS, OptionsCategory::IPC, OptionsCategory::HIDDEN}); + const OptionsCategory options_category = fuzzed_data_provider.PickValueInArray({OptionsCategory::OPTIONS, OptionsCategory::CONNECTION, OptionsCategory::WALLET, OptionsCategory::WALLET_DEBUG_TEST, OptionsCategory::ZMQ, OptionsCategory::DEBUG_TEST, OptionsCategory::CHAINPARAMS, OptionsCategory::NODE_RELAY, OptionsCategory::BLOCK_CREATION, OptionsCategory::RPC, OptionsCategory::GUI, OptionsCategory::COMMANDS, OptionsCategory::REGISTER_COMMANDS, OptionsCategory::CLI_COMMANDS, OptionsCategory::IPC, OptionsCategory::COMMAND_OPTIONS, OptionsCategory::HIDDEN}); // Avoid hitting: // common/args.cpp:563: void ArgsManager::AddArg(const std::string &, const std::string &, unsigned int, const OptionsCategory &): Assertion `ret.second' failed. const std::string argument_name = GetArgumentName(fuzzed_data_provider.ConsumeRandomLengthString(16)); @@ -69,6 +71,22 @@ FUZZ_TARGET(system, .init = initialize_system) auto help = fuzzed_data_provider.ConsumeRandomLengthString(16); auto flags = fuzzed_data_provider.ConsumeIntegral() & ~ArgsManager::COMMAND; args_manager.AddArg(argument_name, help, flags, options_category); + if (options_category == OptionsCategory::COMMAND_OPTIONS) { + command_option_names.push_back(argument_name); + } + }, + [&] { + auto cmd = fuzzed_data_provider.ConsumeRandomLengthString(16); + if (cmd.empty() || cmd[0] == '-' || cmd.find('=') != std::string::npos) return; + if (args_manager.GetArgFlags(cmd) != std::nullopt) return; + auto help = fuzzed_data_provider.ConsumeRandomLengthString(16); + std::set options; + for (const auto& opt : command_option_names) { + if (fuzzed_data_provider.ConsumeBool()) { + options.insert(opt); + } + } + args_manager.AddCommand(cmd, help, std::move(options)); }, [&] { // Avoid hitting: @@ -89,6 +107,7 @@ FUZZ_TARGET(system, .init = initialize_system) }, [&] { args_manager.ClearArgs(); + command_option_names.clear(); }, [&] { const std::vector random_arguments = ConsumeRandomLengthStringVector(fuzzed_data_provider); @@ -120,6 +139,10 @@ FUZZ_TARGET(system, .init = initialize_system) } catch (const std::runtime_error&) { } (void)args_manager.GetHelpMessage(); + const auto command = args_manager.GetCommand(); + if (command) { + (void)args_manager.CheckCommandOptions(command->command); + } (void)args_manager.GetUnrecognizedSections(); (void)args_manager.GetUnsuitableSectionOnlyArgs(); (void)args_manager.IsArgNegated(s1);