{
 "number": 35831,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35831",
 "title": "argsman, cli: Allow options after non-option arguments (GNU-style)",
 "author": "pablomartin4btc",
 "author_association": "MEMBER",
 "created_at": "2026-07-28T16:18:57Z",
 "updated_at": "2026-08-28T07:53:46Z",
 "age_days": 50,
 "draft": false,
 "labels": [],
 "milestone": null,
 "base": "master",
 "head_sha": "67a8e5bea2e30c66d7f4eb33e8b124943c5d705f",
 "head_ref": "argsman/gnu-parsing-options-after-commands",
 "head_repo": "pablomartin4btc/bitcoin",
 "head_history": [
  {
   "t": "2026-07-28T16:50:54Z",
   "sha": "3f76d53bcd8ab72e4f11a967b5c3e496d2e7b0f9"
  },
  {
   "t": "2026-07-28T18:48:34Z",
   "sha": "7d111eef0c92b961064cae6089efaacdf8682592"
  },
  {
   "t": "2026-07-28T18:57:22Z",
   "sha": "f0e807ad76129761cb3d0d1b84c62493c0354b6e"
  },
  {
   "t": "2026-07-28T20:11:34Z",
   "sha": "31a3bc5428535cd7177baa646ec3a12367ab9216"
  },
  {
   "t": "2026-07-29T03:09:45Z",
   "sha": "fe22753e5dc1e280f148cb1b513a413e9331f5f3"
  },
  {
   "t": "2026-07-29T04:13:07Z",
   "sha": "586f98c540d09ff087fd874826b974107109ce77"
  },
  {
   "t": "2026-07-29T15:55:46Z",
   "sha": "44da9544aedfe5273ebba3c0ec9263facd77f271"
  },
  {
   "t": "2026-07-29T16:59:46Z",
   "sha": "b2ac15d3c85b15aa98c666c3d2b970dfece7f2c3"
  },
  {
   "t": "2026-08-12T23:04:43Z",
   "sha": "416ae10e801cbc70fc4c396413e02b459cf76942"
  },
  {
   "t": "2026-08-13T04:47:25Z",
   "sha": "67a8e5bea2e30c66d7f4eb33e8b124943c5d705f"
  }
 ],
 "additions": 285,
 "deletions": 38,
 "changed_files": 7,
 "commit_count": 3,
 "size_bucket": "M",
 "mergeable_state": "blocked",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "w0xlt",
      "url": "https://github.com/bitcoin/bitcoin/pull/35831#pullrequestreview-4931128049"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "w0xlt": {
   "kind": "ack",
   "hash": "67a8e5bea2e30c66d7f4eb33e8b124943c5d705f",
   "t": "2026-08-13T20:03:54Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 1,
  "stale_ack": 0,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "vicjuma",
   "w0xlt"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "blocked",
  "last_author_activity": "2026-08-13T05:19:06Z",
  "last_reviewer_activity": "2026-08-13T20:03:54Z",
  "last_reviewer": "w0xlt",
  "author_silent_days": 35,
  "waiting_on_author_days": 34,
  "days_since_update": 20
 },
 "refs": {
  "mentioned": [
   33540
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 33540,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "argsman, cli: GNU-style command-line option parsing (allows options after non-option arguments)"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/bitcoin-cli.cpp"
 ],
 "body": "Allow options to be specified after non-option arguments in `ParseParameters`, matching GNU option parsing conventions (`getopt_long` style).\n\nCurrently 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 \u2014 potentially triggering unintended RPC behaviour.\n\n**_With this change_**:\n- Valid options after a command are stored in `command_line_options` exactly as if they appeared before the command.\n- Unrecognized options after a command are always errors (no silent fallback to positional).\n- A `--` separator stops option processing; all subsequent arguments are treated as positional regardless of leading dashes.\n- `bitcoin-cli`'s `CommandLineRPC` is updated to derive its args vector from `gArgs.GetCommand()` rather than re-scanning raw `argv`.\n- `bitcoin-wallet` benefits automatically: options such as `-wallet` placed after a command are now parsed correctly instead of being rejected as unexpected positional arguments.\n\n<ins>Before and after:</ins> options after a command are now recognized...\n\n**`-rpcwallet` after the command and its positional args:**\n\n- Before (`master`) \u2014 `-rpcwallet` is silently ignored; request goes to the wrong wallet:\n\n    ```\n    $ bitcoin-cli -regtest -datadir=/tmp/btc listtransactions -rpcwallet=nonExistent\n    [\n    ]\n    ```\n\n- After \u2014 correctly routes to the named wallet and fails with the expected error:\n\n    ```\n    $ bitcoin-cli -regtest -datadir=/tmp/btc listtransactions -rpcwallet=nonExistent\n    error code: -18\n    error message:\n    Requested wallet does not exist or is not loaded\n    ```\n\n---\n\n**`getaddressinfo` with `-rpcwallet` appended:**\n\n- Before (`master`) \u2014 `-rpcwallet` becomes an extra positional arg; `getaddressinfo` errors with its usage string:\n\n    ```\n    $ bitcoin-cli -regtest -datadir=/tmp/btc getaddressinfo bcrt1q... -rpcwallet=nonExistent\n    error message:\n    getaddressinfo \"address\"\n    ...\n    ```\n\n- After \u2014 wallet is correctly selected:\n\n    ```\n    $ bitcoin-cli -regtest -datadir=/tmp/btc getaddressinfo bcrt1q... -rpcwallet=nonExistent\n    error code: -18\n    error message:\n    Requested wallet does not exist or is not loaded\n    ```\n\n---\n\n**Multi-wallet: `-rpcwallet` no longer needs to come before the command:**\n\n- Before (`master`):\n\n    ```\n    $ bitcoin-cli -regtest -datadir=/tmp/btc listtransactions\n    error code: -19\n    error message:\n    ...specify the \"-rpcwallet=<walletname>\" option before the command...\n    ```\n\n    ```\n    $ bitcoin-cli -regtest -datadir=/tmp/btc listtransactions -rpcwallet=mywallet\n    error code: -19   \u2190 still fails; option after command was ignored\n    error message:\n    ...specify the \"-rpcwallet=<walletname>\" option before the command...\n    ```\n\n- After:\n\n    \u2014 error message updated:\n    ```\n    $ bitcoin-cli -regtest -datadir=/tmp/btc listtransactions\n    error code: -19\n    error message:\n    ...specify the \"-rpcwallet=<walletname>\" option before or after the command...\n    ```\n\n    \u2014 command executed successfully:\n    ```\n    $ bitcoin-cli -regtest -datadir=/tmp/btc listtransactions -rpcwallet=mywallet\n    [\n      { \"address\": \"bcrt1q...\", \"category\": \"immature\", \"amount\": 50.00000000, ... }\n    ]\n    ```\n\n<ins>Before and after:</ins> misspelled options and the <code>--</code> separator...\n\n**Misspelled option is now an error rather than a silent positional arg:**\n\nBefore (`master`) \u2014 `-rpcwalllet ` (note triple-l) silently absorbed as the `label` positional arg:\n\n    ```\n    $ bitcoin-cli -regtest -datadir=/tmp/btc listtransactions <hash> -rpcwalllet=foo\n    { ... }   \u2190 result returned, -rpcwallet went unnoticed\n    ```\n\nAfter:\n\n    ```\n    $ bitcoin-cli -regtest -datadir=/tmp/btc getblock <hash> -rpcwalllet=foo\n    error: Invalid parameter -rpcwalllet\n    ```\n\n---\n\n**`--` separator: pass a dash-prefixed string as a positional RPC argument:**\n\n    ```\n    $ bitcoin-cli -regtest -datadir=/tmp/btc somecommand -- -not-an-option\n    ```\n\n    Everything after `--` is treated as a positional argument, so `-not-an-option` is passed to the RPC unchanged.\n\n<ins>Before and after:</ins> <code>bitcoin-wallet</code> commands...\n\n**`-wallet` option placed after the command:**\n\nBefore (`master`) \u2014 option treated as an unexpected positional arg:\n\n```\n    $ bitcoin-wallet -regtest -datadir=/tmp/btc create -wallet=myWallet\n    Error: Additional arguments provided (-wallet=myWallet). Methods do not take arguments. Please refer to -help.\n```\n\nAfter:\n\n```\n    $ bitcoin-wallet -regtest -datadir=/tmp/btc create -wallet=myWallet\n    Topping up keypool...\n    Wallet info\n\n    Name: myWallet\n    Format: sqlite\n    Descriptors: yes\n    Encrypted: no\n    HD (hd seed available): yes\n    Keypool Size: 8000\n    Transactions: 0\n    Address Book: 0\n```\n\n---\n\n**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.",
 "commits": [
  {
   "sha": "692bc5be31a2d0113f4e6361dc7d7489b6773be2",
   "date": "2026-08-13T01:39:49Z",
   "message": "argsman, bitcoin-tx, cli, test: Allow options after non-option arguments (GNU-style)\n\nAdd GNU-style command-line parsing to ArgsManager: options that appear\nafter the first non-option argument are parsed and validated exactly like\noptions that appear before it. Previously, arguments after the command\nwere all silently treated as positional. Now unrecognized options are\nalways errors; use \"--\" after the command to pass dash-prefixed positional\narguments such as negative numbers or file paths.\n\nAlso fix CommandLineRawTx to read pre-parsed arguments from ArgsManager\nrather than re-scanning the raw argv array, so that GNU-style options\nsupplied after the transaction hex are applied correctly.\n\nCo-authored-by: w0xlt <w0xlt@users.noreply.github.com>"
  },
  {
   "sha": "5e874970ad31a249c2ee60ac0e36d9f1934855fa",
   "date": "2026-08-13T01:39:58Z",
   "message": "argsman, test: Extend '--' to end option processing before the command\n\nThe \"--\" separator already stopped option processing inside the inner\n(post-command) loop. Extend it to also work in the outer (pre-command)\nloop so that arguments beginning with a dash can be passed as the command\nname or as early positional arguments.\n\nCo-authored-by: w0xlt <w0xlt@users.noreply.github.com>"
  },
  {
   "sha": "67a8e5bea2e30c66d7f4eb33e8b124943c5d705f",
   "date": "2026-08-13T01:40:01Z",
   "message": "doc: Add release notes for GNU-style option parsing"
  }
 ],
 "timeline": [
  {
   "t": "2026-07-28T16:50:54Z",
   "kind": "force_push",
   "who": "pablomartin4btc",
   "commit": "3f76d53bcd8ab72e4f11a967b5c3e496d2e7b0f9"
  },
  {
   "t": "2026-07-28T18:48:34Z",
   "kind": "force_push",
   "who": "pablomartin4btc",
   "commit": "7d111eef0c92b961064cae6089efaacdf8682592"
  },
  {
   "t": "2026-07-28T18:57:22Z",
   "kind": "force_push",
   "who": "pablomartin4btc",
   "commit": "f0e807ad76129761cb3d0d1b84c62493c0354b6e"
  },
  {
   "t": "2026-07-28T20:11:34Z",
   "kind": "force_push",
   "who": "pablomartin4btc",
   "commit": "31a3bc5428535cd7177baa646ec3a12367ab9216"
  },
  {
   "t": "2026-07-29T03:09:45Z",
   "kind": "force_push",
   "who": "pablomartin4btc",
   "commit": "fe22753e5dc1e280f148cb1b513a413e9331f5f3"
  },
  {
   "t": "2026-07-29T04:13:07Z",
   "kind": "force_push",
   "who": "pablomartin4btc",
   "commit": "586f98c540d09ff087fd874826b974107109ce77"
  },
  {
   "t": "2026-07-29T04:18:59Z",
   "kind": "comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "text": "-<ins>_**Updates**_</ins>:\n  - Force-pushed to fix CI failures found after the initial push:\n    - Windows (`rpc_psbt.py`) \u2014 positional arguments after a command (e.g. base64 PSBTs) were being lowercased, corrupting their values.\n    - Windows (`wallet_multiwallet.py`) \u2014 paths starting with `/` after a command were being converted to `-` and parsed as options; fixed by only applying the Windows `/`\u2192`-` conversion when there is no embedded slash (distinguishing `/rpcwallet=foo` from `/bad/./path`).\n    - Alpine/musl (`feature_pruning.py`) \u2014 negative integers like `-10` after a command were incorrectly treated as options instead of positional arguments.\n  - Also [added](https://github.com/pablomartin4btc/bitcoin/blob/586f98c540d09ff087fd874826b974107109ce77/src/test/argsman_tests.cpp#L773-L787) a `#ifdef WIN32` unit test covering Windows option syntax `(/OPT=val`, `-OPTION`) after a command, addressing a case [raised](https://github.com/bitcoin/bitcoin/pull/33540#issuecomment-3940254347) in [#33540](https://github.com/bitcoin/bitcoin/pull/33540)."
  },
  {
   "t": "2026-07-29T08:59:59Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "Concept ACK."
  },
  {
   "t": "2026-07-29T15:55:46Z",
   "kind": "force_push",
   "who": "pablomartin4btc",
   "commit": "44da9544aedfe5273ebba3c0ec9263facd77f271"
  },
  {
   "t": "2026-07-29T16:59:46Z",
   "kind": "force_push",
   "who": "pablomartin4btc",
   "commit": "b2ac15d3c85b15aa98c666c3d2b970dfece7f2c3"
  },
  {
   "t": "2026-07-29T18:01:51Z",
   "kind": "comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "text": "-<ins>_**Updates**_</ins>:\n  - 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.\n  - 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.\n  - Improved release notes: added `bitcoin-wallet` benefit, backwards compatibility note, reordered for clarity.\n  - Updated PR description: the multi-wallet \"After\" example now reflects that the `WALLET_NOT_SPECIFIED` error message reads \"before or after the command\"."
  },
  {
   "t": "2026-08-11T22:53:35Z",
   "kind": "review_comment",
   "who": "vicjuma",
   "assoc": "CONTRIBUTOR",
   "path": "src/bitcoin-cli.cpp",
   "commit": "67a8e5bea2e30c66d7f4eb33e8b124943c5d705f",
   "in_reply_to": null,
   "text": "This change produces a unified output, especially with the `-named` arg. Before the change, different RPC commands could result in different error messages:\n\n### Before this change\n**Getting a new address**\n```console\nratedg@0xratedg:~/projects/contributions/bitcoin/build/bin$ ./bitcoin-cli getnewaddress address_type=bech32m -named\nerror code: -5\nerror message:\nUnknown address type '-named'\n```\n\n**Listing transactions**\n```console\nratedg@0xratedg:~/projects/contributions/bitcoin/build/bin$ ./bitcoin-cli listtransactions count=10 -named\nerror: Error parsing JSON: -named\nratedg@0xratedg:~/projects/contributions/bitcoin/build/bin$\n```\n\n### After this change\n**Getting a new address**\n```console\nratedg@0xratedg:~/projects/contributions/bitcoin/build2/bin$ ./bitcoin-cli getnewaddress address_type=bech32m -named\nbcrt1p9x7uz943tc2h9ugcqkh37y066253k8lc6zmvwafhw4tpp9cgruaqdz4whx\nratedg@0xratedg:~/projects/contributions/bitcoin/build2/bin$\n```\n\n**Listing transactions**\n```console\nratedg@0xratedg:~/projects/contributions/bitcoin/build2/bin$ ./bitcoin-cli listtransactions count=1 -named\n[\n  {\n    \"address\": \"bcrt1prpkf2ezqw9aex635w7uja9f4mzy58660ls3xgynequ0ytvg6qy9qllxzgd\",\n    \"category\": \"send\",\n    \"amount\": -5.00000000,\n    \"label\": \"\",\n    \"vout\": 1,\n    \"fee\": -0.00001550,\n    \"confirmations\": 0,\n    \"trusted\": true,\n    \"txid\": \"a12778bc29388a83a128de9d9becda897fca096da075451e5fbb0b3592b1c7fe\",\n    \"wtxid\": \"8f3b585d8c65e83f0a662e338793aa07bb2418ecda66cc38c82cb24b26ad531b\",\n    \"walletconflicts\": [\n    ],\n    \"mempoolconflicts\": [\n    ],\n    \"time\": 1786485008,\n    \"timereceived\": 1786485008,\n    \"abandoned\": false\n  }\n]\nratedg@0xratedg:~/projects/contributions/bitcoin/build/bin$\n```\nI 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\n\n```console\nratedg@0xratedg:~/projects/contributions/bitcoin/build2/bin$ ./bitcoin-tx -regtest \\\n     in=7dc357e7ba70b0b503c9a12f4a293add0b1d230e08de3cad83e142737746c4b8:1 \\\n     outaddr=0.9:mmhnpJgEDuPxTmw6Nuu5Ms2xmP21n6XoCi -create\nerror: unknown command\nratedg@0xratedg:~/projects/contributions/bitcoin/build2/bin$\n```\n\nThe help utility is already detailed for users who may be confused about the ordering\n\n```txt\nUsage: bitcoin-cli [options] <command> [params]\nor:    bitcoin-cli [options] -named <command> [name=value]...\nor:    bitcoin-cli [options] help\nor:    bitcoin-cli [options] help <command>\n```\nI will wait for other reviews on this to get a clear picture."
  },
  {
   "t": "2026-08-11T22:57:36Z",
   "kind": "review",
   "who": "vicjuma",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "b2ac15d3c85b15aa98c666c3d2b970dfece7f2c3",
   "text": ":-)"
  },
  {
   "t": "2026-08-12T15:40:23Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/bitcoin-cli.cpp",
   "commit": "67a8e5bea2e30c66d7f4eb33e8b124943c5d705f",
   "in_reply_to": 3762311317,
   "text": "Thanks for your review @vicjuma.\n\n[quoted text omitted]\n`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](https://github.com/bitcoin/bitcoin/pull/33540#issuecomment-5106839935) and more focused to the GNU-style parsing.\n\n[quoted text omitted]\nPerhaps you should include the previous line that was the one added:\n\n```\n[options] can be specified before or after <command>.\n```"
  },
  {
   "t": "2026-08-12T23:04:43Z",
   "kind": "force_push",
   "who": "pablomartin4btc",
   "commit": "416ae10e801cbc70fc4c396413e02b459cf76942"
  },
  {
   "t": "2026-08-12T23:53:52Z",
   "kind": "review",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "416ae10e801cbc70fc4c396413e02b459cf76942",
   "text": "`--` 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 --`.\n\nIn GNU-style parsing, `--` should end option processing wherever it appears.\n\nSuggested fix\n\n```diff\ndiff --git a/src/common/args.cpp b/src/common/args.cpp\nindex bdc01ce61c..be42134123 100644\n--- a/src/common/args.cpp\n+++ b/src/common/args.cpp\n@@ -188,20 +188,28 @@ bool ArgsManager::ParseParameters(int argc, const char* const argv[], std::strin\n         if (key.starts_with(\"-psn_\")) continue;\n #endif\n\n-        if (key == \"-\") break; //bitcoin-tx using stdin\n+        bool options_ended{false};\n+        if (key == \"--\") {\n+            options_ended = true;\n+            if (++i >= argc) break;\n+            key = argv[i];\n+        }\n+        if (!options_ended && key == \"-\") break; //bitcoin-tx using stdin\n         std::optional<std::string> val;\n-        size_t is_index = key.find('=');\n-        if (is_index != std::string::npos) {\n-            val = key.substr(is_index + 1);\n-            key.erase(is_index);\n+        if (!options_ended) {\n+            size_t is_index = key.find('=');\n+            if (is_index != std::string::npos) {\n+                val = key.substr(is_index + 1);\n+                key.erase(is_index);\n+            }\n         }\n #ifdef WIN32\n         key = ToLower(key);\n-        if (key[0] == '/')\n+        if (!options_ended && key[0] == '/')\n             key[0] = '-';\n #endif\n\n-        if (key[0] != '-') {\n+        if (options_ended || key[0] != '-') {\n             if (!m_accept_any_command && m_command.empty()) {\n                 // The first non-dash arg is a registered command\n                 std::optional<unsigned int> flags = GetArgFlags_(key);\n@@ -211,6 +219,10 @@ bool ArgsManager::ParseParameters(int argc, const char* const argv[], std::strin\n                 }\n             }\n             m_command.push_back(key);\n+            if (options_ended) {\n+                while (++i < argc) m_command.emplace_back(argv[i]);\n+                break;\n+            }\n             while (++i < argc) {\n                 std::string cmd_arg(argv[i]);\n                 // \"--\" ends option processing; all subsequent args are positional\ndiff --git a/src/test/argsman_tests.cpp b/src/test/argsman_tests.cpp\nindex c8fad91c0c..4b9834a956 100644\n--- a/src/test/argsman_tests.cpp\n+++ b/src/test/argsman_tests.cpp\n@@ -750,6 +750,39 @@ BOOST_AUTO_TEST_CASE(util_ParseParameters_gnu_style)\n         BOOST_CHECK_EQUAL(cmd->args[1], \"arg2\");\n     }\n\n+    // \"--\" also ends option processing before the command.\n+    {\n+        TestArgsManager test;\n+        test.SetupArgs({{\"-opt\", ArgsManager::ALLOW_ANY}});\n+        test.AddCommand(\"cmd\", \"test command\");\n+        std::string error;\n+        const char* argv[] = {\"x\", \"--\", \"cmd\", \"-not-an-option\", \"arg2\"};\n+        BOOST_CHECK(test.ParseParameters(5, argv, error));\n+        BOOST_CHECK(!test.IsArgSet(\"-opt\"));\n+        const auto cmd = test.GetCommand();\n+        BOOST_REQUIRE(cmd);\n+        BOOST_CHECK_EQUAL(cmd->command, \"cmd\");\n+        BOOST_REQUIRE_EQUAL(cmd->args.size(), 2U);\n+        BOOST_CHECK_EQUAL(cmd->args[0], \"-not-an-option\");\n+        BOOST_CHECK_EQUAL(cmd->args[1], \"arg2\");\n+    }\n+\n+    // Options before \"--\" are parsed, and the first positional argument after\n+    // it may begin with a dash.\n+    {\n+        TestArgsManager test;\n+        test.SetupArgs({{\"-opt\", ArgsManager::ALLOW_ANY}});\n+        std::string error;\n+        const char* argv[] = {\"x\", \"-opt\", \"--\", \"-1\"};\n+        BOOST_CHECK(test.ParseParameters(4, argv, error));\n+        BOOST_CHECK(test.IsArgSet(\"-opt\"));\n+        const auto cmd = test.GetCommand();\n+        BOOST_REQUIRE(cmd);\n+        BOOST_CHECK(cmd->command.empty());\n+        BOOST_REQUIRE_EQUAL(cmd->args.size(), 1U);\n+        BOOST_CHECK_EQUAL(cmd->args[0], \"-1\");\n+    }\n+\n     // Options and positional args can be freely mixed after a command.\n     {\n         TestArgsManager test;\n```"
  },
  {
   "t": "2026-08-13T00:06:16Z",
   "kind": "review",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "416ae10e801cbc70fc4c396413e02b459cf76942",
   "text": "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 `/`.\n\nSuggested fix\n\n```diff\ndiff --git a/src/common/args.cpp b/src/common/args.cpp\nindex bdc01ce61c..98c254cbaa 100644\n--- a/src/common/args.cpp\n+++ b/src/common/args.cpp\n@@ -224,10 +224,12 @@ bool ArgsManager::ParseParameters(int argc, const char* const argv[], std::strin\n #ifdef WIN32\n                 cmd_key = ToLower(cmd_key);\n                 // Unlike pre-command args (options only), post-command args can be\n-                // file paths. Only treat /foo as -foo when there is no embedded\n-                // slash, which distinguishes Windows options (/rpcwallet=foo) from\n-                // paths (/bad/./path). Bare '/' is excluded by the size > 1 check.\n-                if (cmd_key.size() > 1 && cmd_key[0] == '/' && cmd_key.find('/', 1) == std::string::npos)\n+                // file paths. Only check the option name for embedded slashes, as\n+                // the option value itself may be a path. This distinguishes Windows\n+                // options (/rpcwallet=dir/wallet) from paths (/bad/./path). Bare '/'\n+                // is excluded by the size > 1 check.\n+                const auto option_name{cmd_key.substr(0, cmd_key.find('='))};\n+                if (option_name.size() > 1 && option_name[0] == '/' && option_name.find('/', 1) == std::string::npos)\n                     cmd_key[0] = '-';\n #endif\n                 // Negative integers (e.g. -10, -3) are positional args, not options.\ndiff --git a/src/test/argsman_tests.cpp b/src/test/argsman_tests.cpp\nindex c8fad91c0c..4744d20030 100644\n--- a/src/test/argsman_tests.cpp\n+++ b/src/test/argsman_tests.cpp\n@@ -782,18 +782,20 @@ BOOST_AUTO_TEST_CASE(util_ParseParameters_gnu_style)\n     }\n\n #ifdef WIN32\n-    // On Windows, /option and -OPTION are equivalent to -option after a command.\n+    // On Windows, slash options may have path values, while slash-prefixed paths\n+    // remain positional arguments.\n     {\n         TestArgsManager test;\n         test.SetupArgs({{\"-opt\", ArgsManager::ALLOW_ANY}});\n         test.AddCommand(\"cmd\", \"test command\");\n         std::string error;\n-        const char* argv[] = {\"x\", \"cmd\", \"/OPT=val\"};\n-        BOOST_CHECK(test.ParseParameters(3, argv, error));\n-        BOOST_CHECK_EQUAL(test.GetArg(\"-opt\", \"\"), \"val\");\n+        const char* argv[] = {\"x\", \"cmd\", \"/OPT=C:/bitcoin\", \"/bad/./path\"};\n+        BOOST_CHECK(test.ParseParameters(4, argv, error));\n+        BOOST_CHECK_EQUAL(test.GetArg(\"-opt\", \"\"), \"C:/bitcoin\");\n         const auto cmd = test.GetCommand();\n         BOOST_REQUIRE(cmd);\n-        BOOST_CHECK(cmd->args.empty());\n+        BOOST_REQUIRE_EQUAL(cmd->args.size(), 1U);\n+        BOOST_CHECK_EQUAL(cmd->args[0], \"/bad/./path\");\n     }\n #endif\n }\n```"
  },
  {
   "t": "2026-08-13T00:14:52Z",
   "kind": "review",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "416ae10e801cbc70fc4c396413e02b459cf76942",
   "text": "A lone - after a command, such as in `bitcoin-cli echo -`, is incorrectly treated as an option and rejected with `Invalid parameter -`.\n\nA single hyphen is a positional argument; an option must contain at least one character after it.\n\nSuggested fix\n\n```diff\ndiff --git a/src/common/args.cpp b/src/common/args.cpp\nindex bdc01ce61c..b583938a1a 100644\n--- a/src/common/args.cpp\n+++ b/src/common/args.cpp\n@@ -233,7 +233,7 @@ bool ArgsManager::ParseParameters(int argc, const char* const argv[], std::strin\n                 // Negative integers (e.g. -10, -3) are positional args, not options.\n                 bool is_number = cmd_key.size() > 1 && cmd_key[1] != '-' &&\n                                  std::all_of(cmd_key.begin() + 1, cmd_key.end(), IsDigit);\n-                bool is_option = !cmd_key.empty() && cmd_key[0] == '-' && !is_number;\n+                bool is_option = cmd_key.size() > 1 && cmd_key[0] == '-' && !is_number;\n                 if (is_option) {\n                     // Options after a command are parsed the same way as options\n                     // before a command \u2014 unrecognized options are always errors.\ndiff --git a/src/test/argsman_tests.cpp b/src/test/argsman_tests.cpp\nindex c8fad91c0c..a966606200 100644\n--- a/src/test/argsman_tests.cpp\n+++ b/src/test/argsman_tests.cpp\n@@ -766,6 +766,20 @@ BOOST_AUTO_TEST_CASE(util_ParseParameters_gnu_style)\n         BOOST_CHECK_EQUAL(cmd->args[1], \"arg2\");\n     }\n\n+    // A lone hyphen after a command is a positional argument, not an option.\n+    {\n+        TestArgsManager test;\n+        test.SetupArgs({});\n+        test.AddCommand(\"cmd\", \"test command\");\n+        std::string error;\n+        const char* argv[] = {\"x\", \"cmd\", \"-\"};\n+        BOOST_CHECK(test.ParseParameters(3, argv, error));\n+        const auto cmd = test.GetCommand();\n+        BOOST_REQUIRE(cmd);\n+        BOOST_REQUIRE_EQUAL(cmd->args.size(), 1U);\n+        BOOST_CHECK_EQUAL(cmd->args[0], \"-\");\n+    }\n+\n     // Negative numbers after a command are positional args, not options.\n     {\n         TestArgsManager test;\n```"
  },
  {
   "t": "2026-08-13T00:28:06Z",
   "kind": "review",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "416ae10e801cbc70fc4c396413e02b459cf76942",
   "text": "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`.\n\nThe below fix makes `bitcoin-tx` use the already-filtered arguments while keeping its special `- stdin` behavior intact.\n\nSuggested fix\n\n```diff\ndiff --git a/src/bitcoin-tx.cpp b/src/bitcoin-tx.cpp\nindex 017e8e1e6c..13ed7d77a8 100644\n--- a/src/bitcoin-tx.cpp\n+++ b/src/bitcoin-tx.cpp\n@@ -797,35 +797,40 @@ static int CommandLineRawTx(int argc, char* argv[])\n     std::string strPrint;\n     int nRet = 0;\n     try {\n-        // Skip switches; Permit common stdin convention \"-\"\n-        while (argc > 1 && IsSwitchChar(argv[1][0]) &&\n-               (argv[1][1] != 0)) {\n-            argc--;\n-            argv++;\n+        std::vector<std::string> args;\n+        if (const auto command{gArgs.GetCommand()}) {\n+            args = command->args;\n+        } else {\n+            // Parsing stops at \"-\", so fall back to argv to preserve stdin handling.\n+            while (argc > 1 && IsSwitchChar(argv[1][0]) && argv[1][1] != 0) {\n+                argc--;\n+                argv++;\n+            }\n+            args.assign(argv + 1, argv + argc);\n         }\n\n         CMutableTransaction tx;\n-        int startArg;\n+        size_t startArg;\n\n         if (!fCreateBlank) {\n             // require at least one param\n-            if (argc < 2)\n+            if (args.empty())\n                 throw std::runtime_error(\"too few parameters\");\n\n             // param: hex-encoded bitcoin transaction\n-            std::string strHexTx(argv[1]);\n+            std::string strHexTx(args[0]);\n             if (strHexTx == \"-\")                 // \"-\" implies standard input\n                 strHexTx = readStdin();\n\n             if (!DecodeHexTx(tx, strHexTx, true))\n                 throw std::runtime_error(\"invalid transaction encoding\");\n\n-            startArg = 2;\n-        } else\n             startArg = 1;\n+        } else\n+            startArg = 0;\n\n-        for (int i = startArg; i < argc; i++) {\n-            std::string arg = argv[i];\n+        for (size_t i{startArg}; i < args.size(); ++i) {\n+            const std::string& arg = args[i];\n             std::string key, value;\n             size_t eqpos = arg.find('=');\n             if (eqpos == std::string::npos)\ndiff --git a/src/common/args.cpp b/src/common/args.cpp\nindex bdc01ce61c..655e35a9bd 100644\n--- a/src/common/args.cpp\n+++ b/src/common/args.cpp\n@@ -210,7 +210,8 @@ bool ArgsManager::ParseParameters(int argc, const char* const argv[], std::strin\n                     return false;\n                 }\n             }\n-            m_command.push_back(key);\n+            // Preserve unregistered commands verbatim; key may have been normalized above.\n+            m_command.push_back(m_accept_any_command ? std::string{argv[i]} : key);\n             while (++i < argc) {\n                 std::string cmd_arg(argv[i]);\n                 // \"--\" ends option processing; all subsequent args are positional\ndiff --git a/src/test/argsman_tests.cpp b/src/test/argsman_tests.cpp\nindex c8fad91c0c..415fe6747c 100644\n--- a/src/test/argsman_tests.cpp\n+++ b/src/test/argsman_tests.cpp\n@@ -721,6 +721,19 @@ BOOST_AUTO_TEST_CASE(util_ParseParameters_gnu_style)\n         BOOST_CHECK(cmd->args.empty());\n     }\n\n+    // The first positional argument is preserved exactly.\n+    {\n+        TestArgsManager test;\n+        test.SetupArgs({});\n+        std::string error;\n+        const char* argv[] = {\"x\", \"cmd=val\"};\n+        BOOST_CHECK(test.ParseParameters(2, argv, error));\n+        const auto cmd = test.GetCommand();\n+        BOOST_REQUIRE(cmd);\n+        BOOST_REQUIRE_EQUAL(cmd->args.size(), 1U);\n+        BOOST_CHECK_EQUAL(cmd->args[0], \"cmd=val\");\n+    }\n+\n     // Unrecognized option after command is an error (not silently treated as positional).\n     {\n         TestArgsManager test;\ndiff --git a/test/functional/data/util/bitcoin-util-test.json b/test/functional/data/util/bitcoin-util-test.json\nindex 2aaffff4b5..41013e4c10 100644\n--- a/test/functional/data/util/bitcoin-util-test.json\n+++ b/test/functional/data/util/bitcoin-util-test.json\n@@ -88,6 +88,11 @@\n     \"output_cmp\": \"blanktxv1.json\",\n     \"description\": \"Creates a blank v1 transaction (output in json)\"\n   },\n+  { \"exec\": \"./bitcoin-tx\",\n+    \"args\": [\"01000000000000000000\", \"-json\"],\n+    \"output_cmp\": \"blanktxv1.json\",\n+    \"description\": \"Parses an option after a transaction hex\"\n+  },\n   { \"exec\": \"./bitcoin-tx\",\n     \"args\": [\"-\"],\n     \"input\": \"blanktxv2.hex\",\n```"
  },
  {
   "t": "2026-08-13T00:28:36Z",
   "kind": "review",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "416ae10e801cbc70fc4c396413e02b459cf76942",
   "text": "Approach ACK"
  },
  {
   "t": "2026-08-13T01:20:27Z",
   "kind": "comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nI didn't want to include any change into `bitcoin-tx`, as @ryanofsky [suggested](https://github.com/bitcoin/bitcoin/pull/33540#issuecomment-5106839935) in previous #33540 but it this code is making the tool fail, I'll add the fix, thanks!"
  },
  {
   "t": "2026-08-13T04:47:25Z",
   "kind": "force_push",
   "who": "pablomartin4btc",
   "commit": "67a8e5bea2e30c66d7f4eb33e8b124943c5d705f"
  },
  {
   "t": "2026-08-13T05:19:06Z",
   "kind": "comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "text": "-<ins>_**Updates**_</ins>:\n  - Rebased.\n  - Restructured the PR into 3 commits (GNU parsing + 3 inner-loop fixes, `--` before command, release notes);\n  - Fixed (inner loop, from @w0xlt reviews):\n    - 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;\n    - Lone `-` after a command was misidentified as an option; now requires size > 1;\n    - First positional argument verbatim preservation (`cmd=val` was being normalized by the outer =-split);\n  - Fixed: `bitcoin-tx` `CommandLineRawTx` now uses `gArgs.GetCommand()` instead of re-scanning raw `argv`, so GNU-style options after the transaction hex work correctly;\n  - Extended `--` to also end option processing before the command (outer loop), not just after it.\n\n_(CI failures are not related)_"
  },
  {
   "t": "2026-08-13T20:03:54Z",
   "kind": "review",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "67a8e5bea2e30c66d7f4eb33e8b124943c5d705f",
   "text": "ACK 67a8e5bea2e30c66d7f4eb33e8b124943c5d705f"
  }
 ],
 "labels_log": [
  {
   "t": "2026-07-28T16:51:22Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-29T04:10:51Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-11T16:15:13Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-13T00:59:11Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-13T06:06:50Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-17T09:17:39Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 26525,
 "text_tokens_estimate": 6631,
 "changed_paths": [
  "doc/release-notes-35831.md",
  "src/bitcoin-cli.cpp",
  "src/bitcoin-tx.cpp",
  "src/common/args.cpp",
  "src/test/argsman_tests.cpp",
  "test/functional/data/util/bitcoin-util-test.json",
  "test/functional/interface_bitcoin_cli.py"
 ],
 "files": [
  {
   "path": "doc/release-notes-35831.md",
   "add": 14,
   "del": 0
  },
  {
   "path": "src/bitcoin-cli.cpp",
   "add": 4,
   "del": 7
  },
  {
   "path": "src/bitcoin-tx.cpp",
   "add": 17,
   "del": 12
  },
  {
   "path": "src/common/args.cpp",
   "add": 68,
   "del": 10
  },
  {
   "path": "src/test/argsman_tests.cpp",
   "add": 167,
   "del": 6
  },
  {
   "path": "test/functional/data/util/bitcoin-util-test.json",
   "add": 5,
   "del": 0
  },
  {
   "path": "test/functional/interface_bitcoin_cli.py",
   "add": 10,
   "del": 3
  }
 ],
 "test_lines": 191,
 "git": {
  "head": "67a8e5bea2e30c66d7f4eb33e8b124943c5d705f",
  "head_matches_backup": true,
  "base": "b2c45888fde06429e86913fab5e7b7a075f091c3",
  "commits": [
   {
    "sha": "692bc5be31",
    "subject": "argsman, bitcoin-tx, cli, test: Allow options after non-option arguments (GNU-style)",
    "files": 6,
    "add": 220,
    "del": 31
   },
   {
    "sha": "5e874970ad",
    "subject": "argsman, test: Extend '--' to end option processing before the command",
    "files": 2,
    "add": 51,
    "del": 7
   },
   {
    "sha": "67a8e5bea2",
    "subject": "doc: Add release notes for GNU-style option parsing",
    "files": 1,
    "add": 14,
    "del": 0
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "42a249372e578473",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}