Skip to content

Commit 4e946eb

Browse files
author
MarcoFalke
committed
Merge #20715: util: Add ArgsManager::GetCommand() and use it in bitcoin-wallet
fa61b9d util: Add ArgsManager::GetCommand() and use it in bitcoin-wallet (MarcoFalke) 7777105 refactor: Move all command dependend checks to ExecuteWalletToolFunc (MarcoFalke) fa06bce test: Add tests (MarcoFalke) fac05cc wallet: [refactor] Pass ArgsManager to WalletAppInit (MarcoFalke) Pull request description: This not only moves the parsing responsibility out from the wallet tool, but it also makes it easier to implement bitcoin-util #19937 Fixes: #20902 ACKs for top commit: ajtowns: ACK fa61b9d fjahr: Code review ACK fa61b9d Tree-SHA512: 79622b806e8bf9dcd0dc24a8a6687345710df57720992e83a41cd8d6762a6dc112044ebc58fcf6e8fbf45de29a79b04873c5b8c2494a1eaaf902a2884703e47b
2 parents 5a429d3 + fa61b9d commit 4e946eb

File tree

7 files changed

+115
-51
lines changed

7 files changed

+115
-51
lines changed

src/bitcoin-wallet.cpp

Lines changed: 28 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -33,59 +33,60 @@ static void SetupWalletToolArgs(ArgsManager& argsman)
3333
argsman.AddArg("-format=<format>", "The format of the wallet file to create. Either \"bdb\" or \"sqlite\". Only used with 'createfromdump'", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
3434
argsman.AddArg("-printtoconsole", "Send trace/debug info to console (default: 1 when no -debug is true, 0 otherwise).", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST);
3535

36-
argsman.AddArg("info", "Get wallet info", ArgsManager::ALLOW_ANY, OptionsCategory::COMMANDS);
37-
argsman.AddArg("create", "Create new wallet file", ArgsManager::ALLOW_ANY, OptionsCategory::COMMANDS);
38-
argsman.AddArg("salvage", "Attempt to recover private keys from a corrupt wallet. Warning: 'salvage' is experimental.", ArgsManager::ALLOW_ANY, OptionsCategory::COMMANDS);
39-
argsman.AddArg("dump", "Print out all of the wallet key-value records", ArgsManager::ALLOW_ANY, OptionsCategory::COMMANDS);
40-
argsman.AddArg("createfromdump", "Create new wallet file from dumped records", ArgsManager::ALLOW_ANY, OptionsCategory::COMMANDS);
36+
argsman.AddCommand("info", "Get wallet info", OptionsCategory::COMMANDS);
37+
argsman.AddCommand("create", "Create new wallet file", OptionsCategory::COMMANDS);
38+
argsman.AddCommand("salvage", "Attempt to recover private keys from a corrupt wallet. Warning: 'salvage' is experimental.", OptionsCategory::COMMANDS);
39+
argsman.AddCommand("dump", "Print out all of the wallet key-value records", OptionsCategory::COMMANDS);
40+
argsman.AddCommand("createfromdump", "Create new wallet file from dumped records", OptionsCategory::COMMANDS);
4141
}
4242

