Skip to content

Commit 4490871

Browse files
author
MarcoFalke
committed
Merge #12713: Track negated options in the option parser
f7683cb Track negated arguments in the argument paser. (Evan Klitzke) 4f872b2 Add additional tests for GetBoolArg() (Evan Klitzke) Pull request description: This change explicitly enable tracking negated options in the option parser. A negated option is one passed with a `-no` prefix. For example, `-nofoo` is the negated form of `-foo`. Negated options were originally added in the 0.6 release. The change here allows code to explicitly distinguish between cases like `-nofoo` and `-foo=0`, which was not possible previously. The option parser does not have any changed semantics as a result of this change, and existing code will parse options just as it did before. The motivation for this change is to provide a way to disable options that are otherwise not boolean options. For example, the `-debuglogfile` option is normally interpreted as a string, where the value is the log file name. With this change a user can pass in `-nodebuglogfile` and the code can see that it was explicitly negated, and use that to disable the log file. This change originally split out from #12689. Tree-SHA512: cd5a7354eb03d2d402863c7b69e512cad382781d9b8f18c1ab104fc46d45a712530818d665203082da39572c8a42313c5be09306dc2a7227cdedb20ef7314823
2 parents be299c4 + f7683cb commit 4490871

File tree

3 files changed

+137
-41
lines changed

3 files changed

+137
-41
lines changed

src/test/util_tests.cpp

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -185,17 +185,11 @@ BOOST_AUTO_TEST_CASE(util_FormatISO8601Time)
185185
BOOST_CHECK_EQUAL(FormatISO8601Time(1317425777), "23:36:17Z");
186186
}
187187

188-
class TestArgsManager : public ArgsManager
188+
struct TestArgsManager : public ArgsManager
189189
{
190-
public:
191-
std::map<std::string, std::string>& GetMapArgs()
192-
{
193-
return mapArgs;
194-
};
195-
const std::map<std::string, std::vector<std::string> >& GetMapMultiArgs()
196-
{
197-
return mapMultiArgs;
198-
};
190+
std::map<std::string, std::string>& GetMapArgs() { return mapArgs; }
191+
const std::map<std::string, std::vector<std::string> >& GetMapMultiArgs() { return mapMultiArgs; }
192+
const std::unordered_set<std::string>& GetNegatedArgs() { return m_negated_args; }
199193
};
200194

201195
BOOST_AUTO_TEST_CASE(util_ParseParameters)
@@ -223,6 +217,54 @@ BOOST_AUTO_TEST_CASE(util_ParseParameters)
223217
BOOST_CHECK(testArgs.GetArgs("-ccc").size() == 2);
224218
}
225219

220+
BOOST_AUTO_TEST_CASE(util_GetBoolArg)
221+
{
222+
TestArgsManager testArgs;
223+
const char *argv_test[] = {
224+
"ignored", "-a", "-nob", "-c=0", "-d=1", "-e=false", "-f=true"};
225+
testArgs.ParseParameters(7, (char**)argv_test);
226+
227+
// Each letter should be set.
228+
for (char opt : "abcdef")
229+
BOOST_CHECK(testArgs.IsArgSet({'-', opt}) || !opt);
230+
231+
// Nothing else should be in the map
232+
BOOST_CHECK(testArgs.GetMapArgs().size() == 6 &&
233+
testArgs.GetMapMultiArgs().size() == 6);
234+
235+
// The -no prefix should get stripped on the way in.
236+
BOOST_CHECK(!testArgs.IsArgSet("-nob"));
237+
238+
// The -b option is flagged as negated, and nothing else is
239+
BOOST_CHECK(testArgs.IsArgNegated("-b"));
240+
BOOST_CHECK(testArgs.GetNegatedArgs().size() == 1);
241+
BOOST_CHECK(!testArgs.IsArgNegated("-a"));
242+
243+
// Check expected values.
244+
BOOST_CHECK(testArgs.GetBoolArg("-a", false) == true);
245+
BOOST_CHECK(testArgs.GetBoolArg("-b", true) == false);
246+
BOOST_CHECK(testArgs.GetBoolArg("-c", true) == false);
247+
BOOST_CHECK(testArgs.GetBoolArg("-d", false) == true);
248+
BOOST_CHECK(testArgs.GetBoolArg("-e", true) == false);
249+
BOOST_CHECK(testArgs.GetBoolArg("-f", true) == false);
250+
}
251+
252+
BOOST_AUTO_TEST_CASE(util_GetBoolArgEdgeCases)
253+
{
254+
// Test some awful edge cases that hopefully no user will ever exercise.
255+
TestArgsManager testArgs;
256+
const char *argv_test[] = {"ignored", "-nofoo", "-foo", "-nobar=0"};
257+
testArgs.ParseParameters(4, (char**)argv_test);
258+
259+
// This was passed twice, second one overrides the negative setting.
260+
BOOST_CHECK(!testArgs.IsArgNegated("-foo"));
261+
BOOST_CHECK(testArgs.GetBoolArg("-foo", false) == true);
262+
263+
// A double negative is a positive.
264+
BOOST_CHECK(testArgs.IsArgNegated("-bar"));
265+
BOOST_CHECK(testArgs.GetBoolArg("-bar", false) == true);
266+
}
267+
226268
BOOST_AUTO_TEST_CASE(util_GetArg)
227269
{
228270
TestArgsManager testArgs;

src/util.cpp

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,6 @@
7070
#include <malloc.h>
7171
#endif
7272

73-
#include <boost/algorithm/string/case_conv.hpp> // for to_lower()
74-
#include <boost/algorithm/string/predicate.hpp> // for startswith() and endswith()
7573
#include <boost/interprocess/sync/file_lock.hpp>
7674
#include <boost/program_options/detail/config_file.hpp>
7775
#include <boost/thread.hpp>
@@ -432,21 +430,54 @@ bool DirIsWritable(const fs::path& directory)
432430
return true;
433431
}
434432

