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
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
Reviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.
532@@ -518,6 +533,11 @@ void ArgsManager::AddArg(const std::string& name, const std::string& help, unsig
533     auto ret = arg_map.emplace(arg_name, Arg{name.substr(eq_index, name.size() - eq_index), help, flags});
534     assert(ret.second); // Make sure an insertion actually happened
535 
536+    if (flags & ArgsManager::COMMAND) {
537+        Assert(flags == ArgsManager::COMMAND); // Combination not allowed
538+        Assert(name.find('=') == std::string::npos);
539+        Assert(name.find('-') == std::string::npos);
319+            if (!flags || !(*flags & ArgsManager::COMMAND)) {
320+                error = strprintf("Invalid command '%s'", argv[i]);
321+                return false;
322+            }
323+            m_commands.push_back(key);
324+            continue;
I’m not sure multiple (sub)commands makes sense for us. At present, I think we have:
For bitcoin-util, I think we want it to be more like bitcoin-wallet, and if we were to do a psbt version of bitcoin-tx’s multiple commands in order to construct a psbt, I think that would look like bitcoin-util psbt <subsubcmd> <subsubcmd> ...
So I don’t think it makes much sense to support multiple subcommands, particularly without letting them have their own params. Which I think means you could have a nicer implementation with something like https://github.com/ajtowns/bitcoin/commits/202101-getcommand which lets you just say AddCmd(...) to start introducing new commands, don’t have to explicitly set accept_any_command when parsing, and can just invoke GetCommand() and GetArguments() to get the pre-parsed results. Thoughts?
I’ve added a second commit on that branch that allows the ArgsManager options to come after the command, which is the same as this PR, but differs from current master (which leaves later options up to the command – so bitcoin-cli treats them as rpc args, while bitcoin-wallet currently just silently ignores them).
40-    argsman.AddArg("createfromdump", "Create new wallet file from dumped records", ArgsManager::ALLOW_ANY, OptionsCategory::COMMANDS);
41+    argsman.AddArg("info", "Get wallet info", ArgsManager::COMMAND, OptionsCategory::COMMANDS);
42+    argsman.AddArg("create", "Create new wallet file", ArgsManager::COMMAND, OptionsCategory::COMMANDS);
43+    argsman.AddArg("salvage", "Attempt to recover private keys from a corrupt wallet. Warning: 'salvage' is experimental.", ArgsManager::COMMAND, OptionsCategory::COMMANDS);
44+    argsman.AddArg("dump", "Print out all of the wallet key-value records", ArgsManager::COMMAND, OptionsCategory::COMMANDS);
45+    argsman.AddArg("createfromdump", "Create new wallet file from dumped records", ArgsManager::COMMAND, OptionsCategory::COMMANDS);
I think this would be a lot better if we could link options to commands (so -format could be tied to createfromdump directly rather than having it hidden in the help text and having a special cased if statement). Some options would need to be available for multiple (but not all) commands (-dumpfile for dump and createfromdump eg).
This seems like a bit more of an intrusive change to make, and probably doesn’t have to be done immediately though.
This seems like a bit more of an intrusive change to make, and probably doesn’t have to be done immediately though.
Indeed. Will leave this for a follow-up. This should be a minimal bugfix/feature commit.
373@@ -359,6 +374,19 @@ Optional<unsigned int> ArgsManager::GetArgFlags(const std::string& name) const
374     return nullopt;
375 }
376 
377+bool ArgsManager::GetCommand(std::string& cmd, std::vector<std::string>& args) const
AddCommand and GetCommand for consistency?
              
            373@@ -359,6 +374,19 @@ Optional<unsigned int> ArgsManager::GetArgFlags(const std::string& name) const
374     return nullopt;
375 }
376 
377+bool ArgsManager::GetCommand(std::string& cmd, std::vector<std::string>& args) const
378+{
379+    LOCK(cs_args);
380+    assert(!m_accept_any_command); // AddCmd must have been called before this
m_command and have the only difference be that you get an “unknown command” when m_accept_any_command is set
              
            107-    }
108-
109-    if (method.empty()) {
110+    std::string method;
111+    std::vector<std::string> cmdargs;
112+    if (!args.GetCommand(method, cmdargs)) {
struct CmdArg { string cmd; vector<string> args; }; to track them both.
              
            ACK fad369f04361e2e3a834589b63fab160b9b4e1b2
Looks good; comments are just ideas for consideration. Patch for bitcoin-util at https://github.com/ajtowns/bitcoin/commits/202101-util-addcmd
112+    if (!args.GetCommand(method, cmdargs)) {
113         tfm::format(std::cerr, "No method provided. Run `bitcoin-wallet -help` for valid methods.\n");
114         return EXIT_FAILURE;
115     }
116+    if (cmdargs.size() != 0) {
117+        tfm::format(std::cerr, "Error: Additional arguments provided (%s). Please refer to `-help`. Commands do not take arguments.\n", Join(cmdargs, ", "));
nit: I would slightly prefer it this way around:
0tfm::format(std::cerr, "Error: Additional arguments provided (%s). Commands do not take arguments. Please refer to `-help`. \n", Join(cmdargs, ", "));
105-            method = argv[i];
106-        }
107-    }
108-
109-    if (method.empty()) {
110+    std::string method;
command?
              
            108-
109-    if (method.empty()) {
110+    std::string method;
111+    std::vector<std::string> cmdargs;
112+    if (!args.GetCommand(method, cmdargs)) {
113         tfm::format(std::cerr, "No method provided. Run `bitcoin-wallet -help` for valid methods.\n");
Code review ACK fad369f04361e2e3a834589b63fab160b9b4e1b2
This works, however I liked @ajtowns comments and I think consistency whether method or command is used would be good. It seems method is getting replaced by command, is that a general naming scheme change?
Co-Authored-by: Anthony Towns <aj@erisian.com.au>
187@@ -188,6 +188,8 @@ def test_invalid_tool_commands_and_args(self):
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')
190         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.')
in fa06bce4ac17f93decd4ee38c956e7aa55983f0d:
nit: Commit message could have been a bit more descriptive ;)
250@@ -248,6 +251,20 @@ class ArgsManager
251      */
252     const std::list<SectionInfo> GetUnrecognizedSections() const;
253 
254+    struct Command {
in fa61b9d1a68820758f9540653920deaeae6abe79:
nit: seeing this now I think I would have named it CommandCall or something similar to express that it’s command + args. But feel free to ignore my name bikeshedding.
CmdArg. So I could call it CommandArg or CommandArgs (because there might be multiple args to the command). Not sure what to do here, but I think I’ll leave this as is for now and let the naming committee determine a suitable name and let them change it after merge.
              
            Code review ACK fa61b9d1a68820758f9540653920deaeae6abe79
Feel free to ignore my nits.
ACK fa61b9d1a68820758f9540653920deaeae6abe79
Looks good to me, though the third commit has a typo in the description: “dependend”. Updated https://github.com/ajtowns/bitcoin/commits/202101-util-addcmd
third commit has a typo in the description: “dependend”.
Good catch. Though, I’ll leave this as-is to minimize the re-review backlog.