43-
static bool WalletAppInit(int argc, char* argv[])
43+
static bool WalletAppInit(ArgsManager& args, int argc, char* argv[])
4444
{
45-
SetupWalletToolArgs(gArgs);
45+
SetupWalletToolArgs(args);
4646
std::string error_message;
47-
if (!gArgs.ParseParameters(argc, argv, error_message)) {
47+
if (!args.ParseParameters(argc, argv, error_message)) {
4848
tfm::format(std::cerr, "Error parsing command line arguments: %s\n", error_message);
4949
return false;
5050
}
51-
if (argc < 2 || HelpRequested(gArgs) || gArgs.IsArgSet("-version")) {
51+
if (argc < 2 || HelpRequested(args) || args.IsArgSet("-version")) {
5252
std::string strUsage = strprintf("%s bitcoin-wallet version", PACKAGE_NAME) + " " + FormatFullVersion() + "\n";
53-
if (!gArgs.IsArgSet("-version")) {
54-
strUsage += "\n"
55-
"bitcoin-wallet is an offline tool for creating and interacting with " PACKAGE_NAME " wallet files.\n"
56-
"By default bitcoin-wallet will act on wallets in the default mainnet wallet directory in the datadir.\n"
57-
"To change the target wallet, use the -datadir, -wallet and -testnet/-regtest arguments.\n\n"
58-
"Usage:\n"
59-
" bitcoin-wallet [options] <command>\n";
60-
strUsage += "\n" + gArgs.GetHelpMessage();
61-
}
53+
if (!args.IsArgSet("-version")) {
54+
strUsage += "\n"
55+
"bitcoin-wallet is an offline tool for creating and interacting with " PACKAGE_NAME " wallet files.\n"
56+
"By default bitcoin-wallet will act on wallets in the default mainnet wallet directory in the datadir.\n"
57+
"To change the target wallet, use the -datadir, -wallet and -testnet/-regtest arguments.\n\n"
58+
"Usage:\n"
59+
" bitcoin-wallet [options] <command>\n";
60+
strUsage += "\n" + args.GetHelpMessage();
61+
}
6262
tfm::format(std::cout, "%s", strUsage);
6363
return false;
6464
}
6565

6666
// check for printtoconsole, allow -debug
67-
LogInstance().m_print_to_console = gArgs.GetBoolArg("-printtoconsole", gArgs.GetBoolArg("-debug", false));
67+
LogInstance().m_print_to_console = args.GetBoolArg("-printtoconsole", args.GetBoolArg("-debug", false));
6868

6969
if (!CheckDataDirOption()) {
70-
tfm::format(std::cerr, "Error: Specified data directory \"%s\" does not exist.\n", gArgs.GetArg("-datadir", ""));
70+
tfm::format(std::cerr, "Error: Specified data directory \"%s\" does not exist.\n", args.GetArg("-datadir", ""));
7171
return false;
7272
}
7373
// Check for chain settings (Params() calls are only valid after this clause)
74-
SelectParams(gArgs.GetChainName());
74+
SelectParams(args.GetChainName());
7575

7676
return true;
7777
}
7878

7979
int main(int argc, char* argv[])
8080
{
81+
ArgsManager& args = gArgs;
8182
#ifdef WIN32
8283
util::WinCmdLineArgs winArgs;
8384
std::tie(argc, argv) = winArgs.get();
8485
#endif
8586
SetupEnvironment();
8687
RandomInit();
8788
try {
88-
if (!WalletAppInit(argc, argv)) return EXIT_FAILURE;
89+
if (!WalletAppInit(args, argc, argv)) return EXIT_FAILURE;
8990
} catch (const std::exception& e) {
9091
PrintExceptionContinue(&e, "WalletAppInit()");
9192
return EXIT_FAILURE;
@@ -94,33 +95,19 @@ int main(int argc, char* argv[])
9495
return EXIT_FAILURE;
9596
}
9697

97-
std::string method {};
98-
for(int i = 1; i < argc; ++i) {
99-
if (!IsSwitchChar(argv[i][0])) {
100-
if (!method.empty()) {
101-
tfm::format(std::cerr, "Error: two methods provided (%s and %s). Only one method should be provided.\n", method, argv[i]);
102-
return EXIT_FAILURE;
103-
}
104-
method = argv[i];
105-
}
106-
}
107-
108-
if (method.empty()) {
98+
const auto command = args.GetCommand();
99+
if (!command) {
109100
tfm::format(std::cerr, "No method provided. Run `bitcoin-wallet -help` for valid methods.\n");
110101
return EXIT_FAILURE;
111102
}
112-
113-
// A name must be provided when creating a file
114-
if (method == "create" && !gArgs.IsArgSet("-wallet")) {
115-
tfm::format(std::cerr, "Wallet name must be provided when creating a new wallet.\n");
103+
if (command->args.size() != 0) {
104+
tfm::format(std::cerr, "Error: Additional arguments provided (%s). Methods do not take arguments. Please refer to `-help`.\n", Join(command->args, ", "));
116105
return EXIT_FAILURE;
117106
}
118107

119-
std::string name = gArgs.GetArg("-wallet", "");
120-
121108
ECCVerifyHandle globalVerifyHandle;
122109
ECC_Start();
123-
if (!WalletTool::ExecuteWalletToolFunc(gArgs, method, name)) {
110+
if (!WalletTool::ExecuteWalletToolFunc(args, command->command)) {
124111
return EXIT_FAILURE;
125112
}
126113
ECC_Stop();

src/test/fuzz/system.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ FUZZ_TARGET(system)
5454
if (args_manager.GetArgFlags(argument_name) != nullopt) {
5555
return;
5656
}
57-
args_manager.AddArg(argument_name, fuzzed_data_provider.ConsumeRandomLengthString(16), fuzzed_data_provider.ConsumeIntegral<unsigned int>(), options_category);
57+
args_manager.AddArg(argument_name, fuzzed_data_provider.ConsumeRandomLengthString(16), fuzzed_data_provider.ConsumeIntegral<unsigned int>() & ~ArgsManager::COMMAND, options_category);
5858
},
5959
[&] {
6060
// Avoid hitting:

src/util/system.cpp

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
// Distributed under the MIT software license, see the accompanying
44
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
55

6-
#include <sync.h>
76
#include <util/system.h>
87

98
#ifdef HAVE_BOOST_PROCESS
109
#include <boost/process.hpp>
1110
#endif // HAVE_BOOST_PROCESS
1211

1312
#include <chainparamsbase.h>
13+
#include <sync.h>
14+
#include <util/check.h>
1415
#include <util/strencodings.h>
1516
#include <util/string.h>
1617
#include <util/translation.h>
@@ -310,8 +311,22 @@ bool ArgsManager::ParseParameters(int argc, const char* const argv[], std::strin
310311
key[0] = '-';
311312
#endif
312313

313-
if (key[0] != '-')
314+
if (key[0] != '-') {
315+
if (!m_accept_any_command && m_command.empty()) {
316+
// The first non-dash arg is a registered command
317+
Optional<unsigned int> flags = GetArgFlags(key);
318+
if (!flags || !(*flags & ArgsManager::COMMAND)) {
319+
error = strprintf("Invalid command '%s'", argv[i]);
320+
return false;
321+
}
322+
}
323+
m_command.push_back(key);
324+
while (++i < argc) {
325+
// The remaining args are command args
326+
m_command.push_back(argv[i]);
327+
}
314328
break;
329+
}
315330

316331
// Transform --foo to -foo
317332
if (key.length() > 1 && key[1] == '-')
@@ -359,6 +374,26 @@ Optional<unsigned int> ArgsManager::GetArgFlags(const std::string& name) const
359374
return nullopt;
360375
}
361376

377+
std::optional<const ArgsManager::Command> ArgsManager::GetCommand() const
378+
{
379+
Command ret;
380+
LOCK(cs_args);
381+
auto it = m_command.begin();
382+
if (it == m_command.end()) {
383+
// No command was passed
384+
return std::nullopt;
385+
}
386+
if (!m_accept_any_command) {
387+
// The registered command
388+
ret.command = *(it++);
389+
}
390+
while (it != m_command.end()) {
391+
// The unregistered command and args (if any)
392+
ret.args.push_back(*(it++));
393+
}
394+
return ret;
395+
}
396+
362397
std::vector<std::string> ArgsManager::GetArgs(const std::string& strArg) const
363398
{
364399
std::vector<std::string> result;
@@ -504,8 +539,22 @@ void ArgsManager::ForceSetArg(const std::string& strArg, const std::string& strV
504539
m_settings.forced_settings[SettingName(strArg)] = strValue;
505540
}
506541

542+
void ArgsManager::AddCommand(const std::string& cmd, const std::string& help, const OptionsCategory& cat)
543+
{
544+
Assert(cmd.find('=') == std::string::npos);
545+
Assert(cmd.at(0) != '-');
546+
547+
LOCK(cs_args);
548+
m_accept_any_command = false; // latch to false
549+
std::map<std::string, Arg>& arg_map = m_available_args[cat];
550+
auto ret = arg_map.emplace(cmd, Arg{"", help, ArgsManager::COMMAND});
551+
Assert(ret.second); // Fail on duplicate commands
552+
}
553+
507554
void ArgsManager::AddArg(const std::string& name, const std::string& help, unsigned int flags, const OptionsCategory& cat)
508555
{
556+
Assert((flags & ArgsManager::COMMAND) == 0); // use AddCommand
557+
509558
// Split arg name from its help param
510559
size_t eq_index = name.find('=');
511560
if (eq_index == std::string::npos) {

src/util/system.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ class ArgsManager
181181
NETWORK_ONLY = 0x200,
182182
// This argument's value is sensitive (such as a password).
183183
SENSITIVE = 0x400,
184+
COMMAND = 0x800,
184185
};
185186

186187
protected:
@@ -193,9 +194,11 @@ class ArgsManager
193194

194195
mutable RecursiveMutex cs_args;
195196
util::Settings m_settings GUARDED_BY(cs_args);
197+
std::vector<std::string> m_command GUARDED_BY(cs_args);
196198
std::string m_network GUARDED_BY(cs_args);
197199
std::set<std::string> m_network_only_args GUARDED_BY(cs_args);
198200
std::map<OptionsCategory, std::map<std::string, Arg>> m_available_args GUARDED_BY(cs_args);
201+
bool m_accept_any_command GUARDED_BY(cs_args){true};
199202
std::list<SectionInfo> m_config_sections GUARDED_BY(cs_args);
200203

201204
[[nodiscard]] bool ReadConfigStream(std::istream& stream, const std::string& filepath, std::string& error, bool ignore_invalid_keys = false);
@@ -246,6 +249,20 @@ class ArgsManager
246249
*/
247250
const std::list<SectionInfo> GetUnrecognizedSections() const;
248251

252+
struct Command {
253+
/** The command (if one has been registered with AddCommand), or empty */
254+
std::string command;
255+
/**
256+
* If command is non-empty: Any args that followed it
257+
* If command is empty: The unregistered command and any args that followed it
258+
*/
259+
std::vector<std::string> args;
260+
};
261+
/**
262+
* Get the command and command args (returns std::nullopt if no command provided)
263+
*/
264+
std::optional<const Command> GetCommand() const;
265+
249266
/**
250267
* Return a vector of strings of the given argument
251268
*
@@ -331,6 +348,11 @@ class ArgsManager
331348
*/
332349
void AddArg(const std::string& name, const std::string& help, unsigned int flags, const OptionsCategory& cat);
333350

351+
/**
352+
* Add subcommand
353+
*/
354+
void AddCommand(const std::string& cmd, const std::string& help, const OptionsCategory& cat);
355+
334356
/**
335357
* Add many hidden arguments
336358
*/

src/wallet/wallettool.cpp

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,8 @@ static void WalletShowInfo(CWallet* wallet_instance)
103103
tfm::format(std::cout, "Address Book: %zu\n", wallet_instance->m_address_book.size());
104104
}
105105

106-
bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command, const std::string& name)
106+
bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command)
107107
{
108-
const fs::path path = fsbridge::AbsPathJoin(GetWalletDir(), name);
109-
110108
if (args.IsArgSet("-format") && command != "createfromdump") {
111109
tfm::format(std::cerr, "The -format option can only be used with the \"createfromdump\" command.\n");
112110
return false;
@@ -119,6 +117,12 @@ bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command,
119117
tfm::format(std::cerr, "The -descriptors option can only be used with the 'create' command.\n");
120118
return false;
121119
}
120+
if (command == "create" && !args.IsArgSet("-wallet")) {
121+
tfm::format(std::cerr, "Wallet name must be provided when creating a new wallet.\n");
122+
return false;
123+
}
124+
const std::string name = args.GetArg("-wallet", "");
125+
const fs::path path = fsbridge::AbsPathJoin(GetWalletDir(), name);
122126

123127
if (command == "create") {
124128
DatabaseOptions options;

src/wallet/wallettool.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
namespace WalletTool {
1111

1212
void WalletShowInfo(CWallet* wallet_instance);
13-
bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command, const std::string& file);
13+
bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command);
1414

1515
} // namespace WalletTool
1616

test/functional/tool_wallet.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,13 @@ def do_tool_createfromdump(self, wallet_name, dumpfile, file_format=None):
183183

184184
def test_invalid_tool_commands_and_args(self):
185185
self.log.info('Testing that various invalid commands raise with specific error messages')
186-
self.assert_raises_tool_error('Invalid command: foo', 'foo')
186+
self.assert_raises_tool_error("Error parsing command line arguments: Invalid command 'foo'", 'foo')
187187
# `bitcoin-wallet help` raises an error. Use `bitcoin-wallet -help`.
188-
self.assert_raises_tool_error('Invalid command: help', 'help')
189-
self.assert_raises_tool_error('Error: two methods provided (info and create). Only one method should be provided.', 'info', 'create')
188+
self.assert_raises_tool_error("Error parsing command line arguments: Invalid command 'help'", 'help')
189+
self.assert_raises_tool_error('Error: Additional arguments provided (create). Methods do not take arguments. Please refer to `-help`.', 'info', 'create')
190190
self.assert_raises_tool_error('Error parsing command line arguments: Invalid parameter -foo', '-foo')
191+
self.assert_raises_tool_error('No method provided. Run `bitcoin-wallet -help` for valid methods.')
192+
self.assert_raises_tool_error('Wallet name must be provided when creating a new wallet.', 'create')
191193
locked_dir = os.path.join(self.options.tmpdir, "node0", "regtest", "wallets")
192194
error = 'Error initializing wallet database environment "{}"!'.format(locked_dir)
193195
if self.options.descriptors:

0 commit comments

Comments
 (0)