435-
/** Interpret string as boolean, for argument parsing */
433+
/**
434+
* Interpret a string argument as a boolean.
435+
*
436+
* The definition of atoi() requires that non-numeric string values like "foo",
437+
* return 0. This means that if a user unintentionally supplies a non-integer
438+
* argument here, the return value is always false. This means that -foo=false
439+
* does what the user probably expects, but -foo=true is well defined but does
440+
* not do what they probably expected.
441+
*
442+
* The return value of atoi() is undefined when given input not representable as
443+
* an int. On most systems this means string value between "-2147483648" and
444+
* "2147483647" are well defined (this method will return true). Setting
445+
* -txindex=2147483648 on most systems, however, is probably undefined.
446+
*
447+
* For a more extensive discussion of this topic (and a wide range of opinions
448+
* on the Right Way to change this code), see PR12713.
449+
*/
436450
static bool InterpretBool(const std::string& strValue)
437451
{
438452
if (strValue.empty())
439453
return true;
440454
return (atoi(strValue) != 0);
441455
}
442456

443-
/** Turn -noX into -X=0 */
444-
static void InterpretNegativeSetting(std::string& strKey, std::string& strValue)
457+
/**
458+
* Interpret -nofoo as if the user supplied -foo=0.
459+
*
460+
* This method also tracks when the -no form was supplied, and treats "-foo" as
461+
* a negated option when this happens. This can be later checked using the
462+
* IsArgNegated() method. One use case for this is to have a way to disable
463+
* options that are not normally boolean (e.g. using -nodebuglogfile to request
464+
* that debug log output is not sent to any file at all).
465+
*/
466+
void ArgsManager::InterpretNegatedOption(std::string& key, std::string& val)
445467
{
446-
if (strKey.length()>3 && strKey[0]=='-' && strKey[1]=='n' && strKey[2]=='o')
447-
{
448-
strKey = "-" + strKey.substr(3);
449-
strValue = InterpretBool(strValue) ? "0" : "1";
468+
if (key.substr(0, 3) == "-no") {
469+
bool bool_val = InterpretBool(val);
470+
if (!bool_val ) {
471+
// Double negatives like -nofoo=0 are supported (but discouraged)
472+
LogPrintf("Warning: parsed potentially confusing double-negative %s=%s\n", key, val);
473+
}
474+
key.erase(1, 2);
475+
m_negated_args.insert(key);
476+
val = bool_val ? "0" : "1";
477+
} else {
478+
// In an invocation like "bitcoind -nofoo -foo" we want to unmark -foo
479+
// as negated when we see the second option.
480+
m_negated_args.erase(key);
450481
}
451482
}
452483

@@ -455,34 +486,34 @@ void ArgsManager::ParseParameters(int argc, const char* const argv[])
455486
LOCK(cs_args);
456487
mapArgs.clear();
457488
mapMultiArgs.clear();
458-
459-
for (int i = 1; i < argc; i++)
460-
{
461-
std::string str(argv[i]);
462-
std::string strValue;
463-
size_t is_index = str.find('=');
464-
if (is_index != std::string::npos)
465-
{
466-
strValue = str.substr(is_index+1);
467-
str = str.substr(0, is_index);
489+
m_negated_args.clear();
490+
491+
for (int i = 1; i < argc; i++) {
492+
std::string key(argv[i]);
493+
std::string val;
494+
size_t is_index = key.find('=');
495+
if (is_index != std::string::npos) {
496+
val = key.substr(is_index + 1);
497+
key.erase(is_index);
468498
}
469499
#ifdef WIN32
470-
boost::to_lower(str);
471-
if (boost::algorithm::starts_with(str, "/"))
472-
str = "-" + str.substr(1);
500+
std::transform(key.begin(), key.end(), key.begin(), ::tolower);
501+
if (key[0] == '/')
502+
key[0] = '-';
473503
#endif
474504

475-
if (str[0] != '-')
505+
if (key[0] != '-')
476506
break;
477507

478-
// Interpret --foo as -foo.
479-
// If both --foo and -foo are set, the last takes effect.
480-
if (str.length() > 1 && str[1] == '-')
481-
str = str.substr(1);
482-
InterpretNegativeSetting(str, strValue);
508+
// Transform --foo to -foo
509+
if (key.length() > 1 && key[1] == '-')
510+
key.erase(0, 1);
511+
512+
// Transform -nofoo to -foo=0
513+
InterpretNegatedOption(key, val);
483514

484-
mapArgs[str] = strValue;
485-
mapMultiArgs[str].push_back(strValue);
515+
mapArgs[key] = val;
516+
mapMultiArgs[key].push_back(val);
486517
}
487518
}
488519

@@ -500,6 +531,12 @@ bool ArgsManager::IsArgSet(const std::string& strArg) const
500531
return mapArgs.count(strArg);
501532
}
502533

534+
bool ArgsManager::IsArgNegated(const std::string& strArg) const
535+
{
536+
LOCK(cs_args);
537+
return m_negated_args.find(strArg) != m_negated_args.end();
538+
}
539+
503540
std::string ArgsManager::GetArg(const std::string& strArg, const std::string& strDefault) const
504541
{
505542
LOCK(cs_args);
@@ -711,7 +748,7 @@ void ArgsManager::ReadConfigFile(const std::string& confPath)
711748
// Don't overwrite existing settings so command line settings override bitcoin.conf
712749
std::string strKey = std::string("-") + it->string_key;
713750
std::string strValue = it->value[0];
714-
InterpretNegativeSetting(strKey, strValue);
751+
InterpretNegatedOption(strKey, strValue);
715752
if (mapArgs.count(strKey) == 0)
716753
mapArgs[strKey] = strValue;
717754
mapMultiArgs[strKey].push_back(strValue);

src/util.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include <map>
2626
#include <stdint.h>
2727
#include <string>
28+
#include <unordered_set>
2829
#include <vector>
2930

3031
#include <boost/signals2/signal.hpp>
@@ -224,6 +225,8 @@ class ArgsManager
224225
mutable CCriticalSection cs_args;
225226
std::map<std::string, std::string> mapArgs;
226227
std::map<std::string, std::vector<std::string>> mapMultiArgs;
228+
std::unordered_set<std::string> m_negated_args;
229+
227230
public:
228231
void ParseParameters(int argc, const char*const argv[]);
229232
void ReadConfigFile(const std::string& confPath);
@@ -244,6 +247,15 @@ class ArgsManager
244247
*/
245248
bool IsArgSet(const std::string& strArg) const;
246249

250+
/**
251+
* Return true if the argument was originally passed as a negated option,
252+
* i.e. -nofoo.
253+
*
254+
* @param strArg Argument to get (e.g. "-foo")
255+
* @return true if the argument was passed negated
256+
*/
257+
bool IsArgNegated(const std::string& strArg) const;
258+
247259
/**
248260
* Return string argument or default value
249261
*
@@ -292,6 +304,11 @@ class ArgsManager
292304
// Forces an arg setting. Called by SoftSetArg() if the arg hasn't already
293305
// been set. Also called directly in testing.
294306
void ForceSetArg(const std::string& strArg, const std::string& strValue);
307+
308+
private:
309+
310+
// Munge -nofoo into -foo=0 and track the value as negated.
311+
void InterpretNegatedOption(std::string &key, std::string &val);
295312
};
296313

297314
extern ArgsManager gArgs;

0 commit comments

Comments
 (0)