argsman, cli: GNU-style command-line option parsing (allows options after non-option arguments) #33540

pull pablomartin4btc wants to merge 7 commits into bitcoin:master from pablomartin4btc:argsman-GNU-style-command-line-option-parsing changing 6 files +308 −75
  1. pablomartin4btc commented at 10:43 pm on October 4, 2025: member

    This PR simplifies the argument parsing logic and resolves practical issues for bitcoin-cli and similar binaries (e.g. bitcoin-wallet) making their usage more consistent and predictable, enabling valid patterns such as mixing commands with wallet- or RPC-specific options—without relying on argument ordering quirks.

    • Previously, ArgsManager stopped interpreting options once a non-option argument was encountered (non-GNU parsing). For example:

        bitcoind -a -b param -c
      

      would ignore -c because it followed the non-option argument param. In some cases ‘-c’ is interpreted as an argument for ‘param’, producing unexpected behavior (eg empty arrays for listtransactions command).

    • This PR changes the behavior so that options following non-option argument are recognized and processed (GNU parsing). This is needed for workflows for tools like CLI where command arguments may be followed by wallet- or RPC-specific options, e.g.:

        bitcoin-cli -rpcwallet="" listtransactions -rpcwallet=mywallet
      
        bitcoin-cli getaddressinfo <address> -rpcwallet=mywallet
      
    • (other binaries could also benefit from this change, e.g. bitcoin-wallet)

        bitcoin-wallet create -wallet=newname
      
    • Invalid options passed after non-options will be treated as RPC arguments that begin with a - character (so no behavioural change against master), e.g.:

        bitcoin-cli -regtest -datadir=/tmp/btc createwallet -mywallet
        {
            "name": "-mywallet"
        }
      
    0./build_master/bin/bitcoin-cli -regtest -datadir=/tmp/btc getaddressinfo bcrt1qrnz5xlcwz5d85n3fag6v0vc4lryjc90jhqtpsh -rpcwallet=nonExistent
    1error message:
    2getaddressinfo "address"
    3...
    
    0./build/bin/bitcoin-cli -regtest -datadir=/tmp/btc getaddressinfo bcrt1qrnz5xlcwz5d85n3fag6v0vc4lryjc90jhqtpsh -rpcwallet=nonExistent
    1error code: -18
    2error message:
    3Requested wallet does not exist or is not loaded
    
    0./build_master/bin/bitcoin-cli -regtest -datadir=/tmp/btc listtransactions -rpcwallet=nonExistent
    1[
    2]
    
    0./build/bin/bitcoin-cli -regtest -datadir=/tmp/btc listtransactions -rpcwallet=nonExistent
    1error code: -18
    2error message:
    3Requested wallet does not exist or is not loaded
    
    0./build_master/bin/bitcoin-cli -regtest -datadir=/tmp/btc listtransactions
    1error code: -19
    2error message:
    3Multiple wallets are loaded. Please select which wallet to use by requesting the RPC through the /wallet/<walletname> URI path. Or for the CLI, specify the "-rpcwallet=<walletname>" option before the command (run "bitcoin-cli -h" for help or "bitcoin-cli listwallets" to see which wallets are currently loaded).
    
    0./build_master/bin/bitcoin-cli -regtest -datadir=/tmp/btc listtransactions -rpcwallet=""
    1error code: -19
    2error message:
    3Multiple wallets are loaded. Please select which wallet to use by requesting the RPC through the /wallet/<walletname> URI path. Or for the CLI, specify the "-rpcwallet=<walletname>" option before the command (run "bitcoin-cli -h" for help or "bitcoin-cli listwallets" to see which wallets are currently loaded).
    
     0/build/bin/bitcoin-cli -regtest -datadir=/tmp/btc listtransactions -rpcwallet=""
     1[
     2  {
     3    "address": "bcrt1q5pqkrlfrp9rvsg43sxxagrees7r7ul6uyskhs4",
     4    "parent_descs": [
     5      "wpkh([76a11ab0/84h/1h/0h]tpubDCDCuyC1Rtm1weZ5kfBYtdchcmqa2t8db7M68wsPHbWhUrYnzFTrHu7ufVVRydt3FdWQy8iNmKbZYbgpWWcEKPAj896x53UbS6i2xRD1qct/0/*)#kuff3m9t"
     6    ],
     7    "category": "immature",
     8    "amount": 50.00000000,
     9    "label": "",
    10...
    
    0./build_master/bin/bitcoin-wallet -regtest -datadir=/tmp/btc create
    1Wallet name must be provided when creating a new wallet.
    

    (-wallet option has to be passed before the create command in order to work)

    0./build_master/bin/bitcoin-wallet -regtest -datadir=/tmp/btc create -wallet=newWalletName
    1Error: Additional arguments provided (-wallet=newWalletName). Methods do not take arguments. Please refer to `-help`.
    
     0./build/bin/bitcoin-wallet -regtest -datadir=/tmp/btc create -wallet=newWallet
     1Topping up keypool...
     2Wallet info
     3===========
     4Name: newWallet
     5Format: sqlite
     6Descriptors: yes
     7Encrypted: no
     8HD (hd seed available): yes
     9Keypool Size: 8000
    10Transactions: 0
    11Address Book: 0
    

    -Notes:

    • Backwards compatibility: This PR is backward compatible in the sense that it allows new valid invocations that were previously rejected (failed or somehow ignored). In master, any valid option following a command is treated as a positional argument. This can lead to unexpected RPC errors or help messages due to mismatched argument types. With this PR, those cases are parsed as proper options when applicable.

    • bitcoin-cli continues to accept RPC arguments that begin with a - character as currently in master (check createwallet example in “What’s change in ArgsManager” section above).

    • bitcoin-cli could distinguish between options that begin with single and double dashes, and treat options after the RPC method name that begin with double dashes as RPC named parameters (check example in 4th commit message body).

    • Refactored both ArgsManager::ProcessOptionKey (new function added in this PR) and ArgsManager::ParseParameters in separated commits making the code clearer and the PR much easier to follow and review.

  2. argsman, refactor: Factor out ProcessOptionKey from ParseParameters
    - Centralizes normalization of option keys in `ProcessOptionKey`.
      This will help in next commit to reuse this function avoiding code
      duplication.
    - Improves readability and makes behavior easier to reason about.
    
    Functional and unit tests confirm no behavior change.
    15039d6c64
  3. DrahtBot commented at 10:43 pm on October 4, 2025: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/33540.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK w0xlt, ryanofsky

    If your review is incorrectly listed, please copy-paste <!–meta-tag:bot-skip–> into the comment that the bot should ignore.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #33192 (refactor: unify container presence checks by l0rinc)

    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.

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    • startig -> starting [typo in log message makes the word unintelligible]

    2025-12-08

  4. katesalazar commented at 9:10 am on October 5, 2025: contributor
    Such good news. Hard to believe this PR doesn’t reference an existing user request.
  5. w0xlt commented at 1:27 pm on October 5, 2025: contributor
    Concept ACK
  6. pinheadmz commented at 2:08 pm on October 5, 2025: member

    Such good news. Hard to believe this PR doesn’t reference an existing user request.

    @katesalazar

    This comment does not contribute to the technical discussion. Please put in more effort in the future or you may be banned.

  7. ryanofsky commented at 1:02 pm on October 6, 2025: contributor

    Haven’t looked at code yet, so forgive if these are dumb questions, but could be nice if PR description addressed these:

    • Is there a way to pass bitcoin-cli RPC arguments that begin with a - character? Can you escape these arguments with -- for example?

    • What’s the PR’s goal for backwards compatibility? Is the PR only allowing command lines that were disallowed before, and therefore fully backwards compatible? Or are there some command lines which used to be accepted which will now be interpreted differently?

    • What are “numeric command arguments” referenced in the PR description? Maybe give an example. I wasn’t aware that numbers were treated differently than other characters for separating options arguments, and I would expect interpretation of numeric values to happen at a later phase than the phase distinguishing options from non-option arguments.

  8. ryanofsky commented at 1:09 pm on October 6, 2025: contributor
    Concept ACK on the idea. I think it probably makes sense to allow options arguments to follow non-option arguments even this change is not strictly backwards compatible, as long as there is some way to escape non-option arguments beginning with -, such as with a -- separator.
  9. ryanofsky commented at 2:39 pm on October 6, 2025: contributor

    Assuming we are ok with losing some backwards compatibility by starting to treating arguments that begin with - as options instead of non-options when they follow arguments that don’t begin with -, there could be other interesting ways to extend this PR.

    For example, bitcoin-cli could distinguish between options that begin with single and double dashes, and treat options after the RPC method name that begin with double dashes as RPC named parameters. In other words, accept:

    0bitcoin-cli createwallet mywallet --load_on_startup=1
    

    as an equivalent to:

    0bitcoin-cli -named createwallet mywallet load_on_startup=1
    

    This seems like it could provide a more convenient way of using named RPC parameters.

  10. pablomartin4btc commented at 5:11 pm on October 12, 2025: member
    * Is there a way to pass `bitcoin-cli` RPC arguments that begin with a `-` character?
    

    At the moment, please correct me if I’m wrong, there’s no or I haven’t seen an RPC that would receive a valid argument that starts with - (but I understand it could be in the future), and there’s no test for it otherwise I think the PR would have caught it.

    Testing the scenario you mentioned in master would work like for example in:

    0./build_master/bin/bitcoin-cli -regtest -datadir=/tmp/btc createwallet -xyz
    1{
    2  "name": "-xyz"
    3}
    

    On this PR would fail because it would be an invalid option (from the ArgsMan):

    0./build/bin/bitcoin-cli -regtest -datadir=/tmp/btc createwallet -xyz
    1Error parsing command line arguments: Invalid parameter -xyz
    

    I think perhaps the scenario you mention is a valid one since any a string argument for an RPC could start with -, so only the “invalid” options (like -xyz in the previous example) would be leaved or passed to the RPC, which makes sense.

    The change would look like this, and we can drop the 2nd commit (bringing back alive ParsePrechecks() and ParseDouble()), which won’t make sense anymore (I explain why I brought them back in your last question on this thread since you’ve asked as well). The change already passes the CIs - CentOS CI failure is unrelated.

    So even the following case would be successful:

    0./build_ryan/bin/bitcoin-cli createwallet -regtest -zyx -datadir=/tmp/btc
    1{
    2  "name": "-zyx"
    3}
    

    Can you escape these arguments with -- for example?

    Not on this PR. I see you think we could extend it with the usage of -- in a later comment. I wanted to make a minimum change to allow this feature of GNU-style parsing options so I’d prefer to add that on a separate PR if that’s necessary.

    * What's the PR's goal for backwards compatibility? Is the PR only allowing command lines that were disallowed before, and therefore fully backwards compatible? Or are there some command lines which used to be accepted which will now be interpreted differently?
    

    The difference now will be that valid options starting with - will be considered or accepted while before or in master these scenarios will be considered that an extra argument is passed to the command and sometimes would be treated as a valid string (when the arg is of such type for the command) returning something empty if it had to match certain condition or would fail because (1) it’s a different type like a JSON error, (2) the RPC had received extra arguments resulting in the command help or (3) too many args for CLI commands. So in terms of backwards compatibility, it would consider valid scenarios that were invalid before, and if someone expected an error, if the option is valid it will be taken into consideration and the execution will be successful instead.

    * What are "numeric command arguments" referenced in the PR description? Maybe give an example. I wasn't aware that numbers were treated differently than other characters for separating options arguments, and I would expect interpretation of numeric values to happen at a later phase than the phase distinguishing options from non-option arguments.
    

    Yeah the interpretation of numeric values come later on the command execution, not during the parsing in the Argsman, but when I made the change, there was a a test that was failing:

    https://github.com/bitcoin/bitcoin/blob/d2987102dd13f965c7a0bf1d5b8ee15bcf025398/test/functional/wallet_transactiontime_rescan.py#L182

    So then I checked that there is another RPC case (getnetworkhashps) which has a default RPCArg::Type::NUM with RPCArg::Default{-1}:

    https://github.com/bitcoin/bitcoin/blob/d2987102dd13f965c7a0bf1d5b8ee15bcf025398/src/rpc/mining.cpp#L120

    That’s the reason of the 2nd commit, reintroducing doubles parsing, but since your first question on this thread, I’m considering this change to be pushed (added a test scenario where commands could receive args starting with -) so that 2nd commit can be dropped as it’s no longer needed (that’s done in the change mentioned in the previous sentence).

  11. argsman, test: Allow options after non-option arguments
    Previously, ArgsManager stopped interpreting options once a
    non-option argument was encountered (non-GNU parsing).
    
    For example:
    
        bitcoind -a -b param -c
    
    would ignore `-c` because it followed the non-option argument
    `param`. In cases where some 'param' command receives parameters,
    '-c' is interpreted as one of them producing unexpected behavior
    (e.g. empty arrays for listtransactions command).
    
    This commit changes the behavior so that options following
    non-option argument are recognized and processed (GNU parsing).
    This is needed for workflows for tools like CLI where command
    arguments may be followed by wallet- or RPC-specific options, e.g.:
    
        bitcoin-cli listtransactions -rpcwallet=mywallet
    
        bitcoin-cli getaddressinfo <address> -rpcwallet=mywallet
    
        bitcoin-wallet create -wallet=newname
    
    Also, invalid options passed after non-options will be treated
    as RPC arguments that begin with a '-' (dash) character (so no
    behavioural change against master), e.g.:
    
        bitcoin-cli -regtest -datadir=/tmp/btc createwallet -mywallet
        {
            "name": "-mywallet"
        }
    02ee492a38
  12. cli, refactor, test: Use parsed command args from ArgsManager
    Refactor bitcoin-cli to use command arguments provided by ArgsManager
    instead of manually slicing argv.
    
    - Introduce GetCommandArgs() helper.
    - Remove local argv/argc skipping logic.
    - Simplify CommandLineRPC argument collection.
    
    This improves consistency in bitcoin-cli, avoiding duplicating parsing
    logic.
    
    Added a new functional test passing an option after a non-option
    argument (GNU-style).
    
    No behavior change intended.
    3a4d83106e
  13. pablomartin4btc force-pushed on Oct 13, 2025
  14. pablomartin4btc force-pushed on Oct 13, 2025
  15. pablomartin4btc force-pushed on Oct 13, 2025
  16. pablomartin4btc commented at 3:37 am on October 13, 2025: member

    -Updates:

    • Addressed @ryanofsky’s feedback:
      • bitcoin-cli accepts RPC arguments that begin with a - character;
      • added a note in description about backwards compatibility;
      • bitcoin-cli treat options after the RPC method name that begin with double dashes as RPC named parameters;
    • Dropped “Reintroduce ParsePrechecks and ParseDouble” commit;
    • Updated description accordingly;
    • CentOS CI failure is unrelated (“no space left on device” -> it seems #33293);
    • Pending to fix Tidy CI failure (“recursive call chain”).
  17. argsman, test: Double dashes as RPC named params
    Treat options that begin wih double dashes as RPC named
    params:
    
    bitcoin-cli createwallet mywallet --load_on_startup=true
    
    as an equivalent to:
    
    bitcoin-cli -named createwallet mywallet load_on_startup=true
    a101bbb61f
  18. pablomartin4btc force-pushed on Oct 13, 2025
  19. pablomartin4btc commented at 5:33 am on October 13, 2025: member

    -Updates:

    • Fixed previous Tidy CI failure (“recursive call chain”), but still working on a refactor of ArgsManager::ProcessOptionKey.
  20. w0xlt commented at 10:30 pm on November 20, 2025: contributor

    This command works on master but not in this PR: ./build/bin/bitcoin-cli --datadir=/tmp/btc1 --signet getblockhash 1000

    Could it be related to GetCommandArgs()?

  21. w0xlt commented at 0:20 am on November 21, 2025: contributor

    Restructuring ProcessOptionKey as below seems to solve the problem of ./build/bin/bitcoin-cli --datadir=/tmp/btc1 --signet getblockhash 1000.

    That seems to fix the Method not found regression while preserving the GNU-style options after the command and the new double-dash-named-params feature.

     0bool ArgsManager::ProcessOptionKey(std::string& key, std::optional<std::string>& val, std::string& error, const bool found_after_non_option) {
     1    bool double_dash{false};
     2    const std::optional<std::string> original_val{val};
     3    std::string original_input{key};
     4    if (val) original_input += "=" + *val;
     5
     6    // Normalize leading dashes
     7    if (key.length() > 1 && key[1] == '-') {
     8        key.erase(0, 1);   // `--foo` -> `-foo`
     9        double_dash = true;
    10    }
    11    key.erase(0, 1);       // `-foo`  -> `foo`
    12
    13    KeyInfo keyinfo = InterpretKey(key);
    14    std::optional<unsigned int> flags = GetArgFlags('-' + keyinfo.name);
    15    const bool known_option = flags.has_value() && keyinfo.section.empty();
    16
    17    // Handle the special "named RPC" case early ---
    18    if (double_dash && found_after_non_option && !known_option) {
    19        // Try to map this to `-named`, if supported by the binary.
    20        val.reset();
    21        KeyInfo named_keyinfo = InterpretKey("named");
    22        std::optional<unsigned int> named_flags = GetArgFlags('-' + named_keyinfo.name);
    23
    24        if (named_flags && named_keyinfo.section.empty()) {
    25            // Binary supports -named: enable it and append the stripped parameter.
    26            std::optional<common::SettingsValue> named_value =
    27                InterpretValue(named_keyinfo, /* val */ nullptr, *named_flags, error);
    28            if (!named_value) return false;
    29
    30            {
    31                LOCK(cs_args);
    32                m_settings.command_line_options[named_keyinfo.name].push_back(*named_value);
    33                // Strip the leading "--" before passing to the command as a named param
    34                m_command.emplace_back(original_input.substr(2));
    35            }
    36            return true;
    37        }
    38
    39        // Binary doesn’t support -named: fall back and treat `--foo` as a
    40        // normal option (or unknown option handled below).
    41        keyinfo = InterpretKey(key);
    42        flags = GetArgFlags('-' + keyinfo.name);
    43    }
    44
    45    // --- Normal option handling ---
    46    if (!flags || !keyinfo.section.empty()) {
    47        if (!found_after_non_option) {
    48            // Unknown global option: keep legacy behaviour (error)
    49            error = strprintf("Invalid parameter %s", original_input);
    50            return false;
    51        }
    52
    53        // Unknown option after command: pass through to the command as an arg
    54        LOCK(cs_args);
    55        m_command.emplace_back(original_input);
    56        return true;
    57    }
    58
    59    // Known option (including `--datadir`, `--signet`, etc.)
    60    std::optional<common::SettingsValue> value =
    61        InterpretValue(keyinfo, val ? &*val : nullptr, *flags, error);
    62    if (!value) return false;
    63
    64    LOCK(cs_args);
    65    m_settings.command_line_options[keyinfo.name].push_back(*value);
    66    return true;
    67}
    
  22. pablomartin4btc commented at 2:20 pm on December 4, 2025: member

    This command works on master but not in this PR: ./build/bin/bitcoin-cli --datadir=/tmp/btc1 --signet getblockhash 1000 Thanks for testing it (not sure if that’s valid, no test failed, perhaps we need to add one?), anyways just putting the output’s diff here.

    master:

    0/build_master/bin/bitcoin-cli --datadir=/tmp/btc --signet getblockhash 1000
    1error code: -8
    2error message:
    3Block height out of range
    

    this branch:

    0./build/bin/bitcoin-cli --datadir=/tmp/btc --signet getblockhash 1000
    1error: timeout on transient error: Could not connect to the server 127.0.0.1:8332
    2
    3Make sure the bitcoind server is running and that you are connecting to the correct RPC port.
    4Use "bitcoin-cli -help" for more info.
    
  23. pablomartin4btc commented at 2:24 pm on December 4, 2025: member

    Restructuring ProcessOptionKey as below seems to solve the problem…

    Checking it… Thanks for the suggestion!

  24. pablomartin4btc commented at 3:16 pm on December 4, 2025: member

    Restructuring ProcessOptionKey as below seems to solve the problem…

    Checking it… Thanks for the suggestion!

    Ok, all tests pass…

    First checked only the affected ones by my changes:

    0./build/test/functional/feature_config_args.py
    1./build/test/functional/tool_wallet.py
    2./build/test/functional/wallet_multiwallet.py
    3./build/test/functional/wallet_transactiontime_rescan.py
    4
    5./build/bin/test_bitcoin --log_level=all --run_test=argsman_tests
    

    Then I ran them all including the functional --extended.

    Thanks again for working on this! I’m seeing if I can make the code a bit clearer otherwise I’ll include your suggestion.

  25. pablomartin4btc force-pushed on Dec 8, 2025
  26. DrahtBot added the label CI failed on Dec 8, 2025
  27. DrahtBot commented at 3:02 pm on December 8, 2025: contributor

    🚧 At least one of the CI tasks failed. Task lint: https://github.com/bitcoin/bitcoin/actions/runs/20032344935/job/57444563462 LLM reason (✨ experimental): Python linting failed: ruff detected an f-string without placeholders in test code, causing the lint step to fail and CI to exit with error.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  28. pablomartin4btc force-pushed on Dec 8, 2025
  29. pablomartin4btc force-pushed on Dec 8, 2025
  30. pablomartin4btc force-pushed on Dec 8, 2025
  31. cli, test: Fix parsing of long (--) options
    Improve handling of long (`--`) options passed to bitcoin-cli, such as
    `--datadir` and `--signet`. Previously, some code paths in CLI didn't
    properly recognize or forward double-dash options, causing unexpected
    argument parsing behavior or timeouts.
    
    This change ensures that long options are processed consistently,
    matching ArgsManager behavior and CLI semantics.
    
    Example fixed case:
        /build/bin/bitcoin-cli --datadir=/tmp/btc1 --signet getblockhash 1000
    
    Co-authored-by: w0xlt <w0xlt@users.noreply.github.com>
    13bf4f8865
  32. argsman, refactor: Make ProcessOptionKey clearer
    No behavior changed.
    8d941b1918
  33. argsman, refactor: Make ParseParameters clearer
    No behavior changed.
    4f96cedd03
  34. pablomartin4btc force-pushed on Dec 8, 2025
  35. pablomartin4btc commented at 4:57 pm on December 8, 2025: member

    -Updates:

    • Added @w0xlt’s fix on long (double dash –) options that worked on master, adding a new test for it (the test would pass in master but fail without @w0xlt’s changes).
    • Refactored both ArgsManager::ProcessOptionKey (new function added in this PR) and ArgsManager::ParseParameters in separated commits making the code clearer and the PR much easier to follow and review.
  36. DrahtBot removed the label CI failed on Dec 8, 2025

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2025-12-13 15:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me