argsman, cli: Allow options after non-option arguments (GNU-style) #35831

pull pablomartin4btc wants to merge 3 commits into bitcoin:master from pablomartin4btc:argsman/gnu-parsing-options-after-commands changing 7 files +285 −38
  1. pablomartin4btc commented at 4:18 PM on July 28, 2026: member

    Allow options to be specified after non-option arguments in ParseParameters, matching GNU option parsing conventions (getopt_long style).
    Currently in master, once the first non-dash argument was encountered, all subsequent arguments were collected verbatim into m_command. This meant -rpcwallet and similar options were silently ignored when placed after a command or its positional args. The same misspelled option before the command would be a clear error; after the command, it was silently treated as a positional argument — potentially triggering unintended RPC behaviour. With this change:

    • Valid options after a command are stored in command_line_options exactly as if they appeared before the command.
    • Unrecognized options after a command are always errors (no silent fallback to positional).
    • A -- separator stops option processing; all subsequent arguments are treated as positional regardless of leading dashes.
    • bitcoin-cli's CommandLineRPC is updated to derive its args vector from gArgs.GetCommand() rather than re-scanning raw argv.
    • bitcoin-wallet benefits automatically: options such as -wallet placed after a command are now parsed correctly instead of being rejected as unexpected positional arguments.

    <details> <summary><i><ins>Before and after:</ins></i> options after a command are now recognized...</summary>

    <br>

    -rpcwallet after the command and its positional args:

    • Before (master) — -rpcwallet is silently ignored; request goes to the wrong wallet:

      $ bitcoin-cli -regtest -datadir=/tmp/btc listtransactions -rpcwallet=nonExistent                                                                           
      [                                                                                                                                                          
      ]                                                                                                                                                          
      
    • After — correctly routes to the named wallet and fails with the expected error:

      $ bitcoin-cli -regtest -datadir=/tmp/btc listtransactions -rpcwallet=nonExistent                                                                           
      error code: -18                                                                                                                                            
      error message:                                                                                                                                             
      Requested wallet does not exist or is not loaded                                                                                                           
      

    getaddressinfo with -rpcwallet appended:

    • Before (master) — -rpcwallet becomes an extra positional arg; getaddressinfo errors with its usage string:

      $ bitcoin-cli -regtest -datadir=/tmp/btc getaddressinfo bcrt1q... -rpcwallet=nonExistent                                                                   
      error message:                                                                                                                                             
      getaddressinfo "address"                                                                                                                                   
      ...                                                                                                                                                        
      
    • After — wallet is correctly selected:

      $ bitcoin-cli -regtest -datadir=/tmp/btc getaddressinfo bcrt1q... -rpcwallet=nonExistent                                                                   
      error code: -18                                                                                                                                            
      error message:                                                                                                                                             
      Requested wallet does not exist or is not loaded                                                                                                           
      

                                                                                                                                                               

    Multi-wallet: -rpcwallet no longer needs to come before the command:

    • Before (master):

      $ bitcoin-cli -regtest -datadir=/tmp/btc listtransactions                                                                                                  
      error code: -19                                                                                                                                            
      error message:                                                                                                                                             
      ...specify the "-rpcwallet=<walletname>" option before the command...                                                                                      
      
      $ bitcoin-cli -regtest -datadir=/tmp/btc listtransactions -rpcwallet=mywallet                                                                              
      error code: -19   ← still fails; option after command was ignored                                                                                          
      error message:                                                                                                                                             
      ...specify the "-rpcwallet=<walletname>" option before the command...                                                                                      
      
    • After:

      — error message updated:

      $ bitcoin-cli -regtest -datadir=/tmp/btc listtransactions
      error code: -19
      error message:  
      ...specify the "-rpcwallet=<walletname>" option before or after the command...
      

      — command executed successfully:

      $ bitcoin-cli -regtest -datadir=/tmp/btc listtransactions -rpcwallet=mywallet                                                                              
      [                                                                                                                                                          
        { "address": "bcrt1q...", "category": "immature", "amount": 50.00000000, ... }                                                                                 
      ]                                                                                                                                                          
      

    </details>

    <details> <summary><i><ins>Before and after:</ins></i> misspelled options and the <code>--</code> separator...</summary>

                                                                                                                                                               

    <br>

    Misspelled option is now an error rather than a silent positional arg:

    Before (master) — -rpcwalllet (note triple-l) silently absorbed as the label positional arg:

    ```                                                                                                                                                        
    $ bitcoin-cli -regtest -datadir=/tmp/btc listtransactions <hash> -rpcwalllet=foo                                                                                   
    { ... }   ← result returned, -rpcwallet went unnoticed                                                                                                     
    ```                                                                                                                                                        
                                                                                                                                                               

    After:

    ```                                                                                                                                                        
    $ bitcoin-cli -regtest -datadir=/tmp/btc getblock <hash> -rpcwalllet=foo                                                                                   
    error: Invalid parameter -rpcwalllet                                                                                                                       
    ```                                                                                                                                                        
                                                                                                                                                               

    -- separator: pass a dash-prefixed string as a positional RPC argument:
    $ bitcoin-cli -regtest -datadir=/tmp/btc somecommand -- -not-an-option

    Everything after `--` is treated as a positional argument, so `-not-an-option` is passed to the RPC unchanged.

    </details>

    <details> <summary><i><ins>Before and after:</ins></i> <code>bitcoin-wallet</code> commands...</summary>

    <br>

    -wallet option placed after the command:

    Before (master) — option treated as an unexpected positional arg:

        $ bitcoin-wallet -regtest -datadir=/tmp/btc create -wallet=myWallet
        Error: Additional arguments provided (-wallet=myWallet). Methods do not take arguments. Please refer to -help.
    

    After:

        $ bitcoin-wallet -regtest -datadir=/tmp/btc create -wallet=myWallet
        Topping up keypool...
        Wallet info
    
        Name: myWallet
        Format: sqlite
        Descriptors: yes
        Encrypted: no
        HD (hd seed available): yes
        Keypool Size: 8000
        Transactions: 0
        Address Book: 0
    

    </details>


    Backwards compatibility: command lines where a dash-prefixed argument follows a non-dash argument will now be parsed as an option (if recognized) or an error (if not). Previously such arguments were silently collected as positional args. The -- separator can be used to pass arguments that begin with a dash as positional args.

  2. DrahtBot commented at 4:19 PM on July 28, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

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

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    ACK w0xlt

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    No conflicts as of last run.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

    LLM Linter (✨ experimental)

    Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

    • self.nodes[0].cli('-generate', n5, 1000000, rpcwallet2).send_cli() in test/functional/interface_bitcoin_cli.py

    <sup>2026-08-13 04:47:45</sup>

  3. pablomartin4btc force-pushed on Jul 28, 2026
  4. DrahtBot added the label CI failed on Jul 28, 2026
  5. DrahtBot commented at 4:51 PM on July 28, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task test ancestor commits: https://github.com/bitcoin/bitcoin/actions/runs/30377658579/job/90337348515</sub> <sub>LLM reason (✨ experimental): CI failed because the argsman_tests CTest suite failed (non-zero exit status 8 from “Errors while running CTest”).</sub>

    <details><summary>Hints</summary>

    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.

    </details>

  6. pablomartin4btc force-pushed on Jul 28, 2026
  7. pablomartin4btc force-pushed on Jul 28, 2026
  8. pablomartin4btc force-pushed on Jul 28, 2026
  9. pablomartin4btc force-pushed on Jul 29, 2026
  10. DrahtBot removed the label CI failed on Jul 29, 2026
  11. pablomartin4btc force-pushed on Jul 29, 2026
  12. pablomartin4btc commented at 4:18 AM on July 29, 2026: member

    -<ins>Updates</ins>:

    • Force-pushed to fix CI failures found after the initial push:
      • Windows (rpc_psbt.py) — positional arguments after a command (e.g. base64 PSBTs) were being lowercased, corrupting their values.
      • Windows (wallet_multiwallet.py) — paths starting with / after a command were being converted to - and parsed as options; fixed by only applying the Windows /- conversion when there is no embedded slash (distinguishing /rpcwallet=foo from /bad/./path).
      • Alpine/musl (feature_pruning.py) — negative integers like -10 after a command were incorrectly treated as options instead of positional arguments.
    • Also added a #ifdef WIN32 unit test covering Windows option syntax (/OPT=val, -OPTION) after a command, addressing a case raised in #33540.
  13. w0xlt commented at 8:59 AM on July 29, 2026: contributor

    Concept ACK.

  14. pablomartin4btc force-pushed on Jul 29, 2026
  15. pablomartin4btc force-pushed on Jul 29, 2026
  16. pablomartin4btc commented at 6:01 PM on July 29, 2026: member

    -<ins>Updates</ins>:

    • Simplified the is_number check: removed the opt_start variable, added a cmd_key[1] != '-' guard so --10 is now an error (consistent with pre-command behaviour). Intent is clearer: single dash + all digits => negative integer => positional arg.
    • Added a missing SUCCESS test in util_AddCommand for a valid option specified after its command ({"x", "cmd2", "-opt1=foo"}), complementing the existing error case.
    • Improved release notes: added bitcoin-wallet benefit, backwards compatibility note, reordered for clarity.
    • Updated PR description: the multi-wallet "After" example now reflects that the WALLET_NOT_SPECIFIED error message reads "before or after the command".
  17. DrahtBot added the label Needs rebase on Aug 11, 2026
  18. in src/bitcoin-cli.cpp:223 in b2ac15d3c8 outdated
     219 | @@ -220,6 +220,7 @@ static int AppInitRPC(int argc, char* argv[])
     220 |                  "\nIt can be used to query network information, manage wallets, create or broadcast transactions, and control the " CLIENT_NAME " server.\n"
     221 |                  "\nUse the \"help\" command to list all commands. Use \"help <command>\" to show help for that command.\n"
     222 |                  "The -named option allows you to specify parameters using the key=value format, eliminating the need to pass unused positional parameters.\n"
     223 | +                "[options] can be specified before or after <command>.\n"
    


    vicjuma commented at 10:53 PM on August 11, 2026:

    This change produces a unified output, especially with the -named arg. Before the change, different RPC commands could result in different error messages:

    Before this change

    Getting a new address

    ratedg@0xratedg:~/projects/contributions/bitcoin/build/bin$ ./bitcoin-cli getnewaddress address_type=bech32m -named
    error code: -5
    error message:
    Unknown address type '-named'
    

    Listing transactions

    ratedg@0xratedg:~/projects/contributions/bitcoin/build/bin$ ./bitcoin-cli listtransactions count=10 -named
    error: Error parsing JSON: -named
    ratedg@0xratedg:~/projects/contributions/bitcoin/build/bin$ 
    

    After this change

    Getting a new address

    ratedg@0xratedg:~/projects/contributions/bitcoin/build2/bin$ ./bitcoin-cli getnewaddress address_type=bech32m -named
    bcrt1p9x7uz943tc2h9ugcqkh37y066253k8lc6zmvwafhw4tpp9cgruaqdz4whx
    ratedg@0xratedg:~/projects/contributions/bitcoin/build2/bin$ 
    

    Listing transactions

    ratedg@0xratedg:~/projects/contributions/bitcoin/build2/bin$ ./bitcoin-cli listtransactions count=1 -named
    [
      {
        "address": "bcrt1prpkf2ezqw9aex635w7uja9f4mzy58660ls3xgynequ0ytvg6qy9qllxzgd",
        "category": "send",
        "amount": -5.00000000,
        "label": "",
        "vout": 1,
        "fee": -0.00001550,
        "confirmations": 0,
        "trusted": true,
        "txid": "a12778bc29388a83a128de9d9becda897fca096da075451e5fbb0b3592b1c7fe",
        "wtxid": "8f3b585d8c65e83f0a662e338793aa07bb2418ecda66cc38c82cb24b26ad531b",
        "walletconflicts": [
        ],
        "mempoolconflicts": [
        ],
        "time": 1786485008,
        "timereceived": 1786485008,
        "abandoned": false
      }
    ]
    ratedg@0xratedg:~/projects/contributions/bitcoin/build/bin$ 
    

    I guess it may be necessary. I am a bit skeptical though cause probably it will have to be replicated across all the other targets too as opposed to just bitcoin-cli. This will mean it should be possible to use this in bitcoin-tx successfully, etc

    ratedg@0xratedg:~/projects/contributions/bitcoin/build2/bin$ ./bitcoin-tx -regtest \
         in=7dc357e7ba70b0b503c9a12f4a293add0b1d230e08de3cad83e142737746c4b8:1 \
         outaddr=0.9:mmhnpJgEDuPxTmw6Nuu5Ms2xmP21n6XoCi -create
    error: unknown command
    ratedg@0xratedg:~/projects/contributions/bitcoin/build2/bin$ 
    

    The help utility is already detailed for users who may be confused about the ordering

    Usage: bitcoin-cli [options] <command> [params]
    or:    bitcoin-cli [options] -named <command> [name=value]...
    or:    bitcoin-cli [options] help
    or:    bitcoin-cli [options] help <command>
    

    I will wait for other reviews on this to get a clear picture.


    pablomartin4btc commented at 3:40 PM on August 12, 2026:

    Thanks for your review @vicjuma.

    This will mean it should be possible to use this in bitcoin-tx successfully, etc

    bitcoin-wallet already benefits from this PR as it uses GetCommand() mainly, which would need to be added into bitcoin-tx, feature I've added in previous #33540 at e47c722324dd33337eff4c0a24bb051daebe5f27 and will be added in a follow-up to this PR as other features from the same previous PR. The intention of this PR is to be simpler and more focused to the GNU-style parsing.

    The help utility is already detailed for users who may be confused about the ordering

    Perhaps you should include the previous line that was the one added:

    [options] can be specified before or after <command>.
    
  19. vicjuma commented at 10:57 PM on August 11, 2026: contributor

    :-)

  20. pablomartin4btc force-pushed on Aug 12, 2026
  21. w0xlt commented at 11:53 PM on August 12, 2026: contributor

    -- is only recognized after the command, so using it before the first positional argument (for example, bitcoin-wallet -help -- create) incorrectly fails with Invalid parameter --.

    In GNU-style parsing, -- should end option processing wherever it appears.

    <details> <summary>Suggested fix</summary>

    diff --git a/src/common/args.cpp b/src/common/args.cpp
    index bdc01ce61c..be42134123 100644
    --- a/src/common/args.cpp
    +++ b/src/common/args.cpp
    @@ -188,20 +188,28 @@ bool ArgsManager::ParseParameters(int argc, const char* const argv[], std::strin
             if (key.starts_with("-psn_")) continue;
     #endif
     
    -        if (key == "-") break; //bitcoin-tx using stdin
    +        bool options_ended{false};
    +        if (key == "--") {
    +            options_ended = true;
    +            if (++i >= argc) break;
    +            key = argv[i];
    +        }
    +        if (!options_ended && key == "-") break; //bitcoin-tx using stdin
             std::optional<std::string> val;
    -        size_t is_index = key.find('=');
    -        if (is_index != std::string::npos) {
    -            val = key.substr(is_index + 1);
    -            key.erase(is_index);
    +        if (!options_ended) {
    +            size_t is_index = key.find('=');
    +            if (is_index != std::string::npos) {
    +                val = key.substr(is_index + 1);
    +                key.erase(is_index);
    +            }
             }
     #ifdef WIN32
             key = ToLower(key);
    -        if (key[0] == '/')
    +        if (!options_ended && key[0] == '/')
                 key[0] = '-';
     #endif
     
    -        if (key[0] != '-') {
    +        if (options_ended || key[0] != '-') {
                 if (!m_accept_any_command && m_command.empty()) {
                     // The first non-dash arg is a registered command
                     std::optional<unsigned int> flags = GetArgFlags_(key);
    @@ -211,6 +219,10 @@ bool ArgsManager::ParseParameters(int argc, const char* const argv[], std::strin
                     }
                 }
                 m_command.push_back(key);
    +            if (options_ended) {
    +                while (++i < argc) m_command.emplace_back(argv[i]);
    +                break;
    +            }
                 while (++i < argc) {
                     std::string cmd_arg(argv[i]);
                     // "--" ends option processing; all subsequent args are positional
    diff --git a/src/test/argsman_tests.cpp b/src/test/argsman_tests.cpp
    index c8fad91c0c..4b9834a956 100644
    --- a/src/test/argsman_tests.cpp
    +++ b/src/test/argsman_tests.cpp
    @@ -750,6 +750,39 @@ BOOST_AUTO_TEST_CASE(util_ParseParameters_gnu_style)
             BOOST_CHECK_EQUAL(cmd->args[1], "arg2");
         }
     
    +    // "--" also ends option processing before the command.
    +    {
    +        TestArgsManager test;
    +        test.SetupArgs({{"-opt", ArgsManager::ALLOW_ANY}});
    +        test.AddCommand("cmd", "test command");
    +        std::string error;
    +        const char* argv[] = {"x", "--", "cmd", "-not-an-option", "arg2"};
    +        BOOST_CHECK(test.ParseParameters(5, argv, error));
    +        BOOST_CHECK(!test.IsArgSet("-opt"));
    +        const auto cmd = test.GetCommand();
    +        BOOST_REQUIRE(cmd);
    +        BOOST_CHECK_EQUAL(cmd->command, "cmd");
    +        BOOST_REQUIRE_EQUAL(cmd->args.size(), 2U);
    +        BOOST_CHECK_EQUAL(cmd->args[0], "-not-an-option");
    +        BOOST_CHECK_EQUAL(cmd->args[1], "arg2");
    +    }
    +
    +    // Options before "--" are parsed, and the first positional argument after
    +    // it may begin with a dash.
    +    {
    +        TestArgsManager test;
    +        test.SetupArgs({{"-opt", ArgsManager::ALLOW_ANY}});
    +        std::string error;
    +        const char* argv[] = {"x", "-opt", "--", "-1"};
    +        BOOST_CHECK(test.ParseParameters(4, argv, error));
    +        BOOST_CHECK(test.IsArgSet("-opt"));
    +        const auto cmd = test.GetCommand();
    +        BOOST_REQUIRE(cmd);
    +        BOOST_CHECK(cmd->command.empty());
    +        BOOST_REQUIRE_EQUAL(cmd->args.size(), 1U);
    +        BOOST_CHECK_EQUAL(cmd->args[0], "-1");
    +    }
    +
         // Options and positional args can be freely mixed after a command.
         {
             TestArgsManager test;
    

    </details>

  22. w0xlt commented at 12:06 AM on August 13, 2026: contributor

    In the current PR code, on Windows, a post-command option such as /datadir=C:/bitcoin is mistaken for a positional argument because its value contains /.

    <details> <summary>Suggested fix</summary>

    diff --git a/src/common/args.cpp b/src/common/args.cpp
    index bdc01ce61c..98c254cbaa 100644
    --- a/src/common/args.cpp
    +++ b/src/common/args.cpp
    @@ -224,10 +224,12 @@ bool ArgsManager::ParseParameters(int argc, const char* const argv[], std::strin
     #ifdef WIN32
                     cmd_key = ToLower(cmd_key);
                     // Unlike pre-command args (options only), post-command args can be
    -                // file paths. Only treat /foo as -foo when there is no embedded
    -                // slash, which distinguishes Windows options (/rpcwallet=foo) from
    -                // paths (/bad/./path). Bare '/' is excluded by the size > 1 check.
    -                if (cmd_key.size() > 1 && cmd_key[0] == '/' && cmd_key.find('/', 1) == std::string::npos)
    +                // file paths. Only check the option name for embedded slashes, as
    +                // the option value itself may be a path. This distinguishes Windows
    +                // options (/rpcwallet=dir/wallet) from paths (/bad/./path). Bare '/'
    +                // is excluded by the size > 1 check.
    +                const auto option_name{cmd_key.substr(0, cmd_key.find('='))};
    +                if (option_name.size() > 1 && option_name[0] == '/' && option_name.find('/', 1) == std::string::npos)
                         cmd_key[0] = '-';
     #endif
                     // Negative integers (e.g. -10, -3) are positional args, not options.
    diff --git a/src/test/argsman_tests.cpp b/src/test/argsman_tests.cpp
    index c8fad91c0c..4744d20030 100644
    --- a/src/test/argsman_tests.cpp
    +++ b/src/test/argsman_tests.cpp
    @@ -782,18 +782,20 @@ BOOST_AUTO_TEST_CASE(util_ParseParameters_gnu_style)
         }
     
     #ifdef WIN32
    -    // On Windows, /option and -OPTION are equivalent to -option after a command.
    +    // On Windows, slash options may have path values, while slash-prefixed paths
    +    // remain positional arguments.
         {
             TestArgsManager test;
             test.SetupArgs({{"-opt", ArgsManager::ALLOW_ANY}});
             test.AddCommand("cmd", "test command");
             std::string error;
    -        const char* argv[] = {"x", "cmd", "/OPT=val"};
    -        BOOST_CHECK(test.ParseParameters(3, argv, error));
    -        BOOST_CHECK_EQUAL(test.GetArg("-opt", ""), "val");
    +        const char* argv[] = {"x", "cmd", "/OPT=C:/bitcoin", "/bad/./path"};
    +        BOOST_CHECK(test.ParseParameters(4, argv, error));
    +        BOOST_CHECK_EQUAL(test.GetArg("-opt", ""), "C:/bitcoin");
             const auto cmd = test.GetCommand();
             BOOST_REQUIRE(cmd);
    -        BOOST_CHECK(cmd->args.empty());
    +        BOOST_REQUIRE_EQUAL(cmd->args.size(), 1U);
    +        BOOST_CHECK_EQUAL(cmd->args[0], "/bad/./path");
         }
     #endif
     }
    

    </details>

  23. w0xlt commented at 12:14 AM on August 13, 2026: contributor

    A lone - after a command, such as in bitcoin-cli echo -, is incorrectly treated as an option and rejected with Invalid parameter -.

    A single hyphen is a positional argument; an option must contain at least one character after it.

    <details> <summary>Suggested fix</summary>

    diff --git a/src/common/args.cpp b/src/common/args.cpp
    index bdc01ce61c..b583938a1a 100644
    --- a/src/common/args.cpp
    +++ b/src/common/args.cpp
    @@ -233,7 +233,7 @@ bool ArgsManager::ParseParameters(int argc, const char* const argv[], std::strin
                     // Negative integers (e.g. -10, -3) are positional args, not options.
                     bool is_number = cmd_key.size() > 1 && cmd_key[1] != '-' &&
                                      std::all_of(cmd_key.begin() + 1, cmd_key.end(), IsDigit);
    -                bool is_option = !cmd_key.empty() && cmd_key[0] == '-' && !is_number;
    +                bool is_option = cmd_key.size() > 1 && cmd_key[0] == '-' && !is_number;
                     if (is_option) {
                         // Options after a command are parsed the same way as options
                         // before a command — unrecognized options are always errors.
    diff --git a/src/test/argsman_tests.cpp b/src/test/argsman_tests.cpp
    index c8fad91c0c..a966606200 100644
    --- a/src/test/argsman_tests.cpp
    +++ b/src/test/argsman_tests.cpp
    @@ -766,6 +766,20 @@ BOOST_AUTO_TEST_CASE(util_ParseParameters_gnu_style)
             BOOST_CHECK_EQUAL(cmd->args[1], "arg2");
         }
     
    +    // A lone hyphen after a command is a positional argument, not an option.
    +    {
    +        TestArgsManager test;
    +        test.SetupArgs({});
    +        test.AddCommand("cmd", "test command");
    +        std::string error;
    +        const char* argv[] = {"x", "cmd", "-"};
    +        BOOST_CHECK(test.ParseParameters(3, argv, error));
    +        const auto cmd = test.GetCommand();
    +        BOOST_REQUIRE(cmd);
    +        BOOST_REQUIRE_EQUAL(cmd->args.size(), 1U);
    +        BOOST_CHECK_EQUAL(cmd->args[0], "-");
    +    }
    +
         // Negative numbers after a command are positional args, not options.
         {
             TestArgsManager test;
    

    </details>

  24. w0xlt commented at 12:28 AM on August 13, 2026: contributor

    In the current PR code, the parser correctly recognizes a trailing option like -json, but bitcoin-tx later rereads the original arguments and mistakenly treats -json as a transaction-editing command, causing error: unknown command.

    The below fix makes bitcoin-tx use the already-filtered arguments while keeping its special - stdin behavior intact.

    <details> <summary>Suggested fix</summary>

    diff --git a/src/bitcoin-tx.cpp b/src/bitcoin-tx.cpp
    index 017e8e1e6c..13ed7d77a8 100644
    --- a/src/bitcoin-tx.cpp
    +++ b/src/bitcoin-tx.cpp
    @@ -797,35 +797,40 @@ static int CommandLineRawTx(int argc, char* argv[])
         std::string strPrint;
         int nRet = 0;
         try {
    -        // Skip switches; Permit common stdin convention "-"
    -        while (argc > 1 && IsSwitchChar(argv[1][0]) &&
    -               (argv[1][1] != 0)) {
    -            argc--;
    -            argv++;
    +        std::vector<std::string> args;
    +        if (const auto command{gArgs.GetCommand()}) {
    +            args = command->args;
    +        } else {
    +            // Parsing stops at "-", so fall back to argv to preserve stdin handling.
    +            while (argc > 1 && IsSwitchChar(argv[1][0]) && argv[1][1] != 0) {
    +                argc--;
    +                argv++;
    +            }
    +            args.assign(argv + 1, argv + argc);
             }
     
             CMutableTransaction tx;
    -        int startArg;
    +        size_t startArg;
     
             if (!fCreateBlank) {
                 // require at least one param
    -            if (argc < 2)
    +            if (args.empty())
                     throw std::runtime_error("too few parameters");
     
                 // param: hex-encoded bitcoin transaction
    -            std::string strHexTx(argv[1]);
    +            std::string strHexTx(args[0]);
                 if (strHexTx == "-")                 // "-" implies standard input
                     strHexTx = readStdin();
     
                 if (!DecodeHexTx(tx, strHexTx, true))
                     throw std::runtime_error("invalid transaction encoding");
     
    -            startArg = 2;
    -        } else
                 startArg = 1;
    +        } else
    +            startArg = 0;
     
    -        for (int i = startArg; i < argc; i++) {
    -            std::string arg = argv[i];
    +        for (size_t i{startArg}; i < args.size(); ++i) {
    +            const std::string& arg = args[i];
                 std::string key, value;
                 size_t eqpos = arg.find('=');
                 if (eqpos == std::string::npos)
    diff --git a/src/common/args.cpp b/src/common/args.cpp
    index bdc01ce61c..655e35a9bd 100644
    --- a/src/common/args.cpp
    +++ b/src/common/args.cpp
    @@ -210,7 +210,8 @@ bool ArgsManager::ParseParameters(int argc, const char* const argv[], std::strin
                         return false;
                     }
                 }
    -            m_command.push_back(key);
    +            // Preserve unregistered commands verbatim; key may have been normalized above.
    +            m_command.push_back(m_accept_any_command ? std::string{argv[i]} : key);
                 while (++i < argc) {
                     std::string cmd_arg(argv[i]);
                     // "--" ends option processing; all subsequent args are positional
    diff --git a/src/test/argsman_tests.cpp b/src/test/argsman_tests.cpp
    index c8fad91c0c..415fe6747c 100644
    --- a/src/test/argsman_tests.cpp
    +++ b/src/test/argsman_tests.cpp
    @@ -721,6 +721,19 @@ BOOST_AUTO_TEST_CASE(util_ParseParameters_gnu_style)
             BOOST_CHECK(cmd->args.empty());
         }
     
    +    // The first positional argument is preserved exactly.
    +    {
    +        TestArgsManager test;
    +        test.SetupArgs({});
    +        std::string error;
    +        const char* argv[] = {"x", "cmd=val"};
    +        BOOST_CHECK(test.ParseParameters(2, argv, error));
    +        const auto cmd = test.GetCommand();
    +        BOOST_REQUIRE(cmd);
    +        BOOST_REQUIRE_EQUAL(cmd->args.size(), 1U);
    +        BOOST_CHECK_EQUAL(cmd->args[0], "cmd=val");
    +    }
    +
         // Unrecognized option after command is an error (not silently treated as positional).
         {
             TestArgsManager test;
    diff --git a/test/functional/data/util/bitcoin-util-test.json b/test/functional/data/util/bitcoin-util-test.json
    index 2aaffff4b5..41013e4c10 100644
    --- a/test/functional/data/util/bitcoin-util-test.json
    +++ b/test/functional/data/util/bitcoin-util-test.json
    @@ -88,6 +88,11 @@
         "output_cmp": "blanktxv1.json",
         "description": "Creates a blank v1 transaction (output in json)"
       },
    +  { "exec": "./bitcoin-tx",
    +    "args": ["01000000000000000000", "-json"],
    +    "output_cmp": "blanktxv1.json",
    +    "description": "Parses an option after a transaction hex"
    +  },
       { "exec": "./bitcoin-tx",
         "args": ["-"],
         "input": "blanktxv2.hex",
    
  25. w0xlt commented at 12:28 AM on August 13, 2026: contributor

    Approach ACK

  26. DrahtBot removed the label Needs rebase on Aug 13, 2026
  27. pablomartin4btc commented at 1:20 AM on August 13, 2026: member

    The below fix makes bitcoin-tx use the already-filtered arguments while keeping its special - stdin behavior intact.

    I didn't want to include any change into bitcoin-tx, as @ryanofsky suggested in previous #33540 but it this code is making the tool fail, I'll add the fix, thanks!

  28. argsman, bitcoin-tx, cli, test: Allow options after non-option arguments (GNU-style)
    Add GNU-style command-line parsing to ArgsManager: options that appear
    after the first non-option argument are parsed and validated exactly like
    options that appear before it. Previously, arguments after the command
    were all silently treated as positional. Now unrecognized options are
    always errors; use "--" after the command to pass dash-prefixed positional
    arguments such as negative numbers or file paths.
    
    Also fix CommandLineRawTx to read pre-parsed arguments from ArgsManager
    rather than re-scanning the raw argv array, so that GNU-style options
    supplied after the transaction hex are applied correctly.
    
    Co-authored-by: w0xlt <w0xlt@users.noreply.github.com>
    692bc5be31
  29. argsman, test: Extend '--' to end option processing before the command
    The "--" separator already stopped option processing inside the inner
    (post-command) loop. Extend it to also work in the outer (pre-command)
    loop so that arguments beginning with a dash can be passed as the command
    name or as early positional arguments.
    
    Co-authored-by: w0xlt <w0xlt@users.noreply.github.com>
    5e874970ad
  30. doc: Add release notes for GNU-style option parsing 67a8e5bea2
  31. pablomartin4btc force-pushed on Aug 13, 2026
  32. pablomartin4btc commented at 5:19 AM on August 13, 2026: member

    -<ins>Updates</ins>:

    • Rebased.
    • Restructured the PR into 3 commits (GNU parsing + 3 inner-loop fixes, -- before command, release notes);
    • Fixed (inner loop, from @w0xlt reviews):
      • Windows: /OPT=C:/bitcoin after a command was mistakenly treated as positional because the embedded / in the value triggered the path check; now only the option name (before =) is checked;
      • Lone - after a command was misidentified as an option; now requires size > 1;
      • First positional argument verbatim preservation (cmd=val was being normalized by the outer =-split);
    • Fixed: bitcoin-tx CommandLineRawTx now uses gArgs.GetCommand() instead of re-scanning raw argv, so GNU-style options after the transaction hex work correctly;
    • Extended -- to also end option processing before the command (outer loop), not just after it.

    (CI failures are not related)

  33. DrahtBot added the label CI failed on Aug 13, 2026
  34. w0xlt commented at 8:03 PM on August 13, 2026: contributor

    ACK 67a8e5bea2e30c66d7f4eb33e8b124943c5d705f

  35. DrahtBot removed the label CI failed on Aug 17, 2026

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: 2026-08-24 02:51 UTC

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