refactor: Implement missing error checking for ArgsManager flags #16545

pull ryanofsky wants to merge 6 commits into bitcoin:master from ryanofsky:pr/argcheck changing 9 files +836 −88
  1. ryanofsky commented at 2:13 pm on August 4, 2019: contributor

    Trigger startup errors if bitcoin is configured with bad setting values according to flags. Also raise internal errors if settings are registered and retrieved with inconsistent flags.

    This change has no effect on behavior because the new ArgsManager flags added here are not used outside of tests yet.

    It’ll probably be easier to start applying type checking flags to new options than existing options. But for examples of how type checking flags can make sense applied to existing options, see the new example_options unit test added in this PR.


    Followups:

    • Support for flag combinations is possible and is added in commit c919f51c044f0e80ecd301f9c9d396ddff2331ba (branch)
    • ALLOW_LIST flags are added and enforced in #17580
    • Bad IsArgSet usages with ALLOW_LIST are removed in #30529 and prevented in #17783
    • Confusing and ignored multiple assignments in config files are disallowed in #17493
    • Confusing reverse-precedence settings merging code is removed in #17581
    • Type flags added in this PR implement runtime part of a change to add more compile-time checking in #22978
  2. ryanofsky commented at 2:14 pm on August 4, 2019: contributor
    FYI @hebasto, this adds error checking for the flags from #16097
  3. ryanofsky force-pushed on Aug 4, 2019
  4. in src/util/system.h:137 in 1efb9ee41a outdated
    138+        NONE = 0x0, //<! Indicates flag lookup failed.
    139+
    140+        /* Low level validation flags. In most cases these should be avoided,
    141+         * and standard TYPE_* flag combinations below should be preferred. */
    142+        ALLOW_ANY      = 0x01, //!< disable validation
    143+        ALLOW_NEGATED  = 0x02, //!< allow -nofoo and -nofoo=1
    


    hebasto commented at 2:52 pm on August 4, 2019:
    Nice to see ALLOW_NEGATED flag again ;)

    ryanofsky commented at 8:55 am on August 7, 2019:

    re: #16545 (review)

    Nice to see ALLOW_NEGATED flag again ;)

    It is ok to have this, but like the comment above it says, it should only be used in rare cases, where a negated setting needs to be treated differently than a false/0/empty bool/int/string setting. Normal code for normal options should never have to use ALLOW_NEGATED or IsArgNegated.

  5. in src/util/system.h:149 in 1efb9ee41a outdated
    147+        ALLOW_BOOL     = 0x20, //!< allow -foo=0 and -foo=1
    148+
    149+        /* Standard value types. */
    150+        TYPE_STRING = ALLOW_NONEMPTY,
    151+        TYPE_OPTIONAL_STRING = ALLOW_NONEMPTY | ALLOW_NEGATED | ALLOW_EMPTY,
    152+        TYPE_INT = ALLOW_INT | ALLOW_NEGATED,
    


    hebasto commented at 2:54 pm on August 4, 2019:
    Could we drop ALLOW_NEGATED for TYPE_INT now or in the future?

    ryanofsky commented at 8:55 am on August 7, 2019:

    #16545 (review)

    Could we drop ALLOW_NEGATED for TYPE_INT now or in the future?

    This seems like a bad thing to provide as a shortcut. If an option is an integer, treating -nonumber exactly the same as -number=0 provides a simple interface to users consistent with what we’ve always provided in the past. Treating these the same is also less error prone for developers than treating them differently. It is possible there may be exceptions where treating -nonumber differently from -number=0 would be a good thing, but I’d honestly be surprised to see one, and ALLOW_ flags and IsArgNegated function are available if this is actually desired.

    In case the reason you’re asking about this is because you want range checking for integer options that shouldn’t accept 0, I think it’d be better to implement that in a way that disallows both -nonumber and -number=0 settings, not just one of them. This could be added with a TYPE_POSITIVE_INT shortcut, or letting AddArg take validation callbacks or options structs (IntType{}.MinValue(1).MaxValue(65535)) in place of flags.


    hebasto commented at 3:48 pm on August 10, 2019:

    If an option is an integer, treating -nonumber exactly the same as -number=0 provides a simple interface to users…

    Such practice is a bad habit, IMO.

    … consistent with what we’ve always provided in the past. Treating these the same is also less error prone for developers than treating them differently.

    Agree. It is important.

  6. DrahtBot commented at 2:59 pm on August 4, 2019: 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/16545.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK laanwj, hodlinator
    Approach ACK l0rinc
    Approach NACK ajtowns
    Stale ACK hebasto

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    Conflicts

    No conflicts as of last run.

  7. DrahtBot added the label Tests on Aug 4, 2019
  8. DrahtBot added the label Utils/log/libs on Aug 4, 2019
  9. hebasto commented at 3:21 pm on August 4, 2019: member

    It seems that not providing a new InterpretNegated() function has some benefits:

    • no need for key_name local variables
    • diff gets much smaller
  10. ryanofsky force-pushed on Aug 5, 2019
  11. ryanofsky marked this as ready for review on Aug 5, 2019
  12. in src/util/system.cpp:274 in 00973301f6 outdated
    259@@ -260,8 +260,60 @@ class ArgsManagerHelper {
    260         }
    261         return InterpretBool(found_result.second); // is set, so evaluate
    262     }
    263+
    264+    static inline void CheckFlags(const ArgsManager& am, const std::string& key, unsigned int require_any)
    265+    {
    266+        unsigned int flags = am.FlagsOfKnownArg(key);
    267+        if (flags != ArgsManager::NONE && ((flags & require_any) == 0)) {
    


    promag commented at 11:32 pm on August 6, 2019:

    00973301f6a1efb6aa2232544a49b080ac0c6c50

    Looks like this should be an assertion?

    0        if (flags) assert(flags & require_any);
    

    ryanofsky commented at 8:55 am on August 7, 2019:

    re: #16545 (review)

    Looks like this should be an assertion?

    I know you didn’t check the tests yet, but the main reason for this being a logic_error instead of an assert is so the test can require and check this logic so it won’t be accidentally broken in the future. The other reason this is a logic_error is to provide clearer feedback to a developer who accidentally sets the wrong flag or calls the wrong GetArg overload, by providing the option names and flags values in error messages.

    I tdo think it makes sense to use assert/abort instead of logic_error in cases where the error is critical (affecting consensus, or data integrity) and it’s important to abort unconditionally and not take a risk that a caller may catch the error and handle it inappropriately. But this case is less critical than that.

    Also this case really can and should be a compile error, not a runtime error. But it will take some more work and scripted-diffs to make that happen.


    promag commented at 10:53 am on August 7, 2019:

    Actually I did check and ran the tests with my change and I understand the idea of testing the exception, which wouldn’t make sense if it was an assertion.

    Also this case really can and should be a compile error

    Agree.

  13. in src/util/system.h:144 in 1efb9ee41a outdated
    145+        ALLOW_NONEMPTY = 0x08, //!< allow -foo=bar
    146+        ALLOW_INT      = 0x10, //!< allow -foo=123
    147+        ALLOW_BOOL     = 0x20, //!< allow -foo=0 and -foo=1
    148+
    149+        /* Standard value types. */
    150+        TYPE_STRING = ALLOW_NONEMPTY,
    


    promag commented at 11:59 pm on August 6, 2019:

    1efb9ee41ab228a73d293603838951af6fb0f59c

    It would be nice to see a commit elsewhere using these “types”.


    ryanofsky commented at 8:55 am on August 7, 2019:

    re: #16545 (review)

    It would be nice to see a commit elsewhere using these “types”.

    This is a good idea. I can open a draft PR applying these to some wallet flags, since I know hebasto already has good work on node flags like -datadir, -blocksdir, -pid etc that I don’t want to interfere with. I do think any PR actually using flags should be merged after this one since changing flag behavior after flags are already in use would be unnecessarily confusing and risky.


    promag commented at 10:54 am on August 7, 2019:
    Maybe a brief example in the PR description then?

    ryanofsky commented at 1:34 pm on August 8, 2019:

    Maybe a brief example in the PR description then?

    Another really good idea! Will add this.

  14. promag commented at 0:01 am on August 7, 2019: contributor
    Looks good, read the code and tried locally. Will review tests, but for now just a couple of comments.
  15. ryanofsky commented at 10:35 am on August 7, 2019: contributor

    Thanks for reviews!

    re: #16545 (comment)

    It seems that not providing a new InterpretNegated() function has some benefits:

    • no need for key_name local variables
    • diff gets much smaller

    The function is only 9 lines long, so I don’t see it having a very big impact on the size of the diff, but there are a few reasons why I think it’s important for this to be separate and not part of FlagsOfKnownArg:

    • FlagsOfKnownArg is now called at argument retrieval time, not jut argument parsing time, so it’s more efficient and easier to reason about if it is only doing a pure map lookup without string manipulation and parsing.

    • Having this be a separate function makes InterpretOption shorter and more understandable. Instead of parsing option names, parsing option values, and dealing with flags, it now only parses option values and no longer deals with flags or names at all.

    • Getting rid of the InterpretNegated function may reduce the size of this diff, but it would increase the diff in #15934, because it would leave more code for that PR to deduplicate and update. It’s nice to take care of some work for #15934 when the changes make sense here as well.

  16. ryanofsky force-pushed on Aug 7, 2019
  17. promag commented at 10:58 am on August 7, 2019: contributor
    Side note, I find FlagsOfKnownArg confusing because it also returns for unknown arguments and it doesn’t tell if it’s known or not. I’d reword to GetArgFlags.
  18. ryanofsky commented at 1:57 pm on August 8, 2019: contributor

    Promag’s suggestion to work on a commit using the flags #16545 (review) was really useful. Turns out after trying this out with wallet flags, the empty string check for TYPE_STRING is a lot less useful than I thought it would be, so I’m going to simplify the flags a little and get rid of it.

    It does seem good to just start off with a minimal set of checks,, and then in the future when we think of checks that actually are useful in practice (maybe checks for ip addresses, timestamps, hex strings, value ranges) we can add them at that point.

    I’m going to:

    • Make a little followup PR applying new types to some wallet args
    • Tweak flags a little in this PR to be less crazy about empty strings
    • Add example of using flags in the PR description as suggested and link to wallet flag PR
    • Improve comments, make /* Standard value types. */ comment more descriptive and add notes to IsArgSet and IsArgNegated methods explaining how there’s no need to call these if the argument has a type declared.

    Actual changes to this PR will be very small, so it still should be reviewable now.

    Side note, I find FlagsOfKnownArg confusing because it also returns for unknown arguments and it doesn’t tell if it’s known or not. I’d reword to GetArgFlags.

    Both seem good to me, but get GetArgFlags does seem a little more standard, so I’ll rename if @hebasto also says it’s better or just as good.

  19. hebasto commented at 4:09 pm on August 10, 2019: member

    Concept ACK 01ca54a2411ff8f39fa10974327e882141140739 @promag

    Side note, I find FlagsOfKnownArg confusing because it also returns for unknown arguments and it doesn’t tell if it’s known or not. I’d reword to GetArgFlags.

    For unknown arguments it returns ArgsManager::NONE. @ryanofsky

    Both seem good to me, but get GetArgFlags does seem a little more standard, so I’ll rename if @hebasto also says it’s better or just as good.

    Naming is the hardest part of coding ;) Agree with @promag’s suggestion about renaming.

  20. in src/util/system.h:131 in 01ca54a241 outdated
    128@@ -129,12 +129,23 @@ class ArgsManager
    129 {
    130 public:
    131     enum Flags {
    


    hebasto commented at 4:56 pm on August 10, 2019:

    Could the underlying type be specified?

    0     enum Flags : unsigned int {
    

    Ref: #16097 (review)


    ryanofsky commented at 4:40 pm on August 14, 2019:

    Could the underlying type be specified?

    Done in 9a1e20eef36b9dfff9de7780b0edd30b8ae92487

  21. ryanofsky force-pushed on Aug 13, 2019
  22. ryanofsky referenced this in commit 0779ce57d4 on Sep 9, 2019
  23. ryanofsky force-pushed on Sep 9, 2019
  24. ryanofsky referenced this in commit 36f2aded05 on Sep 9, 2019
  25. ryanofsky commented at 8:49 pm on September 9, 2019: contributor

    Updated c7d018d78ef0b78891fbba1c02b775fe156e0a1d -> b5e8b2a406900bb8d3f8bac1992f5367bbb3f2c9 (pr/argcheck.5 -> pr/argcheck.6, compare) with changes described in #16545#pullrequestreview-272573893.

    Agree with @promag’s suggestion about renaming.

    Now renamed in 0779ce57d4d97040108c81c75bc9a2b332a74ed1.

  26. laanwj commented at 10:58 am on October 23, 2019: member
    Concept ACK, consistent argument error checking is good.
  27. DrahtBot added the label Needs rebase on Oct 28, 2019
  28. ryanofsky referenced this in commit 959096cbac on Nov 14, 2019
  29. ryanofsky force-pushed on Nov 14, 2019
  30. in src/util/system.cpp:487 in 4b8149b5aa outdated
    409@@ -350,11 +410,28 @@ Optional<unsigned int> ArgsManager::GetArgFlags(const std::string& name) const
    410     return nullopt;
    411 }
    412 
    413+bool ArgsManager::CheckArgFlags(const std::string& name,
    414+    unsigned int require,
    415+    unsigned int forbid,
    416+    const char* context) const
    


    ryanofsky commented at 5:01 am on November 14, 2019:

    In commit “Implement missing error checking for ArgsManager flags” (4b8149b5aae874cffe81423a2d4529c6f5b6e944)

    Note: comment describing this function is in the header file shown below on github

  31. DrahtBot removed the label Needs rebase on Nov 14, 2019
  32. ryanofsky referenced this in commit 6974a2fecb on Nov 14, 2019
  33. ryanofsky referenced this in commit e1dd793a94 on Nov 14, 2019
  34. ryanofsky force-pushed on Nov 14, 2019
  35. ryanofsky renamed this:
    Implement missing error checking for ArgsManager flags
    refactor: Implement missing error checking for ArgsManager flags
    on Nov 24, 2019
  36. ryanofsky referenced this in commit dc0f148074 on Dec 18, 2019
  37. ryanofsky referenced this in commit 425bb30725 on Dec 18, 2019
  38. ryanofsky referenced this in commit cba2710220 on Dec 18, 2019
  39. maflcko referenced this in commit 6677be64f6 on Dec 19, 2019
  40. DrahtBot added the label Needs rebase on Dec 19, 2019
  41. ryanofsky force-pushed on Dec 19, 2019
  42. DrahtBot removed the label Needs rebase on Dec 19, 2019
  43. sidhujag referenced this in commit 33fe88ae5b on Dec 19, 2019
  44. in src/util/system.h:179 in 67518f7cc6 outdated
    174@@ -174,8 +175,9 @@ class ArgsManager
    175     /**
    176      * Get setting value.
    177      *
    178-     * Result will be null if setting was unset, true if "-setting" argument was passed
    179-     * false if "-nosetting" argument was passed, and a string if a "-setting=value"
    180+     * Result will be null if setting was unset, true if "-setting" argument
    181+     * was passed false if "-nosetting" argument was passed, and a string,
    


    promag commented at 10:30 pm on December 22, 2019:
    nit, comma before false?

    ryanofsky commented at 10:17 pm on January 6, 2020:

    re: #16545 (review)

    nit, comma before false?

    Thanks, fixed

  45. in src/util/system.cpp:231 in 67518f7cc6 outdated
    211+ * Return an error string and nullopt if an invalid value was provided that
    212+ * isn't allowed by the flags, otherwise return the parsed value.
    213  */
    214-static bool CheckValid(const std::string& key, const util::SettingsValue& val, unsigned int flags, std::string& error)
    215+static Optional<util::SettingsValue> InterpretValue(const std::string& key,
    216+    const std::string* value,
    


    promag commented at 11:42 pm on December 22, 2019:

    ryanofsky commented at 10:17 pm on January 6, 2020:

    re: #16545 (review)

    nit, I’ve changed this to Optional<string>, see promag@71da58b.

    Using an optional wrapper instead of a simple const pointer seems a little worse to me (less efficient, more verbose), but I wouldn’t object if there’s an advantage I’m not seeing.


    promag commented at 4:45 pm on January 9, 2020:
    Yeah don’t bother
  46. in src/util/system.h:166 in 67518f7cc6 outdated
    137-        ALLOW_BOOL = 0x01,
    138-        ALLOW_INT = 0x02,
    139-        ALLOW_STRING = 0x04,
    140-        ALLOW_ANY = ALLOW_BOOL | ALLOW_INT | ALLOW_STRING,
    141+    enum Flags : unsigned int {
    142+        ALLOW_ANY = 0x01,    //!< disable validation
    


    promag commented at 11:43 pm on December 22, 2019:
    AddArg could disallow any other flag when this one is used?

    ryanofsky commented at 10:17 pm on January 6, 2020:

    re: #16545 (review)

    AddArg could disallow any other flag when this one is used?

    Thanks, added checks for useless flag combinations in AddArg and tests for the checks in util_CheckBadFlagCombinations

  47. promag commented at 11:43 pm on December 22, 2019: contributor
  48. in src/util/system.h:180 in 67518f7cc6 outdated
    174@@ -174,8 +175,9 @@ class ArgsManager
    175     /**
    176      * Get setting value.
    177      *
    178-     * Result will be null if setting was unset, true if "-setting" argument was passed
    179-     * false if "-nosetting" argument was passed, and a string if a "-setting=value"
    180+     * Result will be null if setting was unset, true if "-setting" argument
    181+     * was passed false if "-nosetting" argument was passed, and a string,
    182+     * integer, or boolean depending on ALLOW_ flags if a "-setting=value"
    


    hebasto commented at 4:34 pm on January 3, 2020:
    0     * integer, or boolean depending on ALLOW_{BOOL|INT|STRING} flags if a "-setting=value"
    

    ryanofsky commented at 10:18 pm on January 6, 2020:

    re: #16545 (review)

    Thanks, included suggestion

  49. hebasto approved
  50. hebasto commented at 11:40 am on January 6, 2020: member

    ACK 67518f7cc61bf59ddfa0fd7c8dbbdec3653b9556, tested on Linux Mint 19.3.

    0$ ./src/test/test_bitcoin --run_test=util_tests/util_CheckValue
    1Running 1 test case...
    2
    3*** No errors detected
    
    0$ ./src/test/test_bitcoin --run_test=util_tests/CheckSingleValue
    1Running 1 test case...
    2
    3*** No errors detected
    

    There are two non-blocking suggestions:

    1. add some tests:
     0diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp
     1index 539e1a972..215057227 100644
     2--- a/src/test/util_tests.cpp
     3+++ b/src/test/util_tests.cpp
     4@@ -316,6 +316,8 @@ BOOST_FIXTURE_TEST_CASE(util_CheckValue, CheckValueTest)
     5     CheckValue(M::ALLOW_BOOL, "-value=1", Expect{true}.Bool(true));
     6     CheckValue(M::ALLOW_BOOL, "-value=2", Expect{{}}.Error("Can not set -value value to '2'. It must be set to 0 or 1"));
     7     CheckValue(M::ALLOW_BOOL, "-value=abc", Expect{{}}.Error("Can not set -value value to 'abc'. It must be set to 0 or 1"));
     8+    CheckValue(M::ALLOW_BOOL, "-value=true", Expect{{}}.Error("Can not set -value value to 'true'. It must be set to 0 or 1"));
     9+    CheckValue(M::ALLOW_BOOL, "-value=false", Expect{{}}.Error("Can not set -value value to 'false'. It must be set to 0 or 1"));
    10 
    11     CheckValue(M::ALLOW_INT, nullptr, Expect{{}}.DefaultInt().DefaultBool());
    12     CheckValue(M::ALLOW_INT, "-novalue", Expect{false}.Int(0).Bool(false));
    13@@ -371,6 +373,8 @@ BOOST_FIXTURE_TEST_CASE(util_CheckValue, CheckValueTest)
    14     CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-value=1", Expect{"1"}.String("1").DefaultBool());
    15     CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-value=2", Expect{"2"}.String("2").DefaultBool());
    16     CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-value=abc", Expect{"abc"}.String("abc").DefaultBool());
    17+    CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-value=true", Expect{"true"}.String("true").DefaultBool());
    18+    CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-value=false", Expect{"false"}.String("false").DefaultBool());
    19 
    20     CheckValue(M::ALLOW_STRING | M::ALLOW_LIST, nullptr, Expect{{}}.List({}));
    21     CheckValue(M::ALLOW_STRING | M::ALLOW_LIST, "-novalue", Expect{false}.List({}));
    
    1. move ArgManager class and related functions to its own file (in another PR)
  51. hebasto commented at 12:04 pm on January 6, 2020: member
    Is @MarcoFalke’s suggestion still relevant?
  52. ryanofsky force-pushed on Jan 7, 2020
  53. ryanofsky commented at 8:15 pm on January 7, 2020: contributor

    Updated 67518f7cc61bf59ddfa0fd7c8dbbdec3653b9556 -> 27beca5ffee664360005f74123dad9107769d048 (pr/argcheck.9 -> pr/argcheck.10, compare) with various suggested changes. Thanks for the reviews and feedback!

    re: #16545#pullrequestreview-338599308

    0+    CheckValue(M::ALLOW_BOOL, "-value=true", Expect{{}}.Error("Can not set -value value to 'true'. It must be set to 0 or 1"));
    1+    CheckValue(M::ALLOW_BOOL, "-value=false", Expect{{}}.Error("Can not set -value value to 'false'. It must be set to 0 or 1"));
    

    Thanks, added these checks in a new test (util_CheckBoolStringsNotSpecial).

    re: #16545 (comment)

    Is @MarcoFalke’s suggestion still relevant?

    Yes, the suggestion still applies to master branch and is now incorporated in this PR.

  54. in src/test/util_tests.cpp:409 in 27beca5ffe outdated
    404+    CheckValue(M::ALLOW_BOOL, "-value=false", Expect{{}}.Error("Can not set -value value to 'false'. It must be set to 0 or 1."));
    405+
    406+    // Similarly, check "true" and "false" are not treated specially when
    407+    // ALLOW_BOOL is combined with ALLOW_INT and ALLOW_STRING. (The only
    408+    // difference ALLOW_BOOL makes for int and string arguments is that it
    409+    // enables "-foo" syntax with no equal sign assiging explicit int or string
    


    hebasto commented at 8:48 pm on January 7, 2020:
    assiging ==> assigning

    ryanofsky commented at 9:50 pm on January 7, 2020:

    re: #16545 (review)

    assiging ==> assigning

    Thanks, fixed

  55. hebasto approved
  56. hebasto commented at 9:23 pm on January 7, 2020: member

    re-ACK 27beca5ffee664360005f74123dad9107769d048, tested on Linux Mint 19.3:

    0$ ./src/test/test_bitcoin --run_test=util_tests/util_CheckValue
    1Running 1 test case...
    2
    3*** No errors detected
    
    0$ ./src/test/test_bitcoin --run_test=util_tests/util_CheckBoolStringsNotSpecial
    1Running 1 test case...
    2
    3*** No errors detected
    
    0$ ./src/test/test_bitcoin --run_test=util_tests/util_CheckSingleValue
    1Running 1 test case...
    2
    3*** No errors detected
    
    0$ ./src/test/test_bitcoin --run_test=util_tests/util_CheckBadFlagCombinations
    1Running 1 test case...
    2
    3*** No errors detected
    
    0$ ./src/bitcoind -nolisten=0 -nowallet=0
    12020-01-07T21:22:29Z Warning: parsed potentially confusing double-negative -listen=0
    22020-01-07T21:22:29Z Warning: parsed potentially confusing double-negative -wallet=0
    3...
    
  57. ryanofsky force-pushed on Jan 7, 2020
  58. ryanofsky commented at 9:52 pm on January 7, 2020: contributor

    Thanks for re-ack!

    Updated 27beca5ffee664360005f74123dad9107769d048 -> 5bb512aa51ae46350f8527ff7b3817dd719bb455 (pr/argcheck.10 -> pr/argcheck.11, compare) with suggested spelling fix

  59. hebasto approved
  60. hebasto commented at 10:25 pm on January 7, 2020: member
    ACK 5bb512aa51ae46350f8527ff7b3817dd719bb455, just fix a typo.
  61. hebasto commented at 10:28 pm on January 7, 2020: member

    A note for future: it seems worth adding a functional test for “Warning: parsed potentially confusing double-negative …” in the debug.log.

    nm - it is already, see #17893

  62. ajtowns commented at 5:28 am on January 9, 2020: contributor
    Rather than changing foo= from being foo=0 to becoming foo=<whatever foo's default was> it might be clearer to write reset-foo for that behaviour (like -nofoo), so that it can be consistent no matter what the type of the setting is?
  63. ryanofsky commented at 7:24 pm on January 9, 2020: contributor

    Thanks for looking at this!

    Rather than changing foo= from being foo=0 to becoming foo=<whatever foo's default was>

    I don’t think that’s really an accurate description. -foo="" is currently interpreted as true for boolean arguments, 0 for integer arguments, and all kinds of random ways for string arguments. The PR doesn’t change this, or affect any existing behavior.

    What the PR does is implement flags that let the GetArg family of functions retrieve settings in a consistent way, so when you are trying to retrieve a setting that has a default value, you can just call a GetArg function and not have to deal with IsArgSet IsArgNegated or do manual parsing to get sensible behavior. After porting lots of existing arguments and dealing with bugs like #15864, I think that sensible behavior in most cases means treating -nofoo like false/0/"" for bool/int/string arguments, allowing -foo syntax for bool arguments but not most int/string arguments, and treating empty -foo="" settings like unset settings.

    These are just defaults, though. Since #15934, the internal representation of settings is always unambiguous and doesn’t throw away information, and callers should easily be able to implement any behavior they choose when interpretting settings.

    it might be clearer to write reset-foo for that behaviour (like -nofoo), so that it can be consistent no matter what the type of the setting is?

    I think this could be a reasonable feature to add in its own PR. Reset settings could be represented as null setting values. The GetArg functions already always treat null settings like unset settings, regardless of flags.

  64. in src/util/system.cpp:751 in 5bb512aa51 outdated
    504@@ -427,6 +505,16 @@ void ArgsManager::AddArg(const std::string& name, const std::string& help, unsig
    505     if (flags & ArgsManager::NETWORK_ONLY) {
    506         m_network_only_args.emplace(arg_name);
    507     }
    508+
    509+    if ((flags & (ALLOW_BOOL | ALLOW_INT | ALLOW_STRING)) && (flags & ALLOW_ANY)) {
    


    ajtowns commented at 2:35 pm on January 23, 2020:

    Seems like you might as well have a check for:

    0if ((flags & (ALLOW_BOOL | ALLOW_INT | ALLOW_STRING)) && (flags & ALLOW_LIST)) 
    

    here as well – if you add LIST, you can’t use GetArg to query for a string or int, or GetBoolArg to query for a bool value?


    ryanofsky commented at 8:02 pm on January 24, 2020:

    re: #16545 (review)

    Seems like you might as well have a check for:

    0if ((flags & (ALLOW_BOOL | ALLOW_INT | ALLOW_STRING)) && (flags & ALLOW_LIST)) 
    

    here as well – if you add LIST, you can’t use GetArg to query for a string or int, or GetBoolArg to query for a bool value?

    If the suggestion is make it impossible to accept lists of strings, I don’t see how that makes sense. If the suggestion is to make it impossible to accept lists of bools or ints, I don’t think that is a good idea. ArgsManager API for retrieving typed lists could be improved but it isn’t something that needs to be explicitly supported or forbidden in this PR


    ajtowns commented at 3:42 am on January 28, 2020:
    If you want to support lists of bools or ints later, then it’s easy to remove the restriction then; if lists of bools or ints is supported now, it should be tested along with the other cases in util_tests.cpp – but it doesn’t seem like it makes sense to say that it’s supported now when we’ve only got std::vector<std::string> GetArgs() to query it.

    ryanofsky commented at 11:47 pm on September 1, 2020:

    re: #16545 (review)

    If you want to support lists of bools or ints later, then it’s easy to remove the restriction then; if lists of bools or ints is supported now, it should be tested along with the other cases in util_tests.cpp – but it doesn’t seem like it makes sense to say that it’s supported now when we’ve only got std::vector<std::string> GetArgs() to query it.

    Am not inclined to add a new restriction that only complicates the implementation and doesn’t provide a benefit to end users or developers trying to use the API. There is no missing test coverage here either. Writing this would be just analogous to building a roadblock in front a road that hasn’t been built.

  65. in src/util/system.cpp:478 in 5bb512aa51 outdated
    473@@ -398,15 +474,17 @@ bool ArgsManager::SoftSetArg(const std::string& strArg, const std::string& strVa
    474 
    475 bool ArgsManager::SoftSetBoolArg(const std::string& strArg, bool fValue)
    476 {
    477-    if (fValue)
    478-        return SoftSetArg(strArg, std::string("1"));
    479-    else
    480-        return SoftSetArg(strArg, std::string("0"));
    481+    LOCK(cs_args);
    482+    CheckArgFlags(strArg, /* require= */ ALLOW_BOOL, /* forbid= */ 0, __func__);
    


    ajtowns commented at 2:36 pm on January 23, 2020:
    Seems like this should have /* forbid= */ ALLOW_LIST to match GetBoolArg ?

    ryanofsky commented at 8:02 pm on January 24, 2020:

    re: #16545 (review)

    Seems like this should have /* forbid= */ ALLOW_LIST to match GetBoolArg ?

    Added suggested restriction

  66. ajtowns commented at 4:15 pm on January 23, 2020: contributor

    What the PR does is implement flags that let the GetArg family of functions retrieve settings in a consistent way,

    The way I think of args at the moment is that ArgsManager stores a convenient representation of the config file (so it parses out the “-nofoo” stuff, but otherwise just stores strings) before it’s converted into typed info, and then the GetArg functions do that conversion so that the setting can actually be used, often dropping the result into a global/module variable so it doesn’t have to be repeatedly converted.

    I guess what I’ve got in mind is eventually ending up with something where args get statically declared along with the variable they should stored in, and parsing/merging the config files and command line just fills in the variables, so that instead of GetBoolArg('-addrmantest', false) you just evaluate g_addrmantest and get your answer more directly. I suppose that would mean instead of querying IsArgSet mean you’d use a std::optional<bool> instead of just bool etc; but otherwise it would give you compile time type checking and mean that you’d be writing the default values where you define the parameters rather than every place you try to access them. (I don’t know if this is all actually possible, it’s just what I’m hoping for)

    Anyway, with or without that context, I’m not really seeing that much value in adding the type information while ArgsManager is storing the info; it’s easy enough to say that "" should be interpreted as 0 if asked for as an int and true if asked for as a bool, but when you want to convert the "" to an int, and then convert that int to a bool because you’ve got ALLOW_INT | ALLOW_BOOL set, that starts becoming pretty weird. Weird enough that you’ve implemented it so that in that case -value= returns the defaults instead; but that’s then a user-visible behaviour change for anyone who was using -boolparam= to set it true if it’s default happens to be false when -boolparam is converted from ANY to BOOL.

    To try and fully understand your PR, I’ve split it up into bitesize chunks that make sense to me on their own – https://github.com/ajtowns/bitcoin/commits/202001-argsmanflag . It gets most of the PR done, but doesn’t do the coversion to int within SettingsValue generally, so keeps the same semantics for all the calls that wouldn’t pass an error back to the user. I’m not really seeing enough value in those different semantics to justify the change given the chance of breaking someone’s config isn’t zero…

  67. ajtowns commented at 4:15 pm on January 23, 2020: contributor
    Couple of potential nits
  68. ryanofsky commented at 7:02 pm on January 23, 2020: contributor

    What the PR does is implement flags that let the GetArg family of functions retrieve settings in a consistent way,

    The way I think of args at the moment is that ArgsManager stores a convenient representation of the config file (so it parses out the “-nofoo” stuff, but otherwise just stores strings) before it’s converted into typed info, and then the GetArg functions do that conversion so that the setting can actually be used, often dropping the result into a global/module variable so it doesn’t have to be repeatedly converted.

    I guess what I’ve got in mind is eventually ending up with something where args get statically declared along with the variable they should stored in, and parsing/merging the config files and command line just fills in the variables, so that instead of GetBoolArg('-addrmantest', false) you just evaluate g_addrmantest and get your answer more directly. I suppose that would mean instead of querying IsArgSet mean you’d use a std::optional<bool> instead of just bool etc; but otherwise it would give you compile time type checking and mean that you’d be writing the default values where you define the parameters rather than every place you try to access them. (I don’t know if this is all actually possible, it’s just what I’m hoping for)

    Yes, I have the same thing in mind. But again, like I said in #17580#pullrequestreview-340791706, switching from untyped settings to typed settings is the hard part because it has to do be done on a case-by-case basis (see wallet flag commits in the PR description). Switching from a dynamic representation of types to a static representation of types is a straightforward thing to do after types are in place.

    Anyway, with or without that context, I’m not really seeing that much value in adding the type information while ArgsManager is storing the info

    I’ve tried to clearly motivate this PR and the PRs that depend on it (#17493 #17580 #17581 and #17783). I’m not trying to convince everyone of everything, but I am happy to try to address specific concerns that can be articulated.

    it’s easy enough to say that "" should be interpreted as 0 if asked for as an int and true if asked for as a bool, but when you want to convert the "" to an int, and then convert that int to a bool because you’ve got ALLOW_INT | ALLOW_BOOL set, that starts becoming pretty weird. Weird enough that you’ve implemented it so that in that case -value= returns the defaults instead; but that’s then a user-visible behaviour change for anyone who was using -boolparam= to set it true if it’s default happens to be false when -boolparam is converted from ANY to BOOL.

    I’m having a hard time following this and I probably don’t know how to convince someone that something is weird or not weird. I put a lot of thought into the flags, and did a lot of experimentation with the wallet commits linked to in the description, and have a result that I think:

    • Provides good error and reporting useful behavior to end users
    • Does not require a boilerplate IsArgSet/IsArgNegated nonsense code in places where arguments are accessed
    • Is backwards compatible in all cases except some specific and obscure corner cases, and clearly documented where not backwards compatible

    To try and fully understand your PR, I’ve split it up into bitesize chunks that make sense to me on their own – https://github.com/ajtowns/bitcoin/commits/202001-argsmanflag . It gets most of the PR done, but doesn’t do the coversion to int within SettingsValue generally, so keeps the same semantics for all the calls that wouldn’t pass an error back to the user.

    Can you give a specific example of how this is an improvement? If you can say how it improves rhe wallet changes linked in the PR description, or any of the followup PRs actually making use of these flags, that would be most helpful.

    I’m not really seeing enough value in those different semantics to justify the change given the chance of breaking someone’s config isn’t zero…

    I’m sure you know this, but just so someone reading doesn’t get the wrong idea, this PR is 100% backwards compatible, and is only defining new type flags that are used in future improvements

  69. ajtowns commented at 9:04 am on January 24, 2020: contributor

    https://github.com/ajtowns/bitcoin/commits/202001-argsmanflag

    Can you give a specific example of how this is an improvement?

    It’s not so much meant as an improvement, but more as “your PR is X=A+B+C, the A+B parts make sense to me, but the C part doesn’t and even seems like a step in the wrong direction, and A+B alone seems to do what the PR was aiming to do – ie, implement missing error checking for ArgsManager flags”.

    I’ve added a couple more commits on top now, so that (a) it actually does more of the error checking for the flags and (b) it’s easy to see from the test suite that changing from ANY to BOOL etc only makes some things errors, rather than changing the meaning of user’s configurations. Both these veer more substantially away from your patch than where I was yesterday; consider it executable whiteboarding, I guess?

    I’m still uncomfortable with making working configs start giving errors, but changing the meaning of potentially already existing configs really needs a good justification that I’m not seeing. I realise this PR alone doesn’t do that, but it sets up future PRs to do so in ways that aren’t necessarily super-obvious.

    I think the approach I linked above would make it much more straightforward to switch away from ALLOW_ANY sooner, since it just means you can say “a bunch of things that were previously allowed in config files that weren’t at all sensible (double negatives, repeatedly setting the same option, giving strings where ints are needed or numbers other than 0 or 1 where a bool is needed, or saying “true” to set a bool to false) will now give errors at startup” and be done.

  70. ryanofsky commented at 12:50 pm on January 24, 2020: contributor

    AJ, this critique does not seem substantive because it is too general. Would it be possible for you to cite a specific deficiency in my design, or in any of my PRs, or in any of my wallet flag porting commits that lead to bad user behavior, or an unergnonomic API for developers? I’d be more than happy to adopt suggestions for improvement. Or if we disagree about a specific design decision, I could explain the tradeoffs behind it, and what made me favor one approach. But when you do things like casually state that something in my PR goes in the “wrong direction,” without even telling me what you’re referring to, it does not seem productive.

    Again, to be clear to anyone else reading this: This PR is 100% backwards compatible. It adds a new developer feature which I put a lot of thought and effort into designing, testing, and making use of in followup PRs and commits prioritizing 1) backwards compatibilty, 2) good error reporting for users, 3) flexibility for users about how to specify options, for example supporting -foo and -nofoo valueless syntax whenever not ambiguous 4) having ergonomic code at GetArg call sites and avoiding current ubiquitous IsArgNegated IsArgset bugs 5) having ability to add future improvements like custom validation functions and typed int/string/bool/vector<x>/Optional<x>/Variant<x> setting storage

    I’m pretty satisfied with design of this PR and happy with the way it functions by itself, but the actual test of the PR is when the features it adds are put to use in followup PRs. This PR by itself is a starting point that doesn’t set anything in stone. If it has deficiencies, they can be addressed now or addressed in future PRs, it’s not some hugely high stakes thing where everything has to be worked out in advance, even though I’ve definitely tried to work out everything in advance, and you can look at all the actual use cases in the followups to see that

  71. ryanofsky commented at 9:19 pm on January 24, 2020: contributor
    Updated 5bb512aa51ae46350f8527ff7b3817dd719bb455 -> f42da6bbe3a327a7abe9c29ac1278bd652e86513 (pr/argcheck.11 -> pr/argcheck.12, compare) with suggested softsetbool check
  72. ryanofsky force-pushed on Jan 24, 2020
  73. ajtowns commented at 4:02 am on January 28, 2020: contributor

    Would it be possible for you to cite a specific deficiency in my design, or in any of my PRs, or in any of my wallet flag porting commits that lead to bad user behavior, or an unergnonomic API for developers?

    I don’t know if I can cite a problem you’ll accept, your PRs are no-ops (“this change has no effect”) because you’re modifying dead code, and you seem to be rejecting criticisms that only take place once the code is used… I think:

    • making -value= reset things to defaults changes the meaning of existing configs, and while that might be fine if it were a new system, it doesn’t seem useful enough to justify introducing an incompatibility
    • you’re doing a lot more things in this patch than just implementing missing errors (and given this code doesn’t apply to any existing options, arguably aren’t implementing the missing errors yet!)
    • it would be better to have ALLOW_BOOL etc work exactly the same way as ALLOW_ANY; change the argument declarations types in the first PR; and then change the meaning of ALLOW_BOOL etc to provide better errors in follow up PRs; that makes it easy to see what the effects of the new behaviours you’re coding up actually is
    • rather than bit flags it would be better to have a type enumeration something like: BOOL, INT, STRING, OPTIONAL_STRING, STRING_LIST. At that point having unique “GetArg” functions for each type returning exactly the right type seems straightforward; so Optional<std::string> dbg_log = GetOptionalStringArg("-debuglogfile", DEFAULT_DEBUG_LOGFILE); m_print_to_file = dbg_log ? true : false; m_file_path = dbg_log ? *dbg_log : "";
    • that the IsArgNegated and IsArgSet warts aren’t a priority – they’re largely encapsulated in system.cpp

    EDIT: the first one of these prevents me from giving an ACK at least without seeing a good reason for it, the second one made it hard to review, the latter are just IMO

  74. in src/test/util_tests.cpp:504 in f42da6bbe3 outdated
    367+    CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-novalue=2", Expect{{}}.Error("Can not negate -value at the same time as setting value '2'."));
    368+    CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-novalue=abc", Expect{{}}.Error("Can not negate -value at the same time as setting value 'abc'."));
    369+    CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-value", Expect{true}.DefaultString().Bool(true));
    370+    CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-value=", Expect{""}.DefaultString().DefaultBool());
    371+    CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-value=0", Expect{"0"}.String("0").DefaultBool());
    372+    CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-value=1", Expect{"1"}.String("1").DefaultBool());
    


    ajtowns commented at 4:04 am on January 28, 2020:
    This seems very strange; I don’t see how having “value=1”, GetBoolArg("-value", false)==false for an arg that’s meant to accept a bool or a string could possibly make sense (likewise for value=0, with GetBoolArg("-value", true)==true).

    ryanofsky commented at 11:47 pm on September 1, 2020:

    re: #16545 (review)

    This seems very strange; I don’t see how having “value=1”, GetBoolArg("-value", false)==false for an arg that’s meant to accept a bool or a string could possibly make sense (likewise for value=0, with GetBoolArg("-value", true)==true).

    I think this is just strange if you are not thinking about use cases. The use case for BOOL|STRING is for /imperative/ options like -bind -connect -upgrade -reindex where you want naked options with no value to be accepted and you also want verbose options with string values to be accepted. The BOOL flag is what allows the naked option, and the STRING flag is what allows the string option. Just as with any other STRING option, you are responsible for parsing the string and ArgsManager is not going to interpret it for you. Making the change you suggest would require either choosing a new SettingsValue representation or making it impossible to to distinguish -setting from -setting="", and would make the implementation more complicated, and would have no actual utility.


    ajtowns commented at 3:12 am on August 20, 2021:

    -bind and -connect both support multiple options (require=STRING+LIST for GetArgs) but this is forbidden for GetBoolArg (forbid=LIST); -reindex, -reindexchainstate, and -rescan all don’t seem to be used as anything but a bool as far as I can see. The comments also mention -listen which also seems to be only used as a boolean arg. That does leave -upgradewallet (presuming that’s what -upgrade is referring to) though that option was replaced by an RPC a while ago.

    There’s no need to change the SettingsValue representation – if you can differentiate different values using GetBoolArg/GetArg, then you can differentiate them via some different api too.


    ryanofsky commented at 7:39 pm on August 21, 2021:

    re: #16545 (review)

    -bind and -connect

    We can go down a rabbit hole about these individual cases, but this is nitpicking which I do not think is relevant to the point I am making about imperative options (which trigger behavior and don’t require but can accept string values) being useful to handle differently from string options whose purpose is to set values.

    you can differentiate them via some different api

    Right, I do not want to change the API and I do not want to change the representation. The suggestion is not a good suggestion. Even if it was a good suggestion, it would expand the scope of the PR. Even if it was a good suggestion, it could done separately as a followup.


    ajtowns commented at 7:10 am on September 1, 2021:

    We can go down a rabbit hole about these individual cases, but this is nitpicking which I do not think is relevant to the point I am making about imperative options (which trigger behavior and don’t require but can accept string values)

    I went through all the examples I could find of potential “imperative options”, and none of them fit your definition, except for the one that’s been removed. I don’t think “this feature won’t be used at all so there’s no point implementing it” is nitpicking.

  75. DrahtBot added the label Needs rebase on Feb 5, 2020
  76. HashUnlimited referenced this in commit 7a97631cc5 on Apr 17, 2020
  77. HashUnlimited referenced this in commit 02a6fd02c4 on Apr 17, 2020
  78. HashUnlimited referenced this in commit 51d492e9c1 on Apr 17, 2020
  79. deadalnix referenced this in commit 619943a5c9 on Apr 30, 2020
  80. maflcko removed the label Tests on Aug 28, 2020
  81. ryanofsky force-pushed on Sep 2, 2020
  82. DrahtBot removed the label Needs rebase on Sep 2, 2020
  83. ryanofsky force-pushed on Sep 2, 2020
  84. ryanofsky force-pushed on Sep 2, 2020
  85. ryanofsky force-pushed on Sep 2, 2020
  86. ryanofsky force-pushed on Sep 2, 2020
  87. ryanofsky force-pushed on Sep 2, 2020
  88. sidhujag referenced this in commit 96245fcfb0 on Nov 10, 2020
  89. DrahtBot added the label Needs rebase on Jan 14, 2021
  90. in src/util/system.cpp:359 in 45beb3bc1a outdated
    355@@ -296,7 +356,7 @@ bool ArgsManager::ParseParameters(int argc, const char* const argv[], std::strin
    356 #endif
    357 
    358         if (key == "-") break; //bitcoin-tx using stdin
    359-        std::string val;
    360+        Optional<std::string> val;
    


    fanquake commented at 2:04 am on March 15, 2021:
  91. ryanofsky force-pushed on Mar 22, 2021
  92. DrahtBot removed the label Needs rebase on Mar 23, 2021
  93. ryanofsky force-pushed on Mar 24, 2021
  94. backpacker69 referenced this in commit 7f404e8d42 on Mar 28, 2021
  95. backpacker69 referenced this in commit 87096f79e1 on Mar 28, 2021
  96. backpacker69 referenced this in commit ba6f9db598 on Mar 28, 2021
  97. ryanofsky force-pushed on Mar 29, 2021
  98. DrahtBot added the label Needs rebase on Apr 20, 2021
  99. ryanofsky force-pushed on Jun 2, 2021
  100. DrahtBot removed the label Needs rebase on Jun 2, 2021
  101. ryanofsky commented at 0:59 am on August 19, 2021: contributor

    re: #16545 (comment)

    • making -value= reset things to defaults changes the meaning of existing configs

    No, there is impact no impact on existing settings, which are not consistent in how they treat these values. All this is doing is establishing a convention that lets new settings behave consistently and usefully in the future by default (if you want inconsistent behavior in the future, that is still possible using ALLOW_STRING and std::string::empty()).

    • you’re doing a lot more things in this patch than just implementing missing errors

    The only change in this PR is adding missing validation for argsmanager flags and raising new errors.

    • it would be better to have ALLOW_BOOL etc work exactly the same way as ALLOW_ANY

    I don’t want to change the meaning of the flag over many PRs. I want implement the flag once. If there is something unclear or bad about the behavior that’s being implemented, obviously that would be bad. But it would also be fixable. Clearly I’ve put in the effort here to write extensive tests and apply the flag to real world use-cases in the wallet (see table in PR description), so I don’t think we need to be paralyzed by speculative and vague concerns.

    • rather than bit flags it would be better to have a type enumeration

    I started off thinking similarly until I looked at actual use cases, again see the table in the PR description. There is a balance between having no validation and complete freedom, and rigid validation and not enough freedom to be used everywhere. This PR is taking a step of adding some validation, with a level of API freedom justified by existing use cases, again see table. If you need the API to be more rigid, you can make it more rigid in future PRs. If you need the API to have a different interface without changing the functionality, we can also do that in future PRs. I would like to make the API more ergonomic in the future and would like to check more things at compile time instead of runtime. But the API currently uses bit flags, and bit flags work fine, so this PR is continuing to use bit flags.

    • that the IsArgNegated and IsArgSet warts aren’t a priority – they’re largely encapsulated in system.cpp

    This is not the case. These APIs are widely misused and make the developer and users experiences worse. See actual usages 9c0e6975f42a1197a7b898b19d8af81a03868123 048a4ee606a370d51620e39aecc8f6d4a132c31c and commits in the table in the PR description.

    Coincidentally, in a PR I reviewed today there were two new incorrect set/negated usages: #20487 (review), #20487 (review) and I’m sure you could find more fresh examples of incorrect usages in open and merged PRs.

    EDIT: the first one of these prevents me from giving an ACK at least without seeing a good reason for it

    Again, the reason is just to provide consistent default behavior for future -value="" settings. Current settings are not affected. Future settings that want to override the behavior are not affected. Fixation on this case seems strange to me, and I think it is possibly standing in for some other concern (aesthetic differences? a different understanding of current conventions?) that has not been articulated.

  102. ryanofsky commented at 1:26 am on August 19, 2021: contributor
    Rebased 2772948473df34181535558f6b105ca413730053 -> 0a4ded27b7613656f423a07d889a9648b72200e7 (pr/argcheck.22 -> pr/argcheck.23, compare) adding a scripted-diff commit to actually ensure backwards compatibility. Updated 0a4ded27b7613656f423a07d889a9648b72200e7 -> aa3dec078fc464d7ea2b46088c789bfd4a7e2637 (pr/argcheck.23 -> pr/argcheck.24, compare) retaining “Negating of -%s is meaningless” error for compatibility with #19827.
  103. ryanofsky force-pushed on Aug 19, 2021
  104. ryanofsky force-pushed on Aug 19, 2021
  105. ryanofsky commented at 6:42 pm on August 19, 2021: contributor
    (deleted comment meant for other PR #19827 (comment))
  106. maflcko commented at 6:44 pm on August 19, 2021: member
    I looks like the scripted-diff does change behavior, given that at least ALLOW_INT has some additional checks attached?
  107. ryanofsky commented at 6:48 pm on August 19, 2021: contributor

    I looks like the scripted-diff does change behavior, given that at least ALLOW_INT has some additional checks attached?

    Yeah, it’s been a while I forgot this had dropped the “negating is meaningless” check in some cases. I’ll update the comment to say it avoids breaking existing configs, not that it avoids changing behavior entirely.

  108. ajtowns commented at 5:28 am on August 20, 2021: contributor

    Approach NACK aa3dec078fc464d7ea2b46088c789bfd4a7e2637

    re: #16545 (comment)

    • making -value= reset things to defaults changes the meaning of existing configs No, there is impact no impact on existing settings,

    There is no impact on existing settings because ALLOW_BOOL, ALLOW_INT, ALLOW_STRING and ALLOW_LIST are unused outside of tests, and that’s only because you’ve specifically converted them all to ALLOW_ANY. If they were to be used, which is presumably the idea, there would be an impact. If it’s not the idea to switch existing options from ALLOW_ANY to the more specific things – which will impact existing settings – then this approach just makes config handling more complicated because of the different behaviour between currently existing settings and the new ones.

    (I haven’t re-reviewed to see if there really isn’t an impact on existing settings given those changes)

    I started off thinking similarly until I looked at actual use cases, again see the table in the PR description.

    The only entry for the “(anything) or BOOL” types is no longer relevant as far as I can see, the only “LIST” type is “LIST of STRING”; and list options can only be access via GetArgs which returns a list of strings anyway. It was worth exploring using flags, but you were right the first time.

    • that the IsArgNegated and IsArgSet warts aren’t a priority – they’re largely encapsulated in system.cpp This is not the case. These APIs are widely misused and make the developer and users experiences worse. See actual usages 9c0e697

    I don’t agree with -nofoo sometimes being “override explicit config settings back to the default”, and particularly having -nosignetchallenge as another way of saying “default signet challenge” doesn’t make much sense to me. So I don’t find that commit super convincing…

    EDIT: the first one of these prevents me from giving an ACK at least without seeing a good reason for it

    Again, the reason is just to provide consistent default behavior for future -value="" settings. Current settings are not affected. Future settings that want to override the behavior are not affected. Fixation on this case seems strange to me, and I think it is possibly standing in for some other concern (aesthetic differences? a different understanding of current conventions?) that has not been articulated.

    I feel like I’ve been pretty verbose already (though if you want to chat further offline or something, I’m happy to) but as far as I can see you’re just ignoring my concerns. All you’re doing in this PR is implementing a new API for arguments, but you’re dismissing criticism of that API (vs the current one) as “too general” because, being unused, the API doesn’t impact “user behaviour”. You could defend gets() on the same grounds – it works great so long as it’s not actually used and you can always implement a better API later. So I really don’t know what else to say. I don’t think it’s a good idea to merge an API and then fix it later after you start trying to use it.

    respond to that depressing review comment

    I guess I’m sorry I looked at this PR. I like the idea of better typing of config options and improved error handling, and figured reviewing PRs about that was a positive step.

  109. ryanofsky referenced this in commit c53e8f0894 on Aug 21, 2021
  110. ryanofsky marked this as a draft on Aug 21, 2021
  111. ryanofsky commented at 8:37 pm on August 21, 2021: contributor

    Setting draft status. Will rebase this on #22766 soon to make this PR smaller and eliminate all confusion/questions/bugs related to backwards compatibility.

    Approach NACK aa3dec0

    I don’t think this NACK is justified. This PR is implementing an internal API. We disagree on minor points of the API design, notably some default behaviors which can be overridden by API callers. I think we both agree that the API will provide useful features to developers for providing clearer error messages and more consistency when parsing settings, both for new settings where there are not backwards compatibility concerns, and selectively for existing settings like #19827 (and maybe in examples from the PR description), where we think the usability benefits outweigh the compatibility concerns.

    In every case where the NACK has raised a specific concern with the API, I’ve always responded with specifics, and have also always reiterated the broad point that because this is an internal API, we can change it and make different choices in the future without ever having to affect end-user backwards compatibility in any case.

    There is no impact on existing settings […] this approach just makes config handling more complicated because of the different behaviour between currently existing settings and the new ones.

    Thanks for acknowledging there is no impact on existing settings. Now we can stop talking so much about backwards compatibility, and I can stop repeating I am not breaking backwards compatibility.

    At the end of this paragraph you are hinting about what the real concern may be, which is a concern about complexity and inconsistency. This is a very subjective thing to begin with, and I think it has never been substantiated with specific examples. It is also ignoring existing footguns and complexity which this PR is trying to address, see #17783 #17508 and #17493 for real examples. Even if any part of this concern is valid, it seems overblown when you try to stack up handwavy disagreements about internal design decisions with concrete improvements made and enabled by this PR.

    I don’t agree with -nofoo sometimes

    I suspect would not be useful to respond to this, but I can respond if you believe there are implications for this PR. You are saying ArgSet/Negated warts aren’t a priority. I am saying why I think they are worth fixing. I think we can agree to disagree about this if it is as tangential to the PR as it seems to be.

    dismissing criticism of that API (vs the current one) as “too general” because, being unused, the API doesn’t impact “user behaviour”.

    I defended the API design I chose for the default behavior of these flags AND I’m saying the default behavior does not affect exisitng options and can be overriden where needed. If we ever want to change the default API behavior in the future, we can do this in the future, and never have to break end-user compatibility. I would just like to try my approach to this API design here, which I think is thoughtful and grounded in real world use-cases.

    I guess I’m sorry I looked at this PR. I like the idea of better typing of config options and improved error handling, and figured reviewing PRs about that was a positive step.

    I should clarify I think it’s definitely a good thing that you’ve reviewed the PR, and the fault is totally mine for getting discouraged and failing to reply to comments promptly.

  112. ryanofsky force-pushed on Aug 23, 2021
  113. ryanofsky referenced this in commit 40243483de on Aug 24, 2021
  114. ryanofsky referenced this in commit eca00d8dfd on Aug 24, 2021
  115. ryanofsky referenced this in commit d5ab248929 on Aug 24, 2021
  116. ryanofsky referenced this in commit 4332297f62 on Aug 26, 2021
  117. ryanofsky referenced this in commit 3ffbb3b828 on Aug 26, 2021
  118. ryanofsky force-pushed on Aug 26, 2021
  119. ajtowns referenced this in commit ae72150769 on Sep 1, 2021
  120. ajtowns commented at 7:00 am on September 1, 2021: contributor

    I don’t think this NACK is justified.

    To summarise: 1) This PR doesn’t improve behaviour (in this case, actually issuing errors/warnings when people make mistakes in the config, or removing any of the footguns for any existing option), and we should be prioritising patches that do improve things. For example, an earlier version of this API has already spent over a year merged without getting us the intended benefits during that time, and instead has resulted in needing extra PRs to revert people trying to use it. That’s totally backwards. 2) When introducing a new API we should be using it immediately, both because if it’s better then using it is an immediate win, and because that demonstrates the new API actually works in practice, not just in theory. 3) I don’t think the new API as of the commit id I referenced is particularly great, since the obvious way to use it means existing options and new ones will behave differently in subtle and confusing ways.

    For comparison, #22766 changes a bunch less code and simplifies the API in a way that’s immediately used and (IMHO) obviously useful, so I’d much rather move the focus there than keep going in circles here. As far as I’m concerned that’s a good approach, this isn’t (well, wasn’t, as of the aforementioned commit id), hence the approach nack here, vs the ack there.

  121. ajtowns referenced this in commit e6f76cbabb on Sep 2, 2021
  122. ryanofsky referenced this in commit 313976dfa2 on Sep 15, 2021
  123. ryanofsky referenced this in commit 54cab91151 on Sep 27, 2021
  124. ryanofsky referenced this in commit 6e613a0a30 on Sep 27, 2021
  125. ryanofsky referenced this in commit 7ce0687ef1 on Oct 16, 2021
  126. ryanofsky referenced this in commit b8c069b7a9 on Oct 25, 2021
  127. ryanofsky referenced this in commit 5c428ceef6 on Oct 25, 2021
  128. fanquake referenced this in commit 3fc3641043 on Nov 1, 2021
  129. ryanofsky force-pushed on Nov 1, 2021
  130. ryanofsky commented at 7:27 pm on November 1, 2021: contributor
    Rebased aa3dec078fc464d7ea2b46088c789bfd4a7e2637 -> 9349a02cc41ea537873dd20636aff51e23870cbe (pr/argcheck.24 -> pr/argcheck.25, compare) due to conflict with #22217, on top of new base PR #22766 pr/argscripts.1 Rebased 9349a02cc41ea537873dd20636aff51e23870cbe -> c7ba71fb9165b1aae22d4b55a7f804223d5be3a0 (pr/argcheck.25 -> pr/argcheck.26, compare) on top #22766 pr/argscripts.4 Rebased c7ba71fb9165b1aae22d4b55a7f804223d5be3a0 -> 3345e119b07e6052ea0a6a0cfcc0186e6ddd3a06 (pr/argcheck.26 -> pr/argcheck.27, compare) after #22766 merge Updated 3345e119b07e6052ea0a6a0cfcc0186e6ddd3a06 -> 26f8e60dc6b6bb1d123822e336a93352e38fe148 (pr/argcheck.27 -> pr/argcheck.28, compare) with some minor commit and code cleanups, no changes in behavior. Rebased 26f8e60dc6b6bb1d123822e336a93352e38fe148 -> 770b99ea13d6800534f2aa8b6e65152a92272a94 (pr/argcheck.28 -> pr/argcheck.29, compare) due to various conflicts. Also improved commit messages and added a lot of documentation. Updated 770b99ea13d6800534f2aa8b6e65152a92272a94 -> e6565763879e92d3c17dca296966fbccf6991a83 (pr/argcheck.29 -> pr/argcheck.30, compare) changing TypedArg declaration to avoid signed/unsigned int conversion
  131. ryanofsky force-pushed on Dec 30, 2021
  132. DrahtBot added the label Needs rebase on Feb 9, 2022
  133. uvhw referenced this in commit 47d44ccc3e on Feb 14, 2022
  134. Stackout referenced this in commit caa30cc07d on Feb 17, 2022
  135. Stackout referenced this in commit c8377531b0 on Feb 17, 2022
  136. Stackout referenced this in commit b880790944 on Feb 17, 2022
  137. PastaPastaPasta referenced this in commit e79d73f0fc on Jun 7, 2022
  138. PastaPastaPasta referenced this in commit 78ac0e9361 on Jun 7, 2022
  139. PastaPastaPasta referenced this in commit d1187d70d1 on Jun 10, 2022
  140. PastaPastaPasta referenced this in commit 67f95cb60b on Jun 14, 2022
  141. ryanofsky force-pushed on Sep 22, 2022
  142. DrahtBot removed the label Needs rebase on Sep 22, 2022
  143. ryanofsky force-pushed on Sep 24, 2022
  144. DrahtBot added the label Needs rebase on Nov 15, 2022
  145. ryanofsky force-pushed on Nov 17, 2022
  146. DrahtBot removed the label Needs rebase on Nov 17, 2022
  147. knst referenced this in commit 814f197d85 on Apr 18, 2023
  148. knst referenced this in commit 39082d2565 on Apr 19, 2023
  149. DrahtBot added the label Needs rebase on Apr 21, 2023
  150. knst referenced this in commit 5289219191 on Apr 24, 2023
  151. UdjinM6 referenced this in commit bf9edc1f42 on Apr 25, 2023
  152. ryanofsky force-pushed on May 2, 2023
  153. DrahtBot removed the label Needs rebase on May 2, 2023
  154. DrahtBot added the label Needs rebase on May 9, 2023
  155. achow101 commented at 4:42 pm on September 23, 2023: member
    Are you still working on this? This was converted to draft 2 years ago, is it necessary to keep it as one?
  156. ryanofsky force-pushed on Sep 25, 2023
  157. DrahtBot removed the label Needs rebase on Sep 25, 2023
  158. DrahtBot added the label CI failed on Jan 24, 2024
  159. DrahtBot commented at 1:55 am on April 23, 2024: contributor

    🤔 There hasn’t been much activity lately and the CI seems to be failing.

    If no one reviewed the current pull request by commit hash, a rebase can be considered. While the CI failure may be a false positive, the CI hasn’t been running for some time, so there may be a real issue hiding as well. A rebase triggers the latest CI and makes sure that no silent merge conflicts have snuck in.

  160. maflcko commented at 5:59 am on July 17, 2024: member
    Are you still working on this, or do you want someone to pick it up?
  161. ryanofsky force-pushed on Jul 19, 2024
  162. ryanofsky commented at 11:17 pm on July 19, 2024: contributor

    re: #16545 (comment)

    Are you still working on this, or do you want someone to pick it up?

    I would like code review, and I probably need to respond to some old comments and questions. There are two parts to this PR:

    • The first part of this PR, implemented in the first commit, uses the ALLOW_BOOL, ALLOW_INT, and ALLOW_STRING flags to validate command line and config file settings, and save them into UniValue values as typed values, rather than strings.

    • The second part of this PR, implemented in the second commit, uses the ALLOW_BOOL, ALLOW_INT, ALLOW_STRING, and ALLOW_LIST flags to provide extra safety to the GetArg, GetArgs, GetBoolArg, and GetIntArg accessors. For example it prevents GetIntArg accessor from being called on non-int settings, and disables confusing legacy coercion behavior like making GetBoolArg return false for -setting=0 and true for -setting=1 but false for -setting=true and true for -setting=.

    I think the first commit is less controversial, but the second commit has been nitpicked because it is a developer API and developers have different preferences and concerns.

    Unfortunately, I don’t think it is safe to merge the first commit without the second commit, because the second commit disables unsafe coercions in the Get*Arg functions.

    But fortunately I do not think we need to nitpick and argue about behavior the Get*Arg functions because if any developer does not like the behavior of the Get*Arg functions they can call the GetSetting function instead which gives access to the underlying, validated UniValue setting and allows any developers to implement any behavior they would like to implement to parse their settings using the UniValue API. Also, because this second commit only affects an internal API, it is possible to change its behavior in the future, if necessary, without affecting users.

    I have done substantial work on top of this PR (in #16545 #17580 #17493 #17581 4719738f602a681eb0d1633fbb1651f42cc93129 7f7d82b521b7d78ea9eccab18f068e2881eefafc 6865a198f5db30bd494b3a2540f47ee728963908 51ca84ecfcad3b8b2a85d75c8c1da61d97c2ca9b and 012a320038158a532bd3662c99cd8211941659d2) porting real settings to use the API defined here, and I shaped the API around this work, and I believe if this PR could make its way beyond the nitpicking trap it is in, it would substantially improve user and developer experience and safety. Or, if I am wrong, it would fail to live up to this promise and will need subsequent improvements. Either way I think this PR is a positive step towards improving the current situation where settings are parsed in unsafe and surprising ways.

  163. DrahtBot removed the label CI failed on Jul 20, 2024
  164. maflcko commented at 8:59 am on July 22, 2024: member

    But fortunately I do not think we need to nitpick and argue about behavior the Get*Arg functions because if any developer does not like the behavior of the Get*Arg functions they can call the GetSetting function instead which gives access to the underlying, validated UniValue setting and allows any developers to implement any behavior they would like to implement to parse their settings using the UniValue API.

    I looked at the feedback so far, and I wonder if it could make sense to simplify the changes here for now to only allow an enum class (and not a flags field). The reason is that the controversy seems to stem from where flags are combined. Eg.

    0    CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-value=0", Expect{"0"}.String("0").DefaultBool());
    1    CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-value=1", Expect{"1"}.String("1").DefaultBool());
    

    (falling back to the default seems controversial).

    This way, flags can be implemented later (or implemented via GetSetting), if needed, but the simple stuff (like just a ALLOW_BOOL) should be easily usable in new options?

  165. in src/common/args.cpp:106 in 9196cfe26d outdated
    101@@ -102,6 +102,38 @@ KeyInfo InterpretKey(std::string key)
    102  *
    103  * @return parsed settings value if it is valid, otherwise nullopt accompanied
    104  * by a descriptive error string
    105+ *
    


    hodlinator commented at 2:33 pm on July 23, 2024:

    Ran make -j`nprocs` docs:

    Current: Screenshot from 2024-07-23 16-22-49 After applying…

     0diff --git a/src/common/args.cpp b/src/common/args.cpp
     1index fd9efc265f..26eeabf4e3 100644
     2--- a/src/common/args.cpp
     3+++ b/src/common/args.cpp
     4@@ -109,7 +109,7 @@ KeyInfo InterpretKey(std::string key)
     5  * helper methods can unambiguously determine original configuration strings
     6  * from the JSON values, and flexibly interpret settings and provide good error
     7  * feedback. Specifically:
     8- *
     9+ * \n
    10  * - JSON `null` value is never returned and is reserved for settings that were
    11  *   not configured at all.
    12  *
    

    ..we get: Screenshot from 2024-07-23 16-31-28


    ryanofsky commented at 7:33 pm on July 30, 2024:

    re: #16545 (review)

    Ran make -j`nprocs` docs:

    Thanks, should be fixed. Interesting that \n can be used to join the paragraphs.


    l0rinc commented at 12:56 pm on September 24, 2024:
    nit: I haven’t seen this style used here, it’s meant to keep the note’s formatting, right? (edit, I see @hodlinator already noticed the same, but now it’s cmake --build build --target docs) It seems to me we can achieve the same by removing the line completely:

    ryanofsky commented at 6:56 pm on September 25, 2024:

    re: #16545 (review)

    Happy to change, but I’m not really sure what to do here. I always just look at plaintext comments, not generated documentation, and I do think having breaks between list items makes the plaintext comment more readable since most of the items are paragraphs. I could get rid of the break before the first item while keeping the breaks before the other items, though, if that works and would be preferred.

  166. in src/common/args.h:442 in 9196cfe26d outdated
    436@@ -437,6 +437,13 @@ class ArgsManager
    437         const std::map<std::string, std::vector<common::SettingsValue>>& args) const;
    438 };
    439 
    440+//! Whether the type of the argument has been specified and extra validation
    441+//! rules should apply.
    442+inline bool TypedArg(uint32_t flags)
    


    hodlinator commented at 2:52 pm on July 23, 2024:

    Please clarify that this is not a constructor of some type, but rather checking a boolean condition.

    0inline bool IsTypedArg(uint32_t flags)
    

    ryanofsky commented at 7:33 pm on July 30, 2024:

    re: #16545 (review)

    Please clarify that this is not a constructor of some type, but rather checking a boolean condition.

    Agree that’s better. Updated.

  167. in src/common/args.cpp:152 in 9196cfe26d outdated
    143@@ -112,18 +144,75 @@ std::optional<common::SettingsValue> InterpretValue(const KeyInfo& key, const st
    144             error = strprintf("Negating of -%s is meaningless and therefore forbidden", key.name);
    145             return std::nullopt;
    146         }
    147+        if (TypedArg(flags)) {
    148+            if (value && *value != "1") {
    


    hodlinator commented at 3:03 pm on July 23, 2024:
    Care to insert a comment about why you are avoiding the untyped version’s InterpretBool(*value) here?

    ryanofsky commented at 7:30 pm on July 30, 2024:

    re: #16545 (review)

    Care to insert a comment about why you are avoiding the untyped version’s InterpretBool(*value) here?

    Good idea, added comment

  168. in src/common/args.h:158 in 9196cfe26d outdated
    156-     * false if "-nosetting" argument was passed, and a string if a "-setting=value"
    157-     * argument was passed.
    158+     * Result will be null if setting was unset, true if `-setting` argument was
    159+     * passed, false if `-nosetting` argument was passed, and a string, integer,
    160+     * or boolean depending on ALLOW_{BOOL|INT|STRING} flags if a
    161+     * `-setting=value` argument was passed. See \ref IntepretValue for an
    


    hodlinator commented at 3:31 pm on July 23, 2024:
    Typo: IntepretValue -> InterpretValue

    ryanofsky commented at 7:34 pm on July 30, 2024:

    re: #16545 (review)

    Typo: IntepretValue -> InterpretValue

    Thanks, fixed

  169. in src/test/argsman_tests.cpp:453 in 9196cfe26d outdated
    136@@ -137,16 +137,16 @@ class CheckValueTest : public TestChain100Setup
    137             BOOST_CHECK_EQUAL(test.GetArg("-value", "zzzzz"), "zzzzz");
    138         } else if (expect.string_value) {
    139             BOOST_CHECK_EQUAL(test.GetArg("-value", "zzzzz"), expect.string_value);
    140-        } else {
    141-            BOOST_CHECK(!success);
    142+        } else if (success) {
    143+            BOOST_CHECK_THROW(test.GetArg("-value", "zzzzz"), std::logic_error);
    


    hodlinator commented at 3:34 pm on July 23, 2024:
    This is a bit opaque. Worth adding a comment such as: // Non-string values should always throw on this, even if untyped. ? Not sure even that is correct though.

    ryanofsky commented at 7:39 pm on July 30, 2024:

    re: #16545 (review)

    This is a bit opaque.

    Thanks, that needed comments. The point of this is just to make sure that that coverage is complete, that anytime the test is not checking for expected GetArg() return values, GetArg() must be throwing and not returning anything.

  170. in src/common/args.cpp:149 in 9196cfe26d outdated
    143@@ -112,18 +144,75 @@ std::optional<common::SettingsValue> InterpretValue(const KeyInfo& key, const st
    144             error = strprintf("Negating of -%s is meaningless and therefore forbidden", key.name);
    145             return std::nullopt;
    146         }
    147+        if (TypedArg(flags)) {
    148+            if (value && *value != "1") {
    149+                error = strprintf("Can not negate -%s at the same time as setting value '%s'.", key.name, *value);
    


    hodlinator commented at 9:21 pm on July 23, 2024:

    Small adjustment to clarify that the specific value being set is not important.

    0                error = strprintf("Can not negate -%s at the same time as setting a value ('%s').", key.name, *value);
    

    ryanofsky commented at 7:39 pm on July 30, 2024:

    re: #16545 (review)

    Small adjustment to clarify that the specific value being set is not important.

    Nice suggestion. Applied.

  171. hodlinator commented at 9:56 pm on July 23, 2024: contributor

    Concept ACK 9196cfe26da4784029500da482c6f7d55fca5ac2

    Concept

    In #16545 (review) you say:

    Also this case really can and should be a compile error, not a runtime error.

    That’s along the lines of what I was going to bring up before reading the comments too. Hopefully this PR will take us further in that direction as you say.

    Commit messages

    Both commits mention not changing application behavior 2 times each, seems a bit much.

    Commit message 9196cfe26da4784029500da482c6f7d55fca5ac2

    and they be bypassed -> and they can be bypassed

    Behavior changes once types are applied

    This thread gives me some concern: #16545 (review)

    This seems very strange; I don’t see how having “value=1”, GetBoolArg("-value", false)==false for an arg that’s meant to accept a bool or a string could possibly make sense (likewise for value=0, with GetBoolArg("-value", true)==true).

    Implementing a BOOL|STRING example might help assuage some fears.

  172. ryanofsky force-pushed on Jul 30, 2024
  173. ryanofsky commented at 8:49 pm on July 30, 2024: contributor

    Updated 9196cfe26da4784029500da482c6f7d55fca5ac2 -> 29d932b18559a360f7fade67ad8172ff04f80345 (pr/argcheck.35 -> pr/argcheck.36, compare) adding an initial documentation commit, and a final test commit showing more realistic examples of how the flags are intended to be used. Also implemented all the suggestions from hodlinator’s very helpful review and made small tweaks based on the other changes.


    re: #16545 (comment)

    I looked at the feedback so far, and I wonder if it could make sense to simplify the changes here for now to only allow an enum class (and not a flags field)

    It’s an interesting idea, but I wonder if there’s any particular problem you think it would solve?

    I don’t see why it should be a problem to combine the ALLOW flags in general because the flags control which syntaxes are allowed, so combining the flags just allows more syntaxes.

    It is true that in most cases you don’t want to combine STRING, INT, and BOOL flags. The one case where it’s useful to combine BOOL with INT or STRING, is when you have some imperative setting that causes a new action to happen, and can take an optional int or string value. I think of settings like -ipcbind=<address> and -ipcconnect=<address> where you want to be able to accept an address to connect or bind to, so you want ALLOW_STRING. But you also want to the address to be optional and for -ipcbind and -ipcconnect to work so you need to add ALLOW_BOOL.

    I tried to document this better and also added a new example with -ipcbind in the last commit.


    re: #16545#pullrequestreview-2194180912

    Also this case really can and should be a compile error, not a runtime error.

    That’s along the lines of what I was going to bring up before reading the comments too. Hopefully this PR will take us further in that direction as you say.

    Yes, basically my idea for implementing this is in #22978, and there are other ideas listed there as well. This PR is parsing the settings into typed objects, but the types are determined at runtime because the settings are only defined at runtime when they are registered. If the types were available earlier before the settings are registered, it could be a compile error instead of a runtime error to call the wrong GetArg function.

    Both commits […]

    Thanks for the helpful suggestion and correction. Issues should be fixed now.

    Implementing a BOOL|STRING example might help assuage some fears.

    This is a good idea and it prompted me to add a new commit with more realistic examples of how the API is intended to be used. So hopefully that can help.

  174. in src/common/args.h:116 in 29d932b185 outdated
    118+     *   and does not require a value, add | ALLOW_BOOL to the flags. Adding it
    119+     *   just allows the setting to be specified alone on the command line
    120+     *   without a value, as "-foo" instead of "-foo=value".
    121+     *
    122+     * - Unless it makes no sense for your setting not to have a value, avoid
    123+     *   the DISALLOW_NEGATION and DISALLOW_ELISION flags, so the command line
    


    hodlinator commented at 9:06 pm on July 30, 2024:

    nit: Slightly decreased negation: Unless + avoid => Only

    0     * - Only use the DISALLOW_NEGATION and DISALLOW_ELISION flags when it does
    1     *   not make sense for your setting to have no value, so the command line
    

    ryanofsky commented at 1:05 pm on July 31, 2024:

    re: #16545 (review)

    nit: Slightly decreased negation: Unless + avoid => Only

    Good catch. Simplified to “Only use the DISALLOW_NEGATION flag if your setting really cannot function without a value, so the command line interface will be consistent and support negation generally.”

    I dropped reference to DISALLOW_ELISION since the elision flag is harmless to specify with type flags and just redundant. I also think it could be eliminated after this PR. There are only two current uses and they would both be better off replacing with ALLOW_STRING.

  175. in src/common/args.h:133 in 29d932b185 outdated
    135+     * | -foo=0   |   X    |  X  |  X   |      X      |    X     |
    136+     * | -foo=1   |   X    |  X  |  X   |      X      |    X     |
    137+     * | -foo     |        |     |  X   |      X      |    X     |
    138+     * | -foo=    |   X    |  X  |  X   |      X      |    X     |
    139+     * | -nofoo   |   X    |  X  |  X   |      X      |    X     |
    140+     * | -nofoo=1 |   X    |  X  |  X   |      X      |    X     |
    


    hodlinator commented at 9:11 pm on July 30, 2024:
    :heart: the table! Difference in -foo vs -nofoo is a bit surprising with X for STRING/INT if one isn’t used to “-nologfile” etc behavior which doesn’t set the arg back to default, but rather clears it.

    ryanofsky commented at 2:04 pm on July 31, 2024:

    re: #16545 (review)

    ❤️ the table! Difference in -foo vs -nofoo is a bit surprising with X for STRING/INT if one isn’t used to “-nologfile” etc behavior which doesn’t set the arg back to default, but rather clears it.

    Thanks! Just to make sure I understand the point, you are saying a user could mistakenly add -nologfile to their command line expecting it unset the logfile setting and change the log file path back to the default path. I agree this is possible but I feel that a more natural/common/correct interpretation of -nologfile is just that it would disable creation of a logfile.

    I do think the possibility of misinterpretation is something to keep in mind when implementing options. In this case I think documentation currently does a good job for -debuglogfile:

    0  -debuglogfile=<file>
    1       Specify location of debug log file (default: debug.log). Relative paths
    2       will be prefixed by a net-specific datadir location. Pass
    3       -nodebuglogfile to disable writing the log to a file.
    

    But maybe there could be a more general note in doc/bitcoin-conf.md or command line help about negated options, that for settings which are enabled by default, specifying “-nosetting” typically has effect of disabling the setting rather than unsetting it, and setting documentation may have more details.


    hodlinator commented at 9:23 pm on July 31, 2024:

    Just to make sure I understand the point, you are saying a user could mistakenly add -nologfile to their command line expecting it unset the logfile setting and change the log file path back to the default path.

    Yes. Thanks for helping me start to see the light of negated args. :)

  176. in src/common/args.h:257 in 29d932b185 outdated
    255+     *   requested type is not available or the setting is unspecifed or empty.
    256+     *
    257+     * - "Widening" type conversions from smaller to bigger types are done if
    258+     *   unambiguous (bool -> int -> string). For example, if settings.json
    259+     *   contains {"foo":123}, GetArg("-foo") will return "123". If it contains
    260+     *   {"foo":true}, GetIntArg("-foo") will return 1.
    


    hodlinator commented at 9:21 pm on July 30, 2024:

    Tested out this area a bit:

     0BOOST_AUTO_TEST_CASE(util_BoolJson)
     1{
     2    {
     3        ArgsManager args;
     4        args.LockSettings([&](common::Settings& settings) {
     5            settings.rw_settings["boolarg1"] = true;
     6            settings.rw_settings["boolarg2"] = false;
     7        });
     8        args.WriteSettingsFile();
     9
    10        std::ifstream file{args.GetDataDirBase() / "settings.json"};
    11        const std::string default_warning = strprintf("This file is automatically generated and updated by %s. Please do not edit this file while the node "
    12                                                "is running, as any changes might be ignored or overwritten.",
    13                                                PACKAGE_NAME);
    14        const std::string file_contents{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
    15        const std::string expected = "{\n"
    16            "    \"_warning_\": \""+default_warning+"\",\n"
    17            "    \"boolarg1\": true,\n"
    18            "    \"boolarg2\": false\n"
    19            "}\n";
    20        BOOST_CHECK_EQUAL(file_contents, expected);
    21    }
    22
    23    {
    24        ArgsManager args;
    25        args.AddArg("-boolarg1", "", ArgsManager::ALLOW_BOOL, OptionsCategory::OPTIONS);
    26        args.AddArg("-boolarg2", "", ArgsManager::ALLOW_BOOL, OptionsCategory::OPTIONS);
    27        args.ReadSettingsFile();
    28
    29        BOOST_CHECK_EQUAL(args.GetBoolArg("-boolarg1").value(), true);
    30        BOOST_CHECK_EQUAL(args.GetBoolArg("-boolarg2").value(), false);
    31    }
    32
    33    {
    34        ArgsManager args;
    35        args.AddArg("-boolarg1", "", ArgsManager::ALLOW_INT, OptionsCategory::OPTIONS);
    36        args.AddArg("-boolarg2", "", ArgsManager::ALLOW_INT, OptionsCategory::OPTIONS);
    37        args.ReadSettingsFile();
    38
    39        BOOST_CHECK_EQUAL(args.GetIntArg("-boolarg1").value(), 1);
    40        BOOST_CHECK_EQUAL(args.GetIntArg("-boolarg2").value(), 0);
    41    }
    42}
    

    Why not be more strict for typed args in settings.json, as JSON has distinct bool/number/string types?

    The fact that settings.json can contain {"foo":true} but that it gives a different result than -foo=true on the command line makes me sad, but I guess the latter will cause an error for typed bool args.


    ryanofsky commented at 3:31 pm on July 31, 2024:

    re: #16545 (review)

    0BOOST_AUTO_TEST_CASE(util_BoolJson)
    

    Nice test! Added a slightly simplified version as setting_args_allow_types test in new commit.

    Why not be more strict for typed args in settings.json, as JSON has distinct bool/number/string types?

    This PR is just applies type flags to command line and configuration file settings, but I think it would be be a natural followup for ArgsManager::ReadSettingsFile to start applying type flags to the settings.json file too. I don’t think it is essential to do it here because settings.json is mostly an internal file meant to be updated by the GUI and RPCs rather than users. And I’m reluctant to make this PR too much bigger. Even though the actual code changes here are small, there’s a lot of documentation and many tests, so the size could already be overwhelming.

    Assuming we will apply type flags to settings.json, there are some choices about how strict the type checking should be. For example if the json file specifies "foo": 123 for and “-foo” is an ALLOW_STRING option, we could treat it like parse error, or just treat it like rw_settings["foo"] = "123". Similarly, if the json file specifies "foo": "123" for and “-foo” is an ALLOW_INT option, we could treat it like parse error, or just treat it like rw_settings["foo"] = 123. Similar concerns apply for bool and string settings.

    The fact that settings.json can contain {"foo":true} but that it gives a different result than -foo=true on the command line makes me sad, but I guess the latter will cause an error for typed bool args.

    Right, I think it would be bad for those two things to be interpreted differently than each other. But if one is accepted and the other is rejected because JSON syntax is different than command line syntax, I think that’s ok. I also think if we want to support -foo=true syntax for ALLOW_BOOL options, we can do that, see my earlier comment about “-arg=0/1/false/true/yes/no/on/off” syntax


    hodlinator commented at 9:13 pm on July 31, 2024:
    I’m instinctively eager to add stricter type checking, but would probably necessitate some form of migration support for settings.json when activated, which risks devs messing up and frustrating users. Makes sense to not grow this PR too much. :+1:

    hodlinator commented at 9:18 pm on July 31, 2024:
    (Sorry about helping to cause CI issues with the test code).
  177. in src/common/args.h:276 in 29d932b185 outdated
    282      * Get setting value.
    283      *
    284-     * Result will be null if setting was unset, true if "-setting" argument was passed
    285-     * false if "-nosetting" argument was passed, and a string if a "-setting=value"
    286-     * argument was passed.
    287+     * Result will be null if setting was unspecified, true if `-setting`
    


    hodlinator commented at 9:28 pm on July 30, 2024:

    If paragraph at line 167 is correct:

    0     * Result will be null if setting was unspecified (or empty, `-setting=`), true if `-setting`
    

    ryanofsky commented at 3:47 pm on July 31, 2024:

    re: #16545 (review)

    If paragraph at line 167 is correct

    It is correct, but this paragraph is also correct, and suggested change would be incorrect. The other paragraph (“Since all settings are optional, it is useful to have a way to set them, and a way to unset them.”) is just describing a convention to treat empty settings as unset, which is implemented in the GetArg functions. But this is just a default convention that may not be appropriate to follow in every case, and can be overridden. The lower level GetSetting and InterpretValue functions don’t care about this convention and just represent empty strings as “”.

    Something similar happens with negated settings. The GetSetting and InterpretValue functions return false for negated settings, but GetArg functions decide how to interpret the false values, with default behavior that usually makes sense, but may not be appropriate in every case, and can be overridden.


    hodlinator commented at 9:33 pm on July 31, 2024:
    Alright, makes me wish the overall behavior was simpler somehow. But we have to work with what we’ve got. :+1:
  178. in src/common/args.h:217 in 0e124bbb36 outdated
    213@@ -219,6 +214,8 @@ class ArgsManager
    214     mutable fs::path m_cached_datadir_path GUARDED_BY(cs_args);
    215     mutable fs::path m_cached_network_datadir_path GUARDED_BY(cs_args);
    216 
    217+    bool CheckArgFlags(const std::string& name, uint32_t require, uint32_t forbid, const char* context) const;
    


    hodlinator commented at 10:06 pm on July 30, 2024:
    Why introduce this as protected instead of private?

    ryanofsky commented at 4:05 pm on July 31, 2024:

    re: #16545 (review)

    Why introduce this as protected instead of private?

    No reason, moved to private

  179. DrahtBot added the label CI failed on Jul 30, 2024
  180. DrahtBot commented at 10:12 pm on July 30, 2024: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/28126603473

    Make sure to run all tests locally, according to the documentation.

    The failure may 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.

  181. in src/test/util/setup_common.h:58 in 29d932b185 outdated
    53+}
    54+
    55+template <typename T>
    56+std::ostream& operator<<(std::ostream& os, const std::optional<T>& v)
    57+{
    58+    if (v) os << *v; else os << std::nullopt;
    


    hodlinator commented at 7:04 am on July 31, 2024:

    ryanofsky commented at 4:07 pm on July 31, 2024:

    re: #16545 (review)

    Thanks, applied suggestion

  182. in src/test/argsman_tests.cpp:205 in 29d932b185 outdated
    200+
    201+//! Return Options::load_block as a human readable string for easier testing.
    202+std::string LoadBlockStr(const Options& options)
    203+{
    204+    std::string ret;
    205+    for (const auto& block: options.load_block) {
    


    hodlinator commented at 7:15 am on July 31, 2024:
    meganit: Unlike L223 below, missing space before :.

    ryanofsky commented at 4:09 pm on July 31, 2024:

    #16545 (review)

    meganit: Unlike L223 below, missing space before :.

    Thanks, fixed!

  183. in src/test/argsman_tests.cpp:273 in 29d932b185 outdated
    268+    // Check passing -norpcserver makes it false.
    269+    BOOST_CHECK_EQUAL(ParseOptions({"-norpcserver"}).enable_rpc_server, false);
    270+    // Check passing -rpcserver=0 makes it false.
    271+    BOOST_CHECK_EQUAL(ParseOptions({"-rpcserver=0"}).enable_rpc_server, false);
    272+    // Check adding -rpcserver= sets it back to default.
    273+    BOOST_CHECK_EQUAL(ParseOptions({"-rpcserver=1", "-rpcserver="}).enable_rpc_server, true);
    


    hodlinator commented at 7:19 am on July 31, 2024:

    Forgot to replace value with non-default:

    0    BOOST_CHECK_EQUAL(ParseOptions({"-rpcserver=0", "-rpcserver="}).enable_rpc_server, true);
    

    ryanofsky commented at 4:13 pm on July 31, 2024:

    re: #16545 (review)

    Forgot to replace value with non-default:

    Good catch, fixed

  184. in src/test/argsman_tests.cpp:297 in 29d932b185 outdated
    334+    BOOST_CHECK_EQUAL(ParseOptions({"-logfile=custom.log"}).log_file, fs::path{"custom.log"});
    335+    // Check passing -nologfile makes it disables logging.
    336+    BOOST_CHECK_EQUAL(ParseOptions({"-nologfile"}).log_file, fs::path{});
    337+    // Check adding -logfile= sets it back to default.
    338+    BOOST_CHECK_EQUAL(ParseOptions({"-logfile=custom.log", "-logfile="}).log_file, fs::path{"debug.log"});
    339+    // Check passing invalid values.
    


    hodlinator commented at 7:25 am on July 31, 2024:

    Maybe also test this combo?

    0    // Check re-enabling the log file
    1    BOOST_CHECK_EQUAL(ParseOptions({"-nologfile", "-logfile="}).log_file, fs::path{"debug.log"});
    

    ryanofsky commented at 4:15 pm on July 31, 2024:

    re: #16545 (review)

    Maybe also test this combo?

    Good idea, added this.

  185. in src/test/argsman_tests.cpp:359 in 29d932b185 outdated
    353+    // Check default rescan value is unset.
    354+    BOOST_CHECK(!ParseOptions({}).wallet_rescan);
    355+    // Check passing -rescan makes it rescan from default height
    356+    BOOST_CHECK_EQUAL(ParseOptions({"-rescan"}).wallet_rescan.value().start_height, std::nullopt);
    357+    // Check passing -rescan=500000 makes it scan from specified height.
    358+    BOOST_CHECK_EQUAL(ParseOptions({"-rescan=500000"}).wallet_rescan.value().start_height, 500000);
    


    hodlinator commented at 7:49 am on July 31, 2024:

    Add?

    0    // Check passing -rescan=1 does not simply set the bool but treats it as a height.
    1    BOOST_CHECK_EQUAL(ParseOptions({"-rescan=1"}).wallet_rescan.value().start_height, 1);
    2    // Check passing -rescan=0 does not simply set the bool but treats it as a height.
    3    BOOST_CHECK_EQUAL(ParseOptions({"-rescan=0"}).wallet_rescan.value().start_height, 0);
    

    ryanofsky commented at 4:18 pm on July 31, 2024:

    re: #16545 (review)

    Add?

    Nice, added.

  186. hodlinator commented at 10:11 am on July 31, 2024: contributor

    Reviewed 29d932b18559a360f7fade67ad8172ff04f80345

    Thanks for refining the commit messages and breaking out one commit out of another + adding the new test commit!

    See you fixed a missing ALLOW_BOOL in GetBoolArg().

    From my prior review:

    This thread gives me some concern: #16545 (comment)

    This seems very strange; I don’t see how having “value=1”, GetBoolArg("-value", false)==false for an arg that’s meant to accept a bool or a string could possibly make sense (likewise for value=0, with GetBoolArg("-value", true)==true).

    I see your possible counterpoint - when we have BOOL|INT args, the bool component should only be set using simply “-arg” or “-noarg”, while “-arg=1” or “-arg=0” means setting the int component. It would be more consistent to make BOOL-only args disallow “-arg=1” or “-arg=0” but I understand if you don’t want to take that fight now.

  187. ryanofsky force-pushed on Jul 31, 2024
  188. ryanofsky commented at 5:28 pm on July 31, 2024: contributor

    Updated 29d932b18559a360f7fade67ad8172ff04f80345 -> 7c5cf89ca9bcb62e11cce1c9a711ebd8450d2be0 (pr/argcheck.36 -> pr/argcheck.37, compare) with review suggestions and other minor corrections like spelling fixes.

    Thanks again for the detailed review! I think I implemented all the suggestions except one.

    re: #16545#pullrequestreview-2208698806

    See you fixed a missing ALLOW_BOOL in GetBoolArg().

    Yes, the previous version did not require the ALLOW_BOOL flag to be set to call GetBoolArg(). This behavior was actually intended when the PR was written, because it was before GetArg functions returning std::optional were introduced in fc02f77ca604f0221171bfde3059b34f5d0fb1cd from #25290, so GetBoolArg() was useful as a way to see if an argument was set and nonempty, and it was always allowed to be called regardless of flags. But now that std::optional is available, there is no need for GetBoolArg() to be special.

    It would be more consistent to make BOOL-only args disallow “-arg=1” or “-arg=0” but I understand if you don’t want to take that fight now.

    Ideally, I would like to go the opposite direction and allow -arg=0/1/false/true/yes/no/on/off boolean syntax like git but the problem is InterpretBool() interprets all of these values except 1 as false right now, and probably there are existing configurations inadvertently relying on this behavior, so this change would have to be rolled out thoughtfully. One way to do it would be to first make it an error to use “true” “yes” or “on” to set false values, and make it a warning to specify other, unrecognized boolean values. Then after some time we could start interpreting and “true” “yes” or “on” as true and “false” “no” “off” as false and make it an error instead of a warning to specify unrecognized values. These changes could happen independently of this PR if we wanted to pursue them. But this PR just wants to interpret boolean values safely without changing the way they are normally specified.

  189. ryanofsky force-pushed on Jul 31, 2024
  190. ryanofsky commented at 6:17 pm on July 31, 2024: contributor
    Updated 7c5cf89ca9bcb62e11cce1c9a711ebd8450d2be0 -> c3a8536155afc2e402e864a60958696e9dc6137b (pr/argcheck.37 -> pr/argcheck.38, compare) to fix CI errors
  191. DrahtBot removed the label CI failed on Jul 31, 2024
  192. in src/common/args.h:133 in c3a8536155 outdated
    135+     * | -foo=0   |   X    |  X  |  X   |      X      |    X     |
    136+     * | -foo=1   |   X    |  X  |  X   |      X      |    X     |
    137+     * | -foo     |        |     |  X   |      X      |    X     |
    138+     * | -foo=    |   X    |  X  |  X   |      X      |    X     |
    139+     * | -nofoo   |   X    |  X  |  X   |      X      |    X     |
    140+     * | -nofoo=1 |   X    |  X  |  X   |      X      |    X     |
    


    hodlinator commented at 10:30 pm on July 31, 2024:

    Possible fixes for Doxygen issues I found:

    • Table generation failure due to | in headers. (Tried to surround headers with markdown backticks but couldn’t get it working that way).
    • Code blocks not being generated due to insufficient indentation.
    • File reference not becoming a link.
     0diff --git a/src/common/args.h b/src/common/args.h
     1index d36b491d0b..23ced9e3e3 100644
     2--- a/src/common/args.h
     3+++ b/src/common/args.h
     4@@ -121,16 +121,16 @@ public:
     5      * The ALLOW_STRING, ALLOW_INT, and ALLOW_BOOL flags control what syntaxes are
     6      * accepted, according to the following chart:
     7      *
     8-     * | Syntax   | STRING | INT | BOOL | STRING|BOOL | INT|BOOL |
     9-     * | -------- | ------ | --- | ---- | ----------- | -------- |
    10-     * | -foo=abc |   X    |     |      |      X      |          |
    11-     * | -foo=123 |   X    |  X  |      |      X      |    X     |
    12-     * | -foo=0   |   X    |  X  |  X   |      X      |    X     |
    13-     * | -foo=1   |   X    |  X  |  X   |      X      |    X     |
    14-     * | -foo     |        |     |  X   |      X      |    X     |
    15-     * | -foo=    |   X    |  X  |  X   |      X      |    X     |
    16-     * | -nofoo   |   X    |  X  |  X   |      X      |    X     |
    17-     * | -nofoo=1 |   X    |  X  |  X   |      X      |    X     |
    18+     * | Syntax   | STRING | INT | BOOL | STRING\|BOOL | INT\|BOOL |
    19+     * | -------- | :----: | :-: | :--: | :----------: | :-------: |
    20+     * | -foo=abc |   X    |     |      |      X       |           |
    21+     * | -foo=123 |   X    |  X  |      |      X       |     X     |
    22+     * | -foo=0   |   X    |  X  |  X   |      X       |     X     |
    23+     * | -foo=1   |   X    |  X  |  X   |      X       |     X     |
    24+     * | -foo     |        |     |  X   |      X       |     X     |
    25+     * | -foo=    |   X    |  X  |  X   |      X       |     X     |
    26+     * | -nofoo   |   X    |  X  |  X   |      X       |     X     |
    27+     * | -nofoo=1 |   X    |  X  |  X   |      X       |     X     |
    28      *
    29      * Once validated, settings can be retrieved by called GetSetting(),
    30      * GetArg(), GetIntArg(), and GetBoolArg(). GetSetting() is the most general
    31@@ -234,10 +234,10 @@ protected:
    32      *
    33      * Examples:
    34      *
    35-     *   GetArg("-foo") returns "abc" if -foo=abc was specified, or nullopt if unset
    36-     *   GetIntArg("-foo") returns 123 if -foo=123 was specified, or nullopt if unset
    37-     *   GetBoolArg("-foo") returns true if -foo was specified, or nullopt if unset
    38-     *   GetBoolArg("-foo") returns false if -nofoo was specified, or nullopt if unset
    39+     *     GetArg("-foo")     // returns "abc" if -foo=abc was specified, or nullopt if unset
    40+     *     GetIntArg("-foo")  // returns 123 if -foo=123 was specified, or nullopt if unset
    41+     *     GetBoolArg("-foo") // returns true if -foo was specified, or nullopt if unset
    42+     *     GetBoolArg("-foo") // returns false if -nofoo was specified, or nullopt if unset
    43      *
    44      * If no type flags (ALLOW_STRING, ALLOW_INT, or ALLOW_BOO* Table generation failure due to `|` in headersL) are set, GetArg
    45      * functions do many type coercions and can have surprising behaviors which
    46@@ -263,17 +263,17 @@ protected:
    47      *   handle boolean settings.json or command line values (-foo -nofoo)
    48      *   differently than string values (-foo=abc), you can write:
    49      *
    50-     *     if (auto foo{args.GetBoolArg("-foo")}) {
    51-     *         // handle bool foo.value()
    52-     *     } else if (auto foo{args.GetArg("-foo")}) {
    53-     *         // handle string foo.value()
    54-     *     } else {
    55-     *         // handle unset setting
    56-     *     }
    57+     *       if (auto foo{args.GetBoolArg("-foo")}) {
    58+     *           // handle bool foo.value()
    59+     *       } else if (auto foo{args.GetArg("-foo")}) {
    60+     *           // handle string foo.value()
    61+     *       } else {
    62+     *           // handle unset setting
    63+     *       }
    64      *
    65      * More examples of GetArg function usage can be found in the
    66      * [@ref](/bitcoin-bitcoin/contributor/ref/) example_options::ReadOptions() function in
    67-     * [@ref](/bitcoin-bitcoin/contributor/ref/) ../test/argsman_tests.cpp
    68+     * [@ref](/bitcoin-bitcoin/contributor/ref/) argsman_tests.cpp
    69      *@{*/
    70     std::optional<std::string> GetArg(const std::string& strArg) const;
    71     std::optional<int64_t> GetIntArg(const std::string& strArg) const;
    

    ryanofsky commented at 3:37 am on August 5, 2024:

    re: #16545 (review)

    Thank you! I should try building the docs myself, but I did apply your patch and added a @name to try to fix the grouping

  193. in src/common/args.h:227 in c3a8536155 outdated
    223@@ -152,12 +224,71 @@ class ArgsManager
    224     bool UseDefaultSection(const std::string& arg) const EXCLUSIVE_LOCKS_REQUIRED(cs_args);
    225 
    226  public:
    227+    ArgsManager();
    228+    ~ArgsManager();
    229+
    230+    /**
    


    hodlinator commented at 8:47 pm on August 1, 2024:
    This grouping isn’t fully working due to the lack of a group name: https://www.doxygen.nl/manual/grouping.html#memgroup
  194. ryanofsky force-pushed on Aug 5, 2024
  195. ryanofsky commented at 3:38 am on August 5, 2024: contributor
    Updated c3a8536155afc2e402e864a60958696e9dc6137b -> 1e37bcf9fc11562baaedea24685c31f60ef2de31 (pr/argcheck.38 -> pr/argcheck.39, compare) with suggested doxygen fixes and other tweaks
  196. DrahtBot added the label CI failed on Aug 5, 2024
  197. in src/test/argsman_tests.cpp:648 in 1e37bcf9fc outdated
    643+    CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-novalue=1", Expect{false}.String("").Bool(false));
    644+    CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-novalue=2", Expect{{}}.Error("Can not negate -value at the same time as setting a value ('2')."));
    645+    CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-novalue=abc", Expect{{}}.Error("Can not negate -value at the same time as setting a value ('abc')."));
    646+    CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-value", Expect{true}.DefaultString().Bool(true));
    647+    CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-value=", Expect{""}.DefaultString().DefaultBool());
    648+    CheckValue(M::ALLOW_STRING | M::ALLOW_BOOL, "-value=0", Expect{"0"}.String("0").DefaultBool());
    


    maflcko commented at 9:36 am on August 5, 2024:

    I fail to see how this is different from #30529 (comment)

    Why should -novalue and -value=0 silently behave differently? See git grep 'connect=0' and git grep 'noconnect'.

    I understand that this is a “refactor”, but what is the point of adding the features, when they can’t be applied to existing options, or applying them would be a silently breaking change. Merging this as-is and unlocking the footgun just seems too risky to me.

    This is why I asked to make this an enum class for now, disallowing combinations, so that they can be reviewed and added later on (if reviewers want them).

    Introducing a verification feature like ALLOW_BOOL in a minimal changeset would allow existing options (and new options) to use it, without having reviewers to go through all the other changes (and implicit implications here).

    I know that you disagree and want to introduce all features in one go all at once, but the fact that this pull request is sitting for more than half a decade without a single full review ACK could be a mild hint that the current state may not be the one that reviewers want to review?


    ryanofsky commented at 3:59 pm on August 5, 2024:

    re: #16545 (review)

    Marco if you look at my PR list, a half decade PR is fresh and new. But seriously, I think the reason this PR has been open so long is one reviewer gave a lot of pushback, and other potential reviewers were scared off, and I was previously very slow to update this and respond to comments. Given the previous state of the PR, even if other reviewers were interested in it, and wanted to help, and would agree with the PR’s approach after looking into it, it’s extremely unlikely any new reviewer would be invested enough in this topic to want read the long threads here and wade into an ongoing debate.

    As of last week though, I made major improvements to the PR adding a detailed documentation commit explaining the design of the API, and a new test commit that demonstrates features of the API and how they can realistically be used. The new tests are much different with than previous tests which exhaustively verified correctness of the implementation but did not try to show real use cases. So I’m ready to reengage on this PR and related issues #22978, #17508, #30529, #17783, #17581, #17580, #17493,

    To get to your specific concerns and suggestions, I understand why the approach you are suggesting seems appealing conceptually, but I think if you look at the details it will either (1) bake footguns and misfeatures into the API which will be harder to get rid in the long run or (2) result in a series of changes which cause unnecessary pain for users and also be unnecessarily confusing to developers.

    The appealing thing about your approach is it can let us apply type flags in bulk, and be assured that the flags will only restrict what settings values are allowed, without changing the way any setting value is interpreted once allowed. The problem with this approach is that the way a lot of current settings are interpreted is broken and crazy so I think it would be a mistake to introduce typed settings and either bake in bad design and behavior permanently, or force ourselves to need sweeping changes instead of targeted changes to fix bad behavior in the future.

    I think a better approach is to introduce flags which are internally coherent and actually support features we know we will use, and then roll the flags slowly, to one setting at a time, changing behavior of each setting once rather than multiple times, and documenting changes explicitly. I think this is a better approach because it will allow us to iterate and provide the best API for developers and best configuration interface for users in each specific case, and also provide the least confusing experience for upgrading and reasoning about backwards compatibility, because changes will be described concretely in terms of individual settings rather than abstractly about settings in bulk.

    Two more things:

    1. On -noconnect and -nolisten, I don’t think we have any disagreement and I don’t think anything in this PR adds any footguns or gets in the way of anything you would want to do. If you look at the new ExampleOptions test here, there is even a -nolisten setting implemented with these flags, and I think it looks pretty good but you can let me know if you disagree. Again though, please be aware there are two separate parts to this change because the flags control both (1) validation behavior and (2) behavior of the GetArg convenience functions. If there are disagreements about validation behavior, we should sort those out because there are are limited number of flags we should add and it may be difficult to change meaning of existing flags. But I do not think we should spend too much time disagreeing about behavior of GetArg convenience functions because they are just wrappers around GetSetting(). So if we find a situation where GetArg functions do not work well, or you want a different behavior, they are relatively easy to change or work around by introducing a new convenience function for your use-case.

    2. If you don’t like ALLOW_BOOL | ALLOW_STRING and think std::variant<bool, string> or std::variant<Disabled, std::string> would be better, I’m working on a change to implement that in #22978 (comment). Right now that change is implemented on top of this PR, but it could be implemented as a replacement for this PR so ALLOW_ flags don’t even need to exist. The nice thing about this PR being so old is that now we have C++20, so that change is much easier than implement than it would have been previously.


    maflcko commented at 9:58 am on August 7, 2024:

    I think a better approach is to introduce flags which are internally coherent and actually support features we know we will use, and then roll the flags slowly, to one setting at a time, changing behavior of each setting once rather than multiple times, and documenting changes explicitly.

    I agree, but I don’t think it is incompatible with my suggestion. (In fact, what you say in the quote is what I was trying to say myself). It seems possible to just implement bool error checking (and only that, or any other single feature), and make sure it is coherent and will be used, and then roll out the feature to one bool-only setting at a time, changing the behavior only once for this setting.

    This is what I meant when saying “allow existing options (and new options) to use it”, and I don’t think I mentioned or otherwise implied that the appeal of my suggestion is to “bulk change”.

    Personally, I just find it a bit hard to read the docs in this change (even if they are great and self-consistent) and then say “Yes, this makes sense in the context of all settings, their individual parsing (which may interact with their validation, their GetArg convenience function, as well as the surrounding code of the GetArg function), their historic and current usage, backwards-compatibility of user configs, and provides a way forward to apply to all settings eventually consistently)”.

    Whereas seeing the change for just bool-only settings could make it easier, but maybe I am wrong.


    ryanofsky commented at 1:29 am on August 23, 2024:

    re: #16545 (review)

    I agree, but I don’t think it is incompatible with my suggestion. It seems possible to just implement bool error checking (and only that, or any other single feature), and make sure it is coherent and will be used, and then roll out the feature to one bool-only setting at a time, changing the behavior only once for this setting.

    Thanks for pushing this, and that approach does sound ok to me. I think your advice to split up this PR is basically the same advice you are giving in #25665 to split up that PR, and my resistance in both PRs has been counterproductive. Resistance to break these up has been based on fear that because different parts of the PRs are designed to work together, that I may get review suggestions on the first part that sound good and I implement, but mess up the second part or make it less consistent. Most of the design of the util::Result class and these ArgsManager flags came from trial and error, using the APIs in real code, encountering things that didn’t work, and then updating APIs again in cycles until they seemed usable and complete. It was easy to come up with ideas that seemed appealing but didn’t generalize, and I was afraid it would be harder to recognize problematic changes if the PRs could not be complete. Also I felt like criticism of the PRs that was present was fairly surface level, so I was happy to wait for deeper reviews. But I guess it should be obvious more progress won’t happen by itself and I need to do more to make these changes accessible.


    maflcko commented at 1:40 pm on September 23, 2024:

    I think your advice to split up this PR is basically the same advice you are giving in #25665 to split up that PR, and my resistance in both PRs has been counterproductive.

    It is certainly useful to have a working end-result that reviewers can look at, than not to have one. However, given an end-result, splitting up stuff for review (where it makes sense on a case-by-case basis) should have no downside, because reviewers can confirm that they agree with the overall goal and that the stuff is directly moving toward that goal.

    Since you mentioned 25665: For example commit 5f49cb1bc8e6ba0671c21bf6292d2d3de43fd001 was split up more than a year ago and it was trivial to review it conformed to the end-goal and trivial to review by itself. However, 25665 is still sitting in a state with hundreds of comments (many of which are irrelevant due to changes in master, or only relevant for historic purposes). Using smaller chunks could also allow to have review comments live in smaller and closely related scopes (pull requests). For example, one can jump into #25977#issue-1359327472 and have a full list of all relevant comments around Result<void>, instead of having to read the “end-result” pull and weed through hundreds of comments, where less than 1% are relevant to the specific topic at hand.

  198. DrahtBot removed the label CI failed on Aug 6, 2024
  199. in src/test/util/setup_common.h:49 in 16fe0c5763 outdated
    44@@ -45,6 +45,23 @@ std::ostream& operator<<(typename std::enable_if<std::is_enum<T>::value, std::os
    45 {
    46     return stream << static_cast<typename std::underlying_type<T>::type>(e);
    47 }
    48+
    49+// Enable BOOST_CHECK_EQUAL for std::optional
    


    l0rinc commented at 3:35 pm on August 9, 2024:

    Pulled these to another PR: d1e3f0e (#30618)

    note1: nit: // Enable BOOST_CHECK_EQUAL for enum class types could be put inside the std now that we have multiple note2: we might want to move the remaining ones in the cpp file here as well (inline-ing them all, of course)


    l0rinc commented at 2:32 pm on September 23, 2024:
    Was merged since (added you as co-author), you should be able to remove this now
  200. in src/common/args.h:410 in d2c9af993a outdated
    424@@ -291,7 +425,6 @@ class ArgsManager
    425      * @return command-line argument (0 if invalid number) or default value
    426      */
    427     int64_t GetIntArg(const std::string& strArg, int64_t nDefault) const;
    


    hodlinator commented at 10:17 am on August 15, 2024:

    The example for ALLOW_INT | ALLOW_BOOL in 6865a198f5db30bd494b3a2540f47ee728963908 is a bit worrying.

     0diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
     1index 2f2931cef1..e34bb35b7c 100644
     2--- a/src/wallet/wallet.cpp
     3+++ b/src/wallet/wallet.cpp
     4@@ -3622,8 +3622,8 @@ std::shared_ptr<CWallet> CWallet::CreateWalletFromFile(interfaces::Chain& chain,
     5     int prev_version = walletInstance->GetVersion();
     6     if (gArgs.GetBoolArg("-upgradewallet", fFirstRun))
     7     {
     8-        int nMaxVersion = gArgs.GetArg("-upgradewallet", 0);
     9-        if (nMaxVersion == 0) // the -upgradewallet without argument case
    10+        int nMaxVersion = gArgs.GetArg("-upgradewallet", 1);
    11+        if (nMaxVersion == 1) // the -upgradewallet without argument case
    12         {
    13             walletInstance->WalletLogPrintf("Performing wallet upgrade to %i\n", FEATURE_LATEST);
    14             nMaxVersion = FEATURE_LATEST;
    

    (GetArg() taking an integer default value has been replaced in unrelated changes by GetIntArg(), the given example code is out of date but that’s beside the point).

    Maybe the GetIntArg() overload not taking a default value and returning an optional could be used here instead, as long as it returns nullopt for the -upgradewallet/ALLOW_INT | ALLOW_BOOL case. Better than suddenly requiring a different integer default (1 instead of 0 above).

    0     if (gArgs.GetBoolArg("-upgradewallet", fFirstRun))
    1     {
    2         std::optional<int64_t> nMaxVersion = gArgs.GetIntArg("-upgradewallet");
    3         if (!nMaxVersion) // the -upgradewallet without argument case
    4         {
    

    ryanofsky commented at 1:29 am on August 23, 2024:

    re: #16545 (review)

    The example for ALLOW_INT | ALLOW_BOOL in 6865a198f5db30bd494b3a2540f47ee728963908 is a bit worrying.

    Thanks, and yes that example is very out of date. I removed it from the PR description. Now that std::optional GetArg overloads exist, there is little reason to use the other GetArg functions, and their behavior has also been simplified. The way this would be written with the current PR would look more like the -rescan example from example_tests, and should be more readable:

    https://github.com/bitcoin/bitcoin/blob/1e37bcf9fc11562baaedea24685c31f60ef2de31/src/test/argsman_tests.cpp#L145-L153

  201. hodlinator commented at 11:42 am on August 15, 2024: contributor

    Concept ACK 1e37bcf9fc11562baaedea24685c31f60ef2de31

    The current state of things with no/ad-hoc validation of command line args since #22766 in Nov 2021 is not a good place to be. There can be a tendency to neglect stringent argument parsing on larger projects as long as the happy path of correctly specified args still works.

    Other more embryonic and radical initiatives exist to make args more strictly typed at compile time (#22978, and @ajtowns https://github.com/ajtowns/bitcoin/pull/8). I think the incremental approach of this PR + gradually activating it for existing args will achieve a better baseline to approach those future initiatives from.

    Approach concerns

    nit: d03b7e94cd4f89b46f658bb3f2afbb24b26e84fa - Title of the commit message could be “Add ArgsManager flags to parse and validate settings on startup” -> “Implement ArgsManager flags to parse and validate settings on startup” as it was the prior commit that uncommented the flags themselves.

    Beyond my inline comment regarding the ALLOW_INT | ALLOW_BOOL example, I think the next steps for me to reach an Approach A-C-K would be to try converting some existing complex args to be typed.

    Why this PR hasn’t seen more activity lately

    maflcko:

    I know that you [(ryanofsky)] disagree and want to introduce all features in one go all at once, but the fact that this pull request is sitting for more than half a decade without a single full review ACK could be a mild hint that the current state may not be the one that reviewers want to review? @maflcko I am grateful you pointed me to this PR based on a comment I made in another PR. Had I started on Bitcoin Core earlier, I would have pushed for some version of this sooner. I’ve been doing passes on the PR lately with ryanofsky in the hope of getting traction on it from more contributors.

    I think ryanofsky is correct in that a big factor in lacking reviews was the somewhat heated discussions earlier on.

    ryanofsky has been responsive throughout the PR lifetime and provided example commits to further help reviewers.

    Difficulty of review

    maflcko:

    I agree, but I don’t think it is incompatible with my suggestion. (In fact, what you say in the quote is what I was trying to say myself). It seems possible to just implement bool error checking (and only that, or any other single feature), and make sure it is coherent and will be used, and then roll out the feature to one bool-only setting at a time, changing the behavior only once for this setting.

    maflcko:

    Personally, I just find it a bit hard to read the docs in this change (even if they are great and self-consistent) and then say “Yes, …

    I second ryanofsky’s suggestion to look at the recently added ExampleOptions test in 16fe0c57637dbc83c569a407b1f36c30a24081f5 do help make things more real. Maybe focus on reviewing those before wading through the docs? But I agree it would make the PR easier to review if @ryanofsky could find ways to compress the comments.

    It is useful to evaluate a tentative complete version of the API. If the tactic of breaking the PR down into smaller steps is still a requirement for maflcko then maybe we should take a stab at it. If ryanofsky doesn’t feel like doing that then maybe I can volunteer (although he’d probably be 20x quicker).

    Wacky idea

    The number of args are finite (~270). To inspire confidence in this and later PRs, maybe one could build an --extended functional test harness that runs through all ways they might be used, in all expected valid variants (-norescan, -rescan, -rescan=500000, -rescan=0, -rescan=1), adding debug output where there isn’t some already in order to perform primitive log-based validation that the arg was applied in the expected way.

    If the C++ code were strictly organized with pairs of free functions with the first calling AddArg() and the second function reading args and applying them to each component’s options-struct, then it could even be run as unit tests, and might also take us one step closer to compile-time type safety for args.

  202. ryanofsky force-pushed on Aug 23, 2024
  203. ryanofsky commented at 1:50 am on August 23, 2024: contributor

    Updated 1e37bcf9fc11562baaedea24685c31f60ef2de31 -> 41bdf3d025f900a59ec14d5b497a31a2d84eea52 (pr/argcheck.39 -> pr/argcheck.40, compare) dropping support for flag combinations and fixing a commit message as suggested.

    Support for flag combinations is added back in commit df5c3e123227c1cd01a02b970f63c3682a1522d4 (branch) which could be a followup PR.


    Thanks maflcko and hodlinator for the very helpful feedback. I dropped support for flag combinations which should make this PR easier to merge and start using, if we want to to do that. In parallel, though I’m also working on a scripted-diff change based on #22978 (comment) that will associate C++ types with all existing ArgsManager options while being fully backwards compatible, to make the new behaviors implemented here easier to see and enable in specific cases, without having to think abstractly about flags. So if we can’t make progress here, we could have a another way of adding type checking soon that doesn’t require using the flags directly.


    re: #16545#pullrequestreview-2240110993

    Thank you very much for that summary and perspective!

    nit: d03b7e9 - Title of the commit message could be […]

    Thanks! Updated

    Wacky idea

    The number of args are finite (~270). To inspire confidence in this and later PRs, maybe one could build an --extended functional test harness that runs through all ways they might be used, in all expected valid variants (-norescan, -rescan, -rescan=500000, -rescan=0, -rescan=1), adding debug output where there isn’t some already in order to perform primitive log-based validation that the arg was applied in the expected way.

    This seems pretty feasible to implement, though I imagine a naive implementation could be pretty slow since there are a lot of ways to specify options. Seems ok for an extended test though, and I do think it’s worth thinking about how to test at least some options comprehensively even if not every option.

    If the C++ code were strictly organized with pairs of free functions with the first calling AddArg() and the second function reading args and applying them to each component’s options-struct, then it could even be run as unit tests, and might also take us one step closer to compile-time type safety for args.

    Would be curious about this idea if you can elaborate. I guess I’m not sure how you would want to check the result of the second function.

  204. DrahtBot added the label Needs rebase on Sep 4, 2024
  205. in src/common/args.cpp:689 in 41bdf3d025 outdated
    684+        throw std::logic_error(strprintf("Bug: bad %s flags. ALLOW_INT flag is incompatible with ALLOW_STRING "
    685+                                         "(any valid integer is also a valid string)", arg_name));
    686+    }
    687+    if ((flags & ALLOW_BOOL) && (flags & (ALLOW_INT | ALLOW_STRING))) {
    688+        throw std::logic_error(strprintf("Bug: bad %s flags. ALLOW_BOOL flag may not currently be specified with ALLOW_INT or ALLOW_STRING "
    689+                                         " (integer and string argument values cannot currently be omitted)", arg_name));
    


    hodlinator commented at 8:51 pm on September 19, 2024:

    nit: Remove double spaces

    0        throw std::logic_error(strprintf("Bug: bad %s flags. ALLOW_BOOL flag is incompatible with DISALLOW_ELISION "
    1                                         "(boolean arguments should not require argument values)", arg_name));
    2    }
    3    if ((flags & ALLOW_INT) && (flags & ALLOW_STRING)) {
    4        throw std::logic_error(strprintf("Bug: bad %s flags. ALLOW_INT flag is incompatible with ALLOW_STRING "
    5                                         "(any valid integer is also a valid string)", arg_name));
    6    }
    7    if ((flags & ALLOW_BOOL) && (flags & (ALLOW_INT | ALLOW_STRING))) {
    8        throw std::logic_error(strprintf("Bug: bad %s flags. ALLOW_BOOL flag may not currently be specified with ALLOW_INT or ALLOW_STRING "
    9                                         "(integer and string argument values cannot currently be omitted)", arg_name));
    

    ryanofsky commented at 3:42 pm on September 23, 2024:

    re: #16545 (review)

    nit: Remove double spaces

    Thanks! Applied suggestion

  206. in src/common/args.h:274 in 5a27b423fc outdated
    269+     *           // handle -foo or -nofoo bool in foo.value()
    270+     *       } else if (auto foo{args.GetArg("-foo")}) {
    271+     *           // handle -foo=abc string in foo.value()
    272+     *       } else {
    273+     *           // handle unset setting
    274+     *       }
    


    hodlinator commented at 12:28 pm on September 23, 2024:
    Feels like this is encouraging BOOL | STRING usage and should be broken out too if we are postponing that support.

    ryanofsky commented at 3:45 pm on September 23, 2024:

    re: #16545 (review)

    Feels like this is encouraging BOOL | STRING usage and should be broken out too if we are postponing that support.

    Good catch. Makes sense to drop the example since it doesn’t work yet.

  207. hodlinator commented at 12:53 pm on September 23, 2024: contributor

    git range-diff master 1e37bcf 41bdf3d

    Adds code disallowing combining ALLOW_BOOL with ALLOW_INT and ALLOW_STRING. Removes now invalid tests and adds 2 BOOST_CHECK_THROW()s.

    Curious to hear maflcko’s thoughts now that this change was made (a month ago). Happy to see if I can shake some stuff out first through.

    Making the applying of arguments testable

    hodlinator:

    If the C++ code were strictly organized with pairs of free functions with the first calling AddArg() and the second function reading args and applying them to each component’s options-struct, then it could even be run as unit tests, and might also take us one step closer to compile-time type safety for args.

    ryanofsky:

    Would be curious about this idea if you can elaborate. I guess I’m not sure how you would want to check the result of the second function.

    Rough existing example:

     0struct BlockManagerOpts {
     1    const CChainParams& chainparams;
     2    uint64_t prune_target{0};
     3    bool fast_prune{false};
     4    const fs::path blocks_dir;
     5    Notifications& notifications;
     6};
     7
     8class BlockManager
     9{
    10    ...
    11    using Options = kernel::BlockManagerOpts;
    12    ...
    13};
    14
    15util::Result<void> ApplyArgsManOptions(const ArgsManager& args, BlockManager::Options& opts)
    16{
    17    // block pruning; get the amount of disk space (in MiB) to allot for block & undo files
    18    int64_t nPruneArg{args.GetIntArg("-prune", opts.prune_target)};
    19    if (nPruneArg < 0) {
    20        return util::Error{_("Prune cannot be configured with a negative value.")};
    21    }
    22    uint64_t nPruneTarget{uint64_t(nPruneArg) * 1024 * 1024};
    23    if (nPruneArg == 1) { // manual pruning: -prune=1
    24        nPruneTarget = BlockManager::PRUNE_TARGET_MANUAL;
    25    } else if (nPruneTarget) {
    26        if (nPruneTarget < MIN_DISK_SPACE_FOR_BLOCK_FILES) {
    27            return util::Error{strprintf(_("Prune configured below the minimum of %d MiB.  Please use a higher number."), MIN_DISK_SPACE_FOR_BLOCK_FILES / 1024 / 1024)};
    28        }
    29    }
    30    opts.prune_target = nPruneTarget;
    31
    32    if (auto value{args.GetBoolArg("-fastprune")}) opts.fast_prune = *value;
    33
    34    return {};
    35}
    

    Only 2/5 of the fields are actually set by ApplyArgsManOptions, and prune_target is even being read from first. Ideally the function prototype would be closer to:

    0util::Result<BlockManager::Options> DeriveBlockManagerOptsFromArgs(const ArgsManager& args, const CChainParams& chainparams, int default_prune_target, const fs::path& blocks_dir, Notifications& notifications);
    

    and should also have a separate:

    0void RegisterBlockManagerOptsArgs(const ArgsManager& args)
    1{
    2    args.AddArg("-fastprune", "Use smaller block files and lower minimum prune height for testing purposes", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
    3    args.AddArg("-prune=<n>", strprintf("Reduce storage requirements by enabling pruning (deleting) of old blocks. This allows the pruneblockchain RPC to be called to delete specific blocks and enables automatic pruning of old blocks if a target size in MiB is provided. This mode is incompatible with -txindex. "
    4            "Warning: Reverting this setting requires re-downloading the entire blockchain. "
    5            "(default: 0 = disable pruning blocks, 1 = allow manual pruning via RPC, >=%u = automatically prune block files to stay under the specified target size in MiB)", MIN_DISK_SPACE_FOR_BLOCK_FILES / 1024 / 1024), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
    6}
    

    Then one could create tests such as:

     0BOOST_AUTO_TEST_CASE(util_datadir)
     1{
     2	ArgsManager args;
     3	RegisterBlockManagerOptsArgs(args);
     4
     5	{
     6		args.ForceSetArg("-fastprune", false);
     7		BlockManagerOpts& opts = DeriveBlockManagerOptsFromArgs(args, ...).value();
     8		BOOST_CHECK_EQUAL(opts.prune_target, 1);
     9		BOOST_CHECK_EQUAL(opts.fastprune, false);
    10		...
    11	}
    12
    13	{
    14		args.ForceSetArg("-fastprune", true);
    15		BlockManagerOpts& opts = DeriveBlockManagerOptsFromArgs(args, ...).value();
    16		BOOST_CHECK_EQUAL(opts.prune_target, 0);
    17		BOOST_CHECK_EQUAL(opts.fastprune, true);
    18		...
    19	}
    20
    21	...
    22}
    

    Validating all possible states of Options-objects based off possible args would be a lot of work/code. But it would have a lot shorter run-time than my initial impulse to basically re-run bitcoind for every validation-pass and add debug logging for any arg that doesn’t have it already to enable validation.

    Getting closer to ACK exploration

    Myself from above:

    … I think the next steps for me to reach an Approach A-C-K would be to try converting some existing complex args to be typed.

    I tried making all args in bitcoin-cli.cpp/SetupCliArgs() into typed (ALLOW_BOOL, ALLOW_INT, ALLOW_STRING) and exercising 11 out of 21.

    Omitting -regtest for brevity along with most well-behaving cases.

    -version

    0₿ bitcoin-cli -noversion
    1Bitcoin Core RPC client version v27.99.0-41bdf3d025f9-dirty
    2...
    

    ⚠️ Wrongly prints out version information. I’m itching to apply DISALLOW_NEGATION to it. That contradicts the comment being added to args.h by this PR:

    0     * - Only use the DISALLOW_NEGATION flag if your setting really cannot
    1     *   function without a value, so the command line interface will generally
    2     *   support negation and be more consistent.
    

    ⚠️ (bitcoin-cli -version=0) also prints version information. Might be nice to have generalized code for disallowing at least -version=0-usage (REQUIRE_ELISION?), but could be done in a follow-up.

    -conf

    0₿ bitcoin-cli -noconf getblockcount
    1error: Bug: Can't call GetArg on arg -rpcport registered with flags 0x00000204 (requires 0x8, disallows 0x10)
    

    Oops. Updated -rpcport handling:

     0             } // else, no port was provided in rpcconnect (continue using default one)
     1         }
     2 
     3-        if (std::optional<std::string> rpcport_arg = gArgs.GetArg("-rpcport")) {
     4+        if (std::optional<int64_t> rpcport_arg = gArgs.GetIntArg("-rpcport")) {
     5             // -rpcport was specified
     6-            const uint16_t rpcport_int{ToIntegral<uint16_t>(rpcport_arg.value()).value_or(0)};
     7+            const uint16_t rpcport_int = static_cast<uint64_t>(rpcport_arg.value()) > std::numeric_limits<uint16_t>::max() ? 0 : rpcport_arg.value();
     8             if (rpcport_int == 0) {
     9                 // Uses argument provided as-is
    10                 // (rather than value parsed)
    

    Again:

    0₿ bitcoin-cli -noconf getblockcount
    10
    

    Hm… Tempted to apply DISALLOW_NEGATION to -conf here as well, but I guess skipping having a configuration file is allowable. It turns out -conf uses the only place currently calling IsArgNegated outside of tests:

    0fs::path ArgsManager::GetPathArg(std::string arg, const fs::path& default_value) const
    1{
    2    if (IsArgNegated(arg)) return fs::path{};
    

    Outer context is:

     0bool ArgsManager::ReadConfigFiles(std::string& error, bool ignore_invalid_keys)
     1{
     2    {
     3        LOCK(cs_args);
     4        m_settings.ro_config.clear();
     5        m_config_sections.clear();
     6        m_config_path = AbsPathForConfigVal(*this, GetPathArg("-conf", BITCOIN_CONF_FILENAME), /*net_specific=*/false);
     7    }
     8
     9    const auto conf_path{GetConfigFilePath()};
    10    std::ifstream stream{conf_path};
    11
    12    // not ok to have a config file specified that cannot be opened
    13    if (IsArgSet("-conf") && !stream.good()) {
    14        error = strprintf("specified config file \"%s\" could not be opened.", fs::PathToString(conf_path));
    15        return false;
    16    }
    17    // ok to not have a config file
    18    if (stream.good()) {
    

    Stepped through in the debugger, and the code creates an std::ifstream targeting the directory /home/hodlinator/.bitcoin instead of a config file. stream.good() returns true. No errors are generated by Bitcoin Core. User facing behavior is acceptable but what the code does should probably be considered a minor bug independent of the PR. 🚨

    -datadir

    Similar behavior as -conf.

    0₿ bitcoin-cli -nodatadir getblockcount
    10
    

    🚨 No error for -nodatadir. Arguably we cannot run without a datadir. DISALLOW_NEGATION would be appropriate and conform with its doc-string.

    -generate

    This takes multiple optional positional integer arguments and so falls out of the scope of the PR.

    -addrinfo

    0₿ bitcoin-cli -regtest -addrinfo=foo
    1Error parsing command line arguments: Can not set -addrinfo value to 'foo'. It must be set to 0 or 1.
    

    ✅ Okay across the board.

    -getinfo

    0₿ bitcoin-cli -getinfo
    1Chain: regtest
    2Blocks: 4
    3Headers: 4
    4...
    

     0₿ bitcoin-cli -getinfo=
     1{
     2  "version": 279900,
     3  "blocks": 4,
     4  "headers": 4,
     5  "verificationprogress": 1,
     6  "timeoffset": 0,
     7  "connections": {
     8    "in": 0,
     9    "out": 0,
    10    "total": 0
    11  },
    12...
    

    🆗 Suddenly the input is JSON just by appending ‘=’?! Seems like we end up with a different *RequestHandler.

     0₿ bitcoin-cli -nogetinfo
     1{
     2  "version": 279900,
     3  "blocks": 4,
     4  "headers": 4,
     5  "verificationprogress": 1,
     6  "timeoffset": 0,
     7  "connections": {
     8    "in": 0,
     9    "out": 0,
    10    "total": 0
    11  },
    

    ⚠️ Feels wrong. (Same result with bitcoin-cli -nogetinfo=1).

    0₿ bitcoin-cli -nogetinfo=
    1Error parsing command line arguments: Can not negate -getinfo at the same time as setting a value ('').
    

    🆗 (Same behavior with bitcoin-cli -nogetinfo=0).

    -netinfo

    Okay across the board.

    -color

    Okay across the board. Reflection: -color implicitly depends on -getinfo, should probably be explicit in doc-string, and possibly warn when not used in combination. ⚠️

    -named

    Okay across the board.

    -rpcclienttimeout

    0₿ bitcoin-cli -regtest -rpcclienttimeout=foo getblockcount
    1Error parsing command line arguments: Can not set -rpcclienttimeout value to 'foo'. It must be set to an integer.
    

    Okay across the board.

    -rpcconnect

    Could benefit from DISALLOW_NEGATION:

    0₿ bitcoin-cli -regtest -norpcconnect getblockcount
    1error: timeout on transient error: Could not connect to the server :18443 (error code 1 - "EOF reached")
    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.
    

    -rpccookiefile

    Generally okay, could make an effort to write out the attempted absolute file path:

    0₿ bitcoin-cli -regtest -rpccookiefile=.cookie_wrong getblockcount
    12024-09-23T11:43:19Z ThreadRPCServer incorrect password attempt from 127.0.0.1:60906
    2error: Could not locate RPC credentials. No authentication cookie could be found, and RPC password is not set.  See -rpcpassword and -stdinrpcpass.  Configuration file: (/home/hodlinator/.bitcoin/bitcoin.conf)
    

    The rest

    Didn’t try -rpcpassword, -rpcport, -rpcuser, -rpcwait", -rpcwaittimeout, -rpcwallet, -stdin", -stdinrpcpass", -stdinwalletpassphrase as STRING/INT/BOOL types have all been tried and the above felt like enough of a sample.

    Reflections

    While this PR does not make everything air-tight by itself, it’s a step in the right direction and doesn’t seem to introduce any issues.

    DISALLOW_NEGATION too restrictive

    As experience with -noversion and -nogetinfo suggests, there may be a case for disallowing negation through the flag even in cases where the arg doesn’t take a value, instead of having custom code for each such case - which does not match the quoted doc-string being added.

    optional<*> Get*Arg(str) not a panacea

    (Closely related but independent of the actual changes in the PR). Being able to pass in default values makes for more readable code:

    0                 ParseError(error, strPrint, nRet);
    1             }
    2         } else if (gArgs.GetBoolArg("-addrinfo", false)) {
    3         // Verbose: } else if (auto addrinfo{gArgs.GetBoolArg("-addrinfo")}; addrinfo && *addrinfo) {
    4             rh.reset(new AddrinfoRequestHandler());
    5         } else {
    6             rh.reset(new DefaultRequestHandler());
    
  208. maflcko commented at 1:53 pm on September 23, 2024: member

    Curious to hear maflcko’s thoughts now that this change was made (a month ago). Happy to see if I can shake some stuff out first through.

    This needs rebase and is still in draft, which is why I haven’t taken another look for now. Though, I may take another look later.

  209. ryanofsky commented at 3:13 pm on September 23, 2024: contributor

    Thanks for @hodlinator and @maflcko for the comments.I’ll go ahead and update this PR in case marco is interested in looking at it since dropping support for flag combinations (https://github.com/bitcoin/bitcoin/pull/16545#pullrequestreview-2256044988).

    In the meantime though I’m slowly working on the branch in #22978. Compared to this PR, that approach will be a larger change. This PR adds only ~100 lines if you exclude tests and documentation by only adding flags while not change existing code. By contrast #22978 will get rid of flags everywhere all at once. Both changes are refactorings that don’t change any current behavior, and both changes implement the same semantics for setting validation and retrieval. I think the approach in #22978 might be more appealing than this one because C++ developers should be more comfortable with type declarations like std::variant<bool, std::string> than equivalent declarations with flags like ALLOW_BOOL | ALLOW_STRING. Also the approach in #22978 provides better compile time checking, links setting registration & retrieval so it is easier to unit test, and will have native support for options structs, so it will provide other benefits seem to be interested in, not just basic runtime type checking added in this PR.

    re: #16545#pullrequestreview-2316714013

    • Thanks for clarifying your testing idea. The fact that most settings are registered in a global SetupServerArgs function does make unit testing awkward, so your idea to break up AddArg calls makes sense and #22978 tries to do something very similar pairing setting registration and retrieval methods.

    • On weird -noversion -version=0 -nodatadir behavior. I think these settings are just implemented badly because current IsArgSet behavior is confusing and frequently misused. I’d like to drop that method, and #30529 removes a number of uses, but there are many more that can be dropped, especially in combination with this PR. I also agree disallowing negation in these cases is ok, though personally I think it’s also fine if -noversion or -version=0 is just treated as a normal boolean option and the last option value is used. The comment about disallow_negation is just hoping to make the interface more consistent and discourage special cases, but of course not every case is the same, and some case ares special.

  210. ryanofsky force-pushed on Sep 23, 2024
  211. ryanofsky commented at 4:02 pm on September 23, 2024: contributor

    Rebased 41bdf3d025f900a59ec14d5b497a31a2d84eea52 -> 87b6d4cb5a5c04f6c8542c633c3bfa5f76901d43 (pr/argcheck.40 -> pr/argcheck.41, compare) due to conflicts with #29043 and #30618

    Followup adding support for argument combinations is commit c919f51c044f0e80ecd301f9c9d396ddff2331ba (branch)

  212. DrahtBot removed the label Needs rebase on Sep 23, 2024
  213. DrahtBot added the label CI failed on Sep 23, 2024
  214. in src/common/args.cpp:153 in 87b6d4cb5a outdated
    144@@ -113,18 +145,77 @@ std::optional<common::SettingsValue> InterpretValue(const KeyInfo& key, const st
    145             error = strprintf("Negating of -%s is meaningless and therefore forbidden", key.name);
    146             return std::nullopt;
    147         }
    148+        if (IsTypedArg(flags)) {
    149+            // If argument is typed, only allow negation with no value or with
    150+            // literal "1" value. Avoid calling InterpretBool and accepting
    151+            // other values which could be ambiguous.
    152+            if (value && *value != "1") {
    153+                error = strprintf("Can not negate -%s at the same time as setting a value ('%s').", key.name, *value);
    


    l0rinc commented at 12:13 pm on September 24, 2024:
    nit: you’re using both can not and cannot in the errors

    ryanofsky commented at 6:57 pm on September 25, 2024:

    re: #16545 (review)

    nit: you’re using both can not and cannot in the errors

    Seems like cannot is better so switched to that, thanks!

  215. in src/common/args.cpp:104 in 87b6d4cb5a outdated
    102@@ -103,6 +103,38 @@ KeyInfo InterpretKey(std::string key)
    103  *
    104  * @return parsed settings value if it is valid, otherwise nullopt accompanied
    


    l0rinc commented at 12:55 pm on September 24, 2024:
    0 * [@return](/bitcoin-bitcoin/contributor/return/) parsed settings value if it is valid, otherwise `nullopt` accompanied
    

    ryanofsky commented at 6:32 pm on September 25, 2024:

    re: #16545 (review)

    Thanks, applied suggestion

  216. l0rinc commented at 1:09 pm on September 24, 2024: contributor

    C++ developers should be more comfortable with type declarations like std::variant<bool, std::string> than equivalent declarations with flags like ALLOW_BOOL | ALLOW_STRING

    std::variant is basically a poor-man’s-try-monad, until we can use std::expected in C++23.


    I started reviewing the PR, but rereading your comment I’m not sure whether you want to modify this part twice - because I’m all for a more functional approach instead of playing with flags (did a similar refactor in #30906).

    So, are you planning on migrating this PR away from flags, or should we review this, knowing that in a future PR you’ll likely remove them?

  217. DrahtBot removed the label CI failed on Sep 24, 2024
  218. common: Grammar / formatting tweaks
    Implement cleanup suggestions from l0rinc:
    
    https://github.com/bitcoin/bitcoin/pull/16545#discussion_r1773221823
    https://github.com/bitcoin/bitcoin/pull/16545#discussion_r1773291395
    https://github.com/bitcoin/bitcoin/pull/16545#discussion_r1775897382
    20a9986751
  219. doc: Add detailed ArgsManager type flag documention
    This commit just adds documentation for the type flags. The flags are actually
    implemented in the following two commits.
    30385f2976
  220. common: Implement ArgsManager flags to parse and validate settings on startup
    This commit implements support for new ALLOW_BOOL, ALLOW_INT, ALLOW_STRING, and
    ALLOW_LIST flags by validating settings with these flags earlier on startup and
    providing detailed error messages to users.
    
    The new flags implement stricter error checking than ALLOW_ANY. For example, a
    double negated option like -nosetting=0 is treated like an error instead of
    true, and an unrecognized bool value like -setting=true is treated like an
    error instead of false. And if a non-list setting is assigned multiple times in
    the same section of a configuration file, the later assignments trigger errors
    instead of being silently ignored.
    
    The new flags also provide type information that allows ArgsManager
    GetSettings() and GetSettingsList() methods to return typed integer and boolean
    values instead of unparsed strings.
    
    The changes in this commit have no effect on current application behavior
    because the new flags are only used in unit tests. The existing ALLOW_ANY
    checks in the argsman_tests/CheckValueTest confirm that no behavior is changing
    for current settings, which use ALLOW_ANY.
    a7a35ed080
  221. common: Update ArgManager GetArg helper methods to work better with ALLOW flags
    Update GetArg, GetArgs, GetBoolArg, and GetIntArg helper methods to work
    conveniently with ALLOW_BOOL, ALLOW_INT, and ALLOW_STRING flags.
    
    The GetArg methods are convenience wrappers around the GetSetting method. The
    GetSetting method returns the originally parsed settings values in their
    declared bool/int/string types, while the GetArg wrappers provide extra
    type-coercion and default-value fallback features as additional conveniences
    for callers.
    
    This commit makes two changes to GetArg, GetArgs, GetBoolArg, and GetIntArg
    helper methods when BOOL/INT/STRING flags are used:
    
    1. GetArg methods will now raise errors if they are called with inconsistent
       flags. For example, GetArgs will raise a logic_error if it is called on a
       non-LIST setting, GetIntArg will raise a logic_error if it is called
       on a non-INT setting.
    
    2. GetArg methods will now avoid various type coersion footguns when they are
       called on new BOOL/INT/STRING settings. Existing ALLOW_ANY settings are
       unaffected. For example, negated settings will return "" empty strings
       instead of "0" strings (in the past the "0" strings caused strangeness like
       "-nowallet" options creating wallet files named "0"). The new behaviors are
       fully specified and checked by the `CheckValueTest` unit test.
    
    The ergonomics of the GetArg helper methods are subjective and the behaviors
    they implement can be nitpicked and debated endlessly. But behavior of these
    helper methods does not dictate application behavior, and they can be bypassed
    by calling GetSetting and GetSettingList methods instead. If it's necessary,
    behavior of these helper methods can also be changed again in the future.
    
    The changes have no effect on current application behavior because the new
    flags are only used in unit tests. The `setting_args` unit test and ALLOW_ANY
    checks in the `CheckValueTest` unit test are unchanged and confirm that
    `GetArg` methods behave the same as before for ALLOW_ANY flags (returning the
    same values and throwing the same exceptions).
    a4e8ed267a
  222. test: Add tests to demonstrate usage of ArgsManager flags
    Co-authored-by: Hodlinator <172445034+hodlinator@users.noreply.github.com>
    59c449e034
  223. test: Add test for settings.json parsing with type flags
    The type flags aren't currently used to validate or convert settings in the
    settings.json file, but they should be in the future. Add test to check current
    behavior that can be extended when flags are applied.
    
    Co-authored-by: Hodlinator <172445034+hodlinator@users.noreply.github.com>
    4090dcf6cf
  224. ryanofsky referenced this in commit 0e3aedf80e on Sep 25, 2024
  225. ryanofsky force-pushed on Sep 25, 2024
  226. ryanofsky commented at 7:17 pm on September 25, 2024: contributor

    Updated 87b6d4cb5a5c04f6c8542c633c3bfa5f76901d43 -> 5a945600451037693a032e6df94f99a666dd581f (pr/argcheck.41 -> pr/argcheck.42, compare) with suggested formatting & grammar tweaks.

    re: #16545#pullrequestreview-2325050760

    Thanks for the review. To be clear, I would be very happy to see this PR reviewed and merged as is, even though I am working on other related changes in #22978, and am also happy to proceed with them if this PR is stuck.

    std::variant is basically a poor-man’s-try-monad, until we can use std::expected in C++23.

    It is true that std::variant is a poor substitute for std::expected in cases where std::expected is useful. But std::variant is more general than std::expected and provides a tagged union type / sum type that’s useful for other things besides error handling, such as representing settings in different states.

    I started reviewing the PR, but rereading your comment I’m not sure whether you want to modify this part twice

    Changes here and in the experimental branch in #22978 should be basically orthogonal.

    This PR is implementing new validation and parsing behavior so users can have better feedback about invalid settings, and so support for ALLOW_LIST can be added in #17580 and used to remove various confusing merge behaviors (see list of followups in the description.

    The experimental branch in #22978 doesn’t implement new behavior. It just hides (and will eliminate but doesn’t currently eliminate) the ALLOW_BOOL ALLOW_INT ALLOW_STRING ALLOW_LIST flags, replacing them with equivalent C++ type expressions. If this PR were merged before the changes in that branch, behavior would not be changing twice, it would just be moving with superficial changes from the InterpretValue function here to a new ParseSetting function there.

    If you are interested in the validation and parsing behavior implemented in this PR, or the followups listed in the description, it’s probably worth reviewing this without waiting for the experimental branch to be ready. But if you are less interested in the way settings are validated and parsed, and more interested in being able to represent setting types as C++ type expressions like std::vector<std::string> instead of ALLOW_STRING | ALLOW_LIST, then the branch will be more interesting than this PR. The branch also provides more compile time checking and support for Options structs that should be convenient for developers.

    So, are you planning on migrating this PR away from flags, or should we review this, knowing that in a future PR you’ll likely remove them?

    The branch will replace flags combinations with C++ type expressions as part of a larger migration from AddArgs() calls to Setting<> declarations that I hope to implement as a scripted diff. The parsing and validation implemented here which is controlled by the flags will not change. Replacing flags with C++ type expressions is a pretty superficial change, even though it does enable replacing runtime checks with compile-time ones.

  227. in src/common/args.cpp:152 in 5a94560045 outdated
    144@@ -113,18 +145,77 @@ std::optional<common::SettingsValue> InterpretValue(const KeyInfo& key, const st
    145             error = strprintf("Negating of -%s is meaningless and therefore forbidden", key.name);
    146             return std::nullopt;
    147         }
    148+        if (IsTypedArg(flags)) {
    149+            // If argument is typed, only allow negation with no value or with
    150+            // literal "1" value. Avoid calling InterpretBool and accepting
    151+            // other values which could be ambiguous.
    152+            if (value && *value != "1") {
    


    l0rinc commented at 7:34 pm on September 25, 2024:
    seems to me we’re treating the input value as an implicit optional, even though originally it’s an actual std::optional<std::string>. Given that we’re already returning an std::optional, can we consider avoiding pointers and using a std::optional<std::string_view> value parameter instead?

    ryanofsky commented at 3:11 pm on October 2, 2024:

    re: #16545 (review)

    (note: This comment is about previous code unaffected by this PR)

    seems to me we’re treating the input value as an implicit optional, even though originally it’s an actual std::optional<std::string>.

    Absolutely yes. In C and C++ T* is semantically equivalent to std::optional<T&>.

    Given that we’re already returning an std::optional, can we consider avoiding pointers and using a std::optional<std::string_view> value parameter instead?

    Agree std::optional<std::string_view> value would be slightly better than const std::string* here but I’d be reluctant to change it in this PR, because I don’t want to expand the PR to change the function signature and callers when they aren’t already changing for a minor cleanup.


    l0rinc commented at 5:53 pm on October 2, 2024:
    The change seems small, since we’re already converting an optional variable into a pointer parameter (seems simpler to just pass it along) - but of course will leave it up to you, just want to make sure we’re on the same page here.
  228. in src/common/args.cpp:140 in 5a94560045 outdated
    136+ *
    137+ * - JSON numbers like `123` are returned for settings like `-setting=123` if
    138+ *   the setting enables integer parsing with the ALLOW_INT flag.
    139  */
    140 std::optional<common::SettingsValue> InterpretValue(const KeyInfo& key, const std::string* value,
    141                                                   unsigned int flags, std::string& error)
    


    l0rinc commented at 7:35 pm on September 25, 2024:
    nit: alignment

    ryanofsky commented at 2:57 pm on October 2, 2024:

    re: #16545 (review)

    nit: alignment

    Thanks, updated

  229. in src/common/args.cpp:166 in 5a94560045 outdated
    164     }
    165-    if (!value && (flags & ArgsManager::DISALLOW_ELISION)) {
    166-        error = strprintf("Can not set -%s with no value. Please specify value with -%s=value.", key.name, key.name);
    167-        return std::nullopt;
    168+    if (value) {
    169+        int64_t parsed_int;
    


    l0rinc commented at 7:42 pm on September 25, 2024:
    nit: this is only used once, maybe we could have a local TryParseInt64 next to InterpretBool - details below

    ryanofsky commented at 3:42 pm on October 2, 2024:

    re: #16545 (review)

    nit: this is only used once, maybe we could have a local TryParseInt64 next to InterpretBool - details below

    Thanks, adopted suggested approach.

    Note: other comment with details is: #16545 (review)

  230. in src/common/args.cpp:170 in 5a94560045 outdated
    168+    if (value) {
    169+        int64_t parsed_int;
    170+        if ((flags & ArgsManager::ALLOW_STRING) || !IsTypedArg(flags) || value->empty()) return *value;
    171+        if ((flags & ArgsManager::ALLOW_INT) && ParseInt64(*value, &parsed_int)) return parsed_int;
    172+        if ((flags & ArgsManager::ALLOW_BOOL) && *value == "0") return false;
    173+        if ((flags & ArgsManager::ALLOW_BOOL) && *value == "1") return true;
    


    l0rinc commented at 7:44 pm on September 25, 2024:

    we could group the two bools and use an optional for the in parsing (assuming std::optional value parameter):

    0        if (flags & ArgsManager::ALLOW_INT) {
    1            if (auto parsed = TryParseInt64(*value)) return *parsed;
    2        }
    3        if (flags & ArgsManager::ALLOW_BOOL) {
    4            if (value == "0") return false;
    5            if (value == "1") return true;
    6        }
    
    0std::optional<int64_t> TryParseInt64(std::string_view str) {
    1    int64_t parsed_int;
    2    if (ParseInt64(str, &parsed_int)) return parsed_int;
    3    return std::nullopt;
    4}
    

    ryanofsky commented at 4:03 pm on October 2, 2024:

    re: #16545 (review)

    we could group the two bools and use an optional for the in parsing (assuming std::optional value parameter): TryParseInt64

    Thanks, switched to this approach.

  231. in src/common/args.cpp:160 in 5a94560045 outdated
    155+            }
    156+            return false;
    157+        }
    158         // Double negatives like -nofoo=0 are supported (but discouraged)
    159         if (value && !InterpretBool(*value)) {
    160             LogPrintf("Warning: parsed potentially confusing double-negative -%s=%s\n", key.name, *value);
    


    l0rinc commented at 9:21 pm on September 25, 2024:
    I find it quite confusing that have a return value, and error return value and even warning as possible outcomes.

    ryanofsky commented at 3:17 pm on October 2, 2024:

    re: #16545 (review)

    (note: This comment is about previous code unaffected by this PR)

    I find it quite confusing that have a return value, and error return value and even warning as possible outcomes.

    It would probably be better for this to return util::Result, but in fairness this function is only called two places and intended for internal use, so exact form of its API hopefully doesn’t matter too much.


    l0rinc commented at 5:52 pm on October 2, 2024:
    Thanks for considering
  232. in src/common/args.cpp:166 in 5a94560045 outdated
    165-    if (!value && (flags & ArgsManager::DISALLOW_ELISION)) {
    166-        error = strprintf("Can not set -%s with no value. Please specify value with -%s=value.", key.name, key.name);
    167-        return std::nullopt;
    168+    if (value) {
    169+        int64_t parsed_int;
    170+        if ((flags & ArgsManager::ALLOW_STRING) || !IsTypedArg(flags) || value->empty()) return *value;
    


    l0rinc commented at 9:32 pm on September 25, 2024:

    we could divide this method into typed and untyped parsing, would simplify the logic slightly:

     0std::optional<common::SettingsValue> HandleTypedArg(const KeyInfo& key, std::optional<std::string_view> value,
     1                                                    unsigned int flags, std::string& error)
     2{
     3    if (key.negated) {
     4        // If argument is typed, only allow negation with no value or with
     5        // literal "1" value. Avoid calling InterpretBool and accepting
     6        // other values which could be ambiguous.
     7        if (value && value != "1") {
     8            error = strprintf("Cannot negate -%s at the same time as setting a value ('%s').", key.name, *value);
     9            return std::nullopt;
    10        }
    11        return false;
    12    }
    13    if (value) {
    14        if (IsStringArg(flags) || value->empty()) return *value;
    15        if (IsIntArg(flags)) {
    16            if (auto parsed = TryParseInt64(*value)) return *parsed;
    17        }
    18        if (IsBoolArg(flags)) {
    19            if (value == "0") return false;
    20            if (value == "1") return true;
    21        }
    22        error = strprintf("Cannot set -%s value to '%s'.", key.name, *value);
    23    } else {
    24        if (IsBoolArg(flags)) return true;
    25        error = strprintf("Cannot set -%s with no value. Please specify value with -%s=value.", key.name, key.name);
    26    }
    27
    28    auto type_str = IsStringArg(flags) ? "a string" : IsIntArg(flags) ? "an integer" : "0 or 1";
    29    error += strprintf(" It must be set to %s.", type_str);
    30    return std::nullopt;
    31}
    32
    33std::optional<common::SettingsValue> HandleUntypedArg(const KeyInfo& key, std::optional<std::string_view> value,
    34                                                      unsigned int flags, std::string& error)
    35{
    36    if (key.negated) {
    37        // Double negatives like -nofoo=0 are supported (but discouraged)
    38        if (value && !InterpretBool(*value)) {
    39            LogPrintf("Warning: parsed potentially confusing double-negative -%s=%s\n", key.name, *value);
    40            return true;
    41        }
    42        return false;
    43    }
    44    if (value) return *value;
    45    if (!DisallowElision(flags)) return "";
    46    error = strprintf("Cannot set -%s with no value. Please specify value with -%s=value.", key.name, key.name);
    47    return std::nullopt;
    48}
    49
    50std::optional<common::SettingsValue> InterpretValue(const KeyInfo& key, std::optional<std::string_view> value,
    51                                                    unsigned int flags, std::string& error)
    52{
    53    // Check for disallowed negation
    54    if (key.negated && DisallowNegation(flags)) {
    55        error = strprintf("Negating of -%s is meaningless and therefore forbidden", key.name);
    56        return std::nullopt;
    57    }
    58
    59    return IsTypedArg(flags)
    60         ? HandleTypedArg(key, value, flags, error)
    61         : HandleUntypedArg(key, value, flags, error);
    62}
    

    ryanofsky commented at 4:02 pm on October 2, 2024:

    re: #16545 (review)

    It’s interesting to see what that looks like but I see few disadvantages to that approach:

    • Superficially, the resulting code is longer and the diff is bigger. Current PR is trying to minimally change this function.
    • It seems to be duplicating some code like “Cannot set -%s with no value” error, increasing risk that differences between the two copies of code will be introduced accidentally, and introducing unintended differences in behavior for typed and untyped arguments.
    • It seems to make it harder to get a high level sense of how argument parsing is intended to work, for example parsing of negated arguments is split up across three functions instead of being handled one place.
    • It seems to do more string manipulation, breaking up error messages into smaller strings that might be harder to search for and translate (if we want to support that later).
    • It might not work as well with my branch in #22978. In this PR, distinction between typed and legacy arguments is all-or-nothing, while in the other branch I want allow supporting and disabling legacy behavior with granular options like SettingOptions::allow_double_negation to make code clearer and offer more flexibility for migrating away from legacy behaviors.

    l0rinc commented at 5:52 pm on October 2, 2024:
    Your criticism is reasonable, do you see a clearer way of breaking up this method? Currently I need to know too much about the possible states on a single level, would prefer excluding some states while I’m reviewing.

    ryanofsky commented at 7:59 pm on December 9, 2024:

    re: #16545 (review)

    Currently I need to know too much about the possible states on a single level, would prefer excluding some states while I’m reviewing.

    I think a good strategy is to review the function twice. Review it one time assuming the argument is typed, and another time assuming the argument is untyped, and make sure behavior makes sense in both cases. This approach should also make it easier to see that there aren’t unnecessary inconsistencies between the two cases, since there aren’t two functions that need to be compared.

    Also, I expect this function to get simpler if #31260 is merged first and this is rebased on top. The int/string/bool parsing logic currently here should move into the SettingTraits classes introduced in that PR.

  233. in src/common/args.cpp:186 in 5a94560045 outdated
    181+        error = strprintf("%s %s", error, "It must be set to a string.");
    182+    } else if (flags & ArgsManager::ALLOW_INT) {
    183+        error = strprintf("%s %s", error, "It must be set to an integer.");
    184+    } else if (flags & ArgsManager::ALLOW_BOOL) {
    185+        error = strprintf("%s %s", error, "It must be set to 0 or 1.");
    186+    }
    


    l0rinc commented at 9:34 pm on September 25, 2024:

    we could change this to an error += syntax:

    0auto type_str = IsStringArg(flags) ? "a string" : IsIntArg(flags) ? "an integer" : "0 or 1";
    1error += strprintf(" It must be set to %s.", type_str);
    

    ryanofsky commented at 4:09 pm on October 2, 2024:

    re: #16545 (review)

    we could change this to an error += syntax:

    Unsure about this, I feel like this could make strings harder to search for and translate. And since the new code does not explicitly check for bool type it could lead to a bug if support for other types is added. Leaving alone for now. I can see appeal of this approach but it doesn’t seem like a clear win.


    l0rinc commented at 5:52 pm on October 2, 2024:
    Agree, it was an alternative, both are fine
  234. in src/common/args.cpp:680 in 5a94560045 outdated
    671@@ -580,6 +672,24 @@ void ArgsManager::AddArg(const std::string& name, const std::string& help, unsig
    672     if (flags & ArgsManager::NETWORK_ONLY) {
    673         m_network_only_args.emplace(arg_name);
    674     }
    675+
    676+    // Disallow flag combinations that would result in nonsensical behavior or a bad UX.
    677+    if ((flags & ALLOW_ANY) && (flags & (ALLOW_BOOL | ALLOW_INT | ALLOW_STRING))) {
    


    l0rinc commented at 9:40 pm on September 25, 2024:
    • the last part seems to check if we’re typed
    • what if we introduced flag helpers to make this more readable and easier to transition later:
    0//! Whether the type of the argument has been specified and extra validation rules should apply.
    1inline bool DisallowNegation(uint32_t flags) { return flags & ArgsManager::DISALLOW_NEGATION; }
    2inline bool DisallowElision(uint32_t flags) { return flags & ArgsManager::DISALLOW_ELISION; }
    3
    4inline bool IsAnyArg(uint32_t flags) { return flags & ArgsManager::ALLOW_ANY; }
    5inline bool IsBoolArg(uint32_t flags) { return flags & ArgsManager::ALLOW_BOOL; }
    6inline bool IsIntArg(uint32_t flags) { return flags & ArgsManager::ALLOW_INT; }
    7inline bool IsStringArg(uint32_t flags) { return flags & ArgsManager::ALLOW_STRING; }
    8inline bool IsListArg(uint32_t flags) { return flags & ArgsManager::ALLOW_LIST; }
    9inline bool IsTypedArg(uint32_t flags) { return IsBoolArg(flags) || IsIntArg(flags) || IsStringArg(flags); }
    

    and this line would become:

    0    if (IsAnyArg(flags) && IsTypedArg(flags)) {
    

    ryanofsky commented at 4:18 pm on October 2, 2024:

    re: #16545 (review)

    Again, it’s interesting to see what this looks like. so thanks for spelling it out. I don’t love the flags & ALLOW syntax, so it’s nice that helper functions hide it, but I feel the helper functions add a level of indirection that make it harder to plainly see what the code is doing.

    Specifically they make this checking code less clear because of instead of checking the flags and then mentioning the flags just checked for in error messages, the errors would now be mentioning flags not directly checked for. I don’t think I would complain if code were written this way, but it’s not the way I would prefer to see it written.


    l0rinc commented at 5:52 pm on October 2, 2024:
    • We could still use theIsTypedArg(flags) on this line
    • Eventually we will hopefully migrate away from flags, I would personally prefer we encapsulate them early

    ryanofsky commented at 8:00 pm on December 9, 2024:

    re: #16545 (review)

    I wouldn’t really consider this encapsulating. It is adding a level of indirection and changing syntax used to access flags without reducing the need for callers to know everything about them. #31260 adds a better way of specifying types without flags so that could be a way forward here.

  235. in src/test/argsman_tests.cpp:202 in 5a94560045 outdated
    197+        }
    198+        std::string error;
    199+        if (!args.ParseParameters(argv.size(), argv.data(), error)) {
    200+            throw std::runtime_error(error);
    201+        }
    202+        BOOST_CHECK_EQUAL(error, "");
    


    l0rinc commented at 9:43 pm on September 25, 2024:

    nit:

    0        std::string error;
    1        BOOST_REQUIRE_MESSAGE(args.ParseParameters(argv.size(), argv.data(), error), error);
    2        BOOST_CHECK_EQUAL(error, "");
    

    (though I guess this is needed for your testing style that I commented on later)


    ryanofsky commented at 4:51 pm on October 2, 2024:

    re: #16545 (review)

    Right, as far as I know this suggestion would not allow checking for specific error messages, but if I am missing something or you do want another change here just let me know.

  236. in src/test/argsman_tests.cpp:279 in 5a94560045 outdated
    274+    BOOST_CHECK_EXCEPTION(ParseOptions({"-bytespersigop=abc"}), std::exception, HasReason{"Cannot set -bytespersigop value to 'abc'. It must be set to an integer."});
    275+
    276+    // Check default assumevalid value is unset.
    277+    BOOST_CHECK(!ParseOptions({}).assumevalid);
    278+    // Check passing -assumevalid=<hash> makes it set that hash.
    279+    BOOST_CHECK_EQUAL(ParseOptions({"-assumevalid=0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"}).assumevalid.value().ToString(), "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff");
    


    l0rinc commented at 9:46 pm on September 25, 2024:

    nit:

    0    BOOST_CHECK_EQUAL(ParseOptions({"-assumevalid=0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"}).assumevalid, uint256{"0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"});
    

    ryanofsky commented at 4:52 pm on October 2, 2024:

    re: #16545 (review)

    Thanks, applied suggestion

  237. in src/test/fuzz/system.cpp:54 in 5a94560045 outdated
    45@@ -46,7 +46,11 @@ FUZZ_TARGET(system, .init = initialize_system)
    46             [&] {
    47                 auto str_arg = fuzzed_data_provider.ConsumeRandomLengthString(16);
    48                 auto str_value = fuzzed_data_provider.ConsumeRandomLengthString(16);
    49-                args_manager.SoftSetArg(str_arg, str_value);
    50+                // Avoid Can't call SoftSetArg on arg registered with flags 0x8d8d8d00 (requires 0x2, disallows 0x10)
    51+                try {
    52+                    args_manager.SoftSetArg(str_arg, str_value);
    53+                } catch (const std::logic_error&) {
    54+                }
    


    l0rinc commented at 9:49 pm on September 25, 2024:
    doesn’t this defeat the purpose and give us false confidence? Can we at least assert something in the message to make sure it’s not an unrelated exception?

    ryanofsky commented at 4:28 pm on October 2, 2024:

    re: #16545 (review)

    doesn’t this defeat the purpose and give us false confidence? Can we at least assert something in the message to make sure it’s not an unrelated exception?

    I think the purpose of this fuzz test is more to check for vulnerabilities and crashes than to check for implementation details of the argument parsing framework. I’d be happy to update this if you have a specific suggestion, but I also think it’s fine for this particular fuzz test to just verify that ArgsManager methods can throw logic_errors if called with inconsistent types, and then move onto other cases, without trying to skip these cases or check error messages in fine detail.


    l0rinc commented at 5:51 pm on October 2, 2024:
    Can we at least make absolutely sure we’re only catching allowed exceptions (either by type or message) and not something unexpected bubbling up?

    ryanofsky commented at 8:20 pm on December 9, 2024:

    re: #16545 (review)

    Can we at least make absolutely sure we’re only catching allowed exceptions (either by type or message) and not something unexpected bubbling up?

    Yes, added checks. I don’t think most existing fuzz tests are so strict about what exceptions are thrown when they are expecting exceptions. But I understand being more strict could potentially catch more bugs and make fuzz tests easier to understand even if it adds some maintenance burden.

  238. in src/test/fuzz/system.cpp:82 in 5a94560045 outdated
    75@@ -68,6 +76,11 @@ FUZZ_TARGET(system, .init = initialize_system)
    76                 }
    77                 auto help = fuzzed_data_provider.ConsumeRandomLengthString(16);
    78                 auto flags = fuzzed_data_provider.ConsumeIntegral<unsigned int>() & ~ArgsManager::COMMAND;
    79+                // Avoid hitting "ALLOW_INT flag is incompatible with ALLOW_STRING", etc exceptions
    80+                if (flags & ArgsManager::ALLOW_ANY) flags &= ~(ArgsManager::ALLOW_BOOL | ArgsManager::ALLOW_INT | ArgsManager::ALLOW_STRING);
    


    l0rinc commented at 9:50 pm on September 25, 2024:
    let’s use the flag helper methods here as well

    ryanofsky commented at 4:21 pm on October 2, 2024:

    re: #16545 (review)

    let’s use the flag helper methods here as well

    Can see response to #16545 (review), but logic in this fuzzing code is already obscure and I feel like hiding the flags in the if conditions while continuing to use the flags in the body of the if statements would only make it more obscure.


    l0rinc commented at 5:51 pm on October 2, 2024:
    I mean, you’ve already extracted a IsTypedArg(flags) method for ArgsManager::ALLOW_BOOL | ArgsManager::ALLOW_INT | ArgsManager::ALLOW_STRING, right?

    ryanofsky commented at 8:19 pm on December 9, 2024:

    re: #16545 (review)

    I mean, you’ve already extracted a IsTypedArg(flags) method for ArgsManager::ALLOW_BOOL | ArgsManager::ALLOW_INT | ArgsManager::ALLOW_STRING, right?

    IsTypedArg is used in higher level code to determine if type checking and coercion should be done. I think that code is easier to understand if the distinction between typed and untyped arguments is explicit and it is not dealing with individual flags.

    By contrast, this code is low level fuzz code directly dealing with interactions between the flags, and I think it is clearer if it can reference them directly.

  239. in src/test/util/setup_common.h:262 in 5a94560045 outdated
    258@@ -259,6 +259,10 @@ inline std::ostream& operator<<(std::ostream& os, const std::optional<T>& v)
    259     return v ? os << *v
    260              : os << "std::nullopt";
    261 }
    262+inline std::ostream& operator<<(std::ostream& os, const std::nullopt_t)
    


    l0rinc commented at 9:50 pm on September 25, 2024:

    is this still needed? Though I understand If you think the error messages might be better than:

    0BOOST_CHECK(!ParseOptions({}).enable_dns_seed);
    1BOOST_CHECK(!ParseOptions({"-dnsseed=1", "-dnsseed="}).enable_dns_seed);
    

    ryanofsky commented at 4:35 pm on October 2, 2024:

    re: #16545 (review)

    It’s not needed, and main motivation is not to improve error messages (though it can definitely help to see what value is being returned if not null)

    Main motivation is to be able to write tests like:

    0BOOST_CHECK_EQUAL(fun(val1), std::nullopt);
    1BOOST_CHECK_EQUAL(fun(val2), val3);
    

    instead of:

    0BOOST_CHECK(!fun(val1));
    1BOOST_CHECK_EQUAL(fun(val2), val3);
    

    IMO, former is more consistent, readable, and explicit.


    l0rinc commented at 5:52 pm on October 2, 2024:
    I see you went the other way - I’m fine with that as well, it’s more verbose, but at least consistent

    hodlinator commented at 7:52 pm on October 2, 2024:
    Also note that BOOST_CHECK() is deprecated for unclear reasons: https://github.com/boostorg/test/blob/develop/include/boost/test/tools/interface.hpp#L273-L276 (Commit)
  240. l0rinc commented at 10:00 pm on September 25, 2024: contributor

    Approach ACK

    I did a first scan over the impl, left a few nits, some refactoring ideas, let me know what you think and I’ll continue the reviews based on that.

      0Index: src/test/util/setup_common.h
      1<+>UTF-8
      2===================================================================
      3diff --git a/src/test/util/setup_common.h b/src/test/util/setup_common.h
      4--- a/src/test/util/setup_common.h	(revision 5a945600451037693a032e6df94f99a666dd581f)
      5+++ b/src/test/util/setup_common.h	(date 1727301051076)
      6@@ -259,10 +259,6 @@
      7     return v ? os << *v
      8              : os << "std::nullopt";
      9 }
     10-inline std::ostream& operator<<(std::ostream& os, const std::nullopt_t)
     11-{
     12-    return os << "std::nullopt";
     13-}
     14 } // namespace std
     15 
     16 std::ostream& operator<<(std::ostream& os, const arith_uint256& num);
     17Index: src/common/args.cpp
     18<+>UTF-8
     19===================================================================
     20diff --git a/src/common/args.cpp b/src/common/args.cpp
     21--- a/src/common/args.cpp	(revision 5a945600451037693a032e6df94f99a666dd581f)
     22+++ b/src/common/args.cpp	(date 1727300363436)
     23@@ -56,11 +56,16 @@
     24  * For a more extensive discussion of this topic (and a wide range of opinions
     25  * on the Right Way to change this code), see PR12713.
     26  */
     27-static bool InterpretBool(const std::string& strValue)
     28+static bool InterpretBool(std::string_view strValue)
     29 {
     30-    if (strValue.empty())
     31-        return true;
     32-    return (LocaleIndependentAtoi<int>(strValue) != 0);
     33+    return strValue.empty() || LocaleIndependentAtoi<int>(strValue);
     34+}
     35+
     36+std::optional<int64_t> TryParseInt64(std::string_view str)
     37+{
     38+    int64_t result;
     39+    if (ParseInt64(str, &result)) return result;
     40+    return std::nullopt;
     41 }
     42 
     43 static std::string SettingName(const std::string& arg)
     44@@ -93,6 +98,56 @@
     45     return result;
     46 }
     47 
     48+std::optional<common::SettingsValue> HandleTypedArg(const KeyInfo& key, std::optional<std::string_view> value,
     49+                                                    unsigned int flags, std::string& error)
     50+{
     51+    if (key.negated) {
     52+        // If argument is typed, only allow negation with no value or with
     53+        // literal "1" value. Avoid calling InterpretBool and accepting
     54+        // other values which could be ambiguous.
     55+        if (value && value != "1") {
     56+            error = strprintf("Cannot negate -%s at the same time as setting a value ('%s').", key.name, *value);
     57+            return std::nullopt;
     58+        }
     59+        return false;
     60+    }
     61+    if (value) {
     62+        if (IsStringArg(flags) || value->empty()) return *value;
     63+        if (IsIntArg(flags)) {
     64+            if (auto parsed = TryParseInt64(*value)) return *parsed;
     65+        }
     66+        if (IsBoolArg(flags)) {
     67+            if (value == "0") return false;
     68+            if (value == "1") return true;
     69+        }
     70+        error = strprintf("Cannot set -%s value to '%s'.", key.name, *value);
     71+    } else {
     72+        if (IsBoolArg(flags)) return true;
     73+        error = strprintf("Cannot set -%s with no value. Please specify value with -%s=value.", key.name, key.name);
     74+    }
     75+
     76+    auto type_str = IsStringArg(flags) ? "a string" : IsIntArg(flags) ? "an integer" : "0 or 1";
     77+    error += strprintf(" It must be set to %s.", type_str);
     78+    return std::nullopt;
     79+}
     80+
     81+std::optional<common::SettingsValue> HandleUntypedArg(const KeyInfo& key, std::optional<std::string_view> value,
     82+                                                      unsigned int flags, std::string& error)
     83+{
     84+    if (key.negated) {
     85+        // Double negatives like -nofoo=0 are supported (but discouraged)
     86+        if (value && !InterpretBool(*value)) {
     87+            LogPrintf("Warning: parsed potentially confusing double-negative -%s=%s\n", key.name, *value);
     88+            return true;
     89+        }
     90+        return false;
     91+    }
     92+    if (value) return *value;
     93+    if (!DisallowElision(flags)) return "";
     94+    error = strprintf("Cannot set -%s with no value. Please specify value with -%s=value.", key.name, key.name);
     95+    return std::nullopt;
     96+}
     97+
     98 /**
     99  * Interpret settings value based on registered flags.
    100  *
    101@@ -136,52 +191,18 @@
    102  * - JSON numbers like `123` are returned for settings like `-setting=123` if
    103  *   the setting enables integer parsing with the ALLOW_INT flag.
    104  */
    105-std::optional<common::SettingsValue> InterpretValue(const KeyInfo& key, const std::string* value,
    106-                                                  unsigned int flags, std::string& error)
    107+std::optional<common::SettingsValue> InterpretValue(const KeyInfo& key, std::optional<std::string_view> value,
    108+                                                    unsigned int flags, std::string& error)
    109 {
    110-    // Return negated settings as false values.
    111-    if (key.negated) {
    112-        if (flags & ArgsManager::DISALLOW_NEGATION) {
    113-            error = strprintf("Negating of -%s is meaningless and therefore forbidden", key.name);
    114-            return std::nullopt;
    115-        }
    116-        if (IsTypedArg(flags)) {
    117-            // If argument is typed, only allow negation with no value or with
    118-            // literal "1" value. Avoid calling InterpretBool and accepting
    119-            // other values which could be ambiguous.
    120-            if (value && *value != "1") {
    121-                error = strprintf("Cannot negate -%s at the same time as setting a value ('%s').", key.name, *value);
    122-                return std::nullopt;
    123-            }
    124-            return false;
    125-        }
    126-        // Double negatives like -nofoo=0 are supported (but discouraged)
    127-        if (value && !InterpretBool(*value)) {
    128-            LogPrintf("Warning: parsed potentially confusing double-negative -%s=%s\n", key.name, *value);
    129-            return true;
    130-        }
    131-        return false;
    132+    // Check for disallowed negation
    133+    if (key.negated && DisallowNegation(flags)) {
    134+        error = strprintf("Negating of -%s is meaningless and therefore forbidden", key.name);
    135+        return std::nullopt;
    136     }
    137-    if (value) {
    138-        int64_t parsed_int;
    139-        if ((flags & ArgsManager::ALLOW_STRING) || !IsTypedArg(flags) || value->empty()) return *value;
    140-        if ((flags & ArgsManager::ALLOW_INT) && ParseInt64(*value, &parsed_int)) return parsed_int;
    141-        if ((flags & ArgsManager::ALLOW_BOOL) && *value == "0") return false;
    142-        if ((flags & ArgsManager::ALLOW_BOOL) && *value == "1") return true;
    143-        error = strprintf("Cannot set -%s value to '%s'.", key.name, *value);
    144-    } else {
    145-        if (flags & ArgsManager::ALLOW_BOOL) return true;
    146-        if (!(flags & ArgsManager::DISALLOW_ELISION) && !IsTypedArg(flags)) return "";
    147-        error = strprintf("Cannot set -%s with no value. Please specify value with -%s=value.", key.name, key.name);
    148-    }
    149-    if (flags & ArgsManager::ALLOW_STRING) {
    150-        error = strprintf("%s %s", error, "It must be set to a string.");
    151-    } else if (flags & ArgsManager::ALLOW_INT) {
    152-        error = strprintf("%s %s", error, "It must be set to an integer.");
    153-    } else if (flags & ArgsManager::ALLOW_BOOL) {
    154-        error = strprintf("%s %s", error, "It must be set to 0 or 1.");
    155-    }
    156-    return std::nullopt;
    157+
    158+    return IsTypedArg(flags)
    159+           ? HandleTypedArg(key, value, flags, error)
    160+           : HandleUntypedArg(key, value, flags, error);
    161 }
    162 
    163 //! Return string if setting is a nonempty string or number (-setting=abc,
    164@@ -284,10 +305,10 @@
    165 #endif
    166 
    167         if (key == "-") break; //bitcoin-tx using stdin
    168-        std::optional<std::string> val;
    169+        std::optional<std::string_view> val;
    170         size_t is_index = key.find('=');
    171         if (is_index != std::string::npos) {
    172-            val = key.substr(is_index + 1);
    173+            val = std::string_view{key}.substr(is_index + 1);
    174             key.erase(is_index);
    175         }
    176 #ifdef WIN32
    177@@ -330,10 +351,11 @@
    178             return false;
    179         }
    180 
    181-        std::optional<common::SettingsValue> value = InterpretValue(keyinfo, val ? &*val : nullptr, *flags, error);
    182-        if (!value) return false;
    183-
    184-        m_settings.command_line_options[keyinfo.name].push_back(*value);
    185+        if (auto value = InterpretValue(keyinfo, val, *flags, error)) {
    186+            m_settings.command_line_options[keyinfo.name].push_back(*value);
    187+        } else {
    188+            return false;
    189+        }
    190     }
    191 
    192     // we do not allow -includeconf from command line, only -noincludeconf
    193@@ -674,19 +696,19 @@
    194     }
    195 
    196     // Disallow flag combinations that would result in nonsensical behavior or a bad UX.
    197-    if ((flags & ALLOW_ANY) && (flags & (ALLOW_BOOL | ALLOW_INT | ALLOW_STRING))) {
    198+    if (IsAnyArg(flags) && IsTypedArg(flags)) {
    199         throw std::logic_error(strprintf("Bug: bad %s flags. ALLOW_{BOOL|INT|STRING} flags are incompatible with "
    200                                          "ALLOW_ANY (typed arguments need to be type checked)", arg_name));
    201     }
    202-    if ((flags & ALLOW_BOOL) && (flags & DISALLOW_ELISION)) {
    203+    if (IsBoolArg(flags) && DisallowElision(flags)) {
    204         throw std::logic_error(strprintf("Bug: bad %s flags. ALLOW_BOOL flag is incompatible with DISALLOW_ELISION "
    205                                          "(boolean arguments should not require argument values)", arg_name));
    206     }
    207-    if ((flags & ALLOW_INT) && (flags & ALLOW_STRING)) {
    208+    if (IsIntArg(flags) && IsStringArg(flags)) {
    209         throw std::logic_error(strprintf("Bug: bad %s flags. ALLOW_INT flag is incompatible with ALLOW_STRING "
    210                                          "(any valid integer is also a valid string)", arg_name));
    211     }
    212-    if ((flags & ALLOW_BOOL) && (flags & (ALLOW_INT | ALLOW_STRING))) {
    213+    if (IsBoolArg(flags) && (IsIntArg(flags) || IsStringArg(flags))) {
    214         throw std::logic_error(strprintf("Bug: bad %s flags. ALLOW_BOOL flag may not currently be specified with ALLOW_INT or ALLOW_STRING "
    215                                          "(integer and string argument values cannot currently be omitted)", arg_name));
    216     }
    217Index: src/common/config.cpp
    218<+>UTF-8
    219===================================================================
    220diff --git a/src/common/config.cpp b/src/common/config.cpp
    221--- a/src/common/config.cpp	(revision 5a945600451037693a032e6df94f99a666dd581f)
    222+++ b/src/common/config.cpp	(date 1727296525414)
    223@@ -105,11 +105,11 @@
    224                 error = strprintf("Multiple values specified for -%s in same section of config file.", key.name);
    225                 return false;
    226             }
    227-            std::optional<common::SettingsValue> value = InterpretValue(key, &option.second, *flags, error);
    228-            if (!value) {
    229-                return false;
    230-            }
    231-            m_settings.ro_config[key.section][key.name].push_back(*value);
    232+            if (auto value = InterpretValue(key, option.second, *flags, error)) {
    233+                m_settings.ro_config[key.section][key.name].push_back(*value);
    234+            } else {
    235+                return false;
    236+            }
    237         } else {
    238             if (ignore_invalid_keys) {
    239                 LogPrintf("Ignoring unknown configuration value %s\n", option.first);
    240Index: src/test/argsman_tests.cpp
    241<+>UTF-8
    242===================================================================
    243diff --git a/src/test/argsman_tests.cpp b/src/test/argsman_tests.cpp
    244--- a/src/test/argsman_tests.cpp	(revision 5a945600451037693a032e6df94f99a666dd581f)
    245+++ b/src/test/argsman_tests.cpp	(date 1727301503684)
    246@@ -232,7 +232,7 @@
    247     BOOST_CHECK_EXCEPTION(ParseOptions({"-rpcserver=yes"}), std::exception, HasReason{"Cannot set -rpcserver value to 'yes'. It must be set to 0 or 1."});
    248 
    249     // Check default dnsseed value is unset.
    250-    BOOST_CHECK_EQUAL(ParseOptions({}).enable_dns_seed, std::nullopt);
    251+    BOOST_CHECK(!ParseOptions({}).enable_dns_seed);
    252     // Check passing -dnsseed makes it true.
    253     BOOST_CHECK_EQUAL(ParseOptions({"-dnsseed"}).enable_dns_seed, true);
    254     // Check passing -dnsseed=1 makes it true.
    255@@ -242,7 +242,7 @@
    256     // Check passing -dnsseed=0 makes it false.
    257     BOOST_CHECK_EQUAL(ParseOptions({"-dnsseed=0"}).enable_dns_seed, false);
    258     // Check adding -dnsseed= sets it back to default.
    259-    BOOST_CHECK_EQUAL(ParseOptions({"-dnsseed=1", "-dnsseed="}).enable_dns_seed, std::nullopt);
    260+    BOOST_CHECK(!ParseOptions({"-dnsseed=1", "-dnsseed="}).enable_dns_seed);
    261     // Check passing invalid value.
    262     BOOST_CHECK_EXCEPTION(ParseOptions({"-dnsseed=yes"}), std::exception, HasReason{"Cannot set -dnsseed value to 'yes'. It must be set to 0 or 1."});
    263 
    264@@ -276,7 +276,7 @@
    265     // Check default assumevalid value is unset.
    266     BOOST_CHECK(!ParseOptions({}).assumevalid);
    267     // Check passing -assumevalid=<hash> makes it set that hash.
    268-    BOOST_CHECK_EQUAL(ParseOptions({"-assumevalid=0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"}).assumevalid.value().ToString(), "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff");
    269+    BOOST_CHECK_EQUAL(ParseOptions({"-assumevalid=0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"}).assumevalid, uint256{"0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"});
    270     // Check passing -noassumevalid makes it not assumevalid.
    271     BOOST_CHECK(!ParseOptions({"-noassumevalid"}).assumevalid);
    272     // Check adding -assumevalid= sets it back to default.
    273Index: src/common/args.h
    274<+>UTF-8
    275===================================================================
    276diff --git a/src/common/args.h b/src/common/args.h
    277--- a/src/common/args.h	(revision 5a945600451037693a032e6df94f99a666dd581f)
    278+++ b/src/common/args.h	(date 1727300249024)
    279@@ -77,8 +77,8 @@
    280 
    281 KeyInfo InterpretKey(std::string key);
    282 
    283-std::optional<common::SettingsValue> InterpretValue(const KeyInfo& key, const std::string* value,
    284-                                                         unsigned int flags, std::string& error);
    285+std::optional<common::SettingsValue> InterpretValue(const KeyInfo& key, std::optional<std::string_view> value,
    286+                                                    unsigned int flags, std::string& error);
    287 
    288 struct SectionInfo {
    289     std::string m_name;
    290@@ -561,12 +561,16 @@
    291         const std::map<std::string, std::vector<common::SettingsValue>>& args) const;
    292 };
    293 
    294-//! Whether the type of the argument has been specified and extra validation
    295-//! rules should apply.
    296-inline bool IsTypedArg(uint32_t flags)
    297-{
    298-    return flags & (ArgsManager::ALLOW_BOOL | ArgsManager::ALLOW_INT |  ArgsManager::ALLOW_STRING);
    299-}
    300+//! Whether the type of the argument has been specified and extra validation rules should apply.
    301+inline bool DisallowNegation(uint32_t flags) { return flags & ArgsManager::DISALLOW_NEGATION; }
    302+inline bool DisallowElision(uint32_t flags) { return flags & ArgsManager::DISALLOW_ELISION; }
    303+
    304+inline bool IsAnyArg(uint32_t flags) { return flags & ArgsManager::ALLOW_ANY; }
    305+inline bool IsBoolArg(uint32_t flags) { return flags & ArgsManager::ALLOW_BOOL; }
    306+inline bool IsIntArg(uint32_t flags) { return flags & ArgsManager::ALLOW_INT; }
    307+inline bool IsStringArg(uint32_t flags) { return flags & ArgsManager::ALLOW_STRING; }
    308+inline bool IsListArg(uint32_t flags) { return flags & ArgsManager::ALLOW_LIST; }
    309+inline bool IsTypedArg(uint32_t flags) { return IsBoolArg(flags) || IsIntArg(flags) || IsStringArg(flags); }
    310 
    311 extern ArgsManager gArgs;
    
  241. ryanofsky force-pushed on Oct 2, 2024
  242. ryanofsky commented at 5:09 pm on October 2, 2024: contributor
    Updated 5a945600451037693a032e6df94f99a666dd581f -> b5ef85497436c3e9e60c760465d8991592efef07 (pr/argcheck.42 -> pr/argcheck.43, compare) implementing review suggestions
  243. ryanofsky referenced this in commit 98f2edf32c on Oct 4, 2024
  244. achow101 referenced this in commit 11f68cc810 on Dec 4, 2024
  245. ryanofsky force-pushed on Dec 9, 2024
  246. ryanofsky commented at 8:28 pm on December 9, 2024: contributor

    Appreciate the reviews.

    Updated b5ef85497436c3e9e60c760465d8991592efef07 -> d28a5c88979d89dcaea27dc9a015bac54c640d3f (pr/argcheck.43 -> pr/argcheck.44, compare) with some suggested changes. Updated d28a5c88979d89dcaea27dc9a015bac54c640d3f -> 2057315f03ac6daa6905e1ce9bf7bb92b22442ad (pr/argcheck.44 -> pr/argcheck.45, compare) to fix typo in fuzz check Updated 2057315f03ac6daa6905e1ce9bf7bb92b22442ad -> 4090dcf6cfda2e21b7a7323de0e0499092ea900c (pr/argcheck.45 -> pr/argcheck.46, compare) to fix typo in fuzz check

    While I think this PR is in a good state, I’d encourage review of #31260 ahead of this, because #31260 allows setting types to be specified with C++ types instead of flags, which should semantics implemented here more obvious when this can be rebased on top.

  247. ryanofsky force-pushed on Dec 9, 2024
  248. ryanofsky force-pushed on Dec 9, 2024

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: 2024-12-23 12:12 UTC

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