Browse Source

support negated command-line options

- all options without parameters may be negated by prefixing the option name
  with ‘no-’
- if the option name itself begins with ‘no-’, then the option is negated
  by removing the ‘no-’ prefix
- negation may be used to override options set in the cfg file
- command-line options may also be overridden, with the last-listed option
  taking precedence
- as with ordinary options, substring matching is supported

Examples:

    OPTION        NEGATED OPTION
    --no-license  --license
    --no-license  --lic
    --quiet       --no-quiet
    --quiet       --no-q

Testing:

    $ test/cmdtest.py opts cfgfile
The MMGen Project 1 month ago
parent
commit
df3559d420
6 changed files with 63 additions and 3 deletions
  1. 1 1
      mmgen/data/version
  2. 16 0
      mmgen/opts.py
  3. 19 0
      test/cmdtest_d/ct_cfgfile.py
  4. 20 0
      test/cmdtest_d/ct_opts.py
  5. 4 1
      test/misc/cfg_main.py
  6. 3 1
      test/misc/opts_main.py

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-15.1.dev6
+15.1.dev7

+ 16 - 0
mmgen/opts.py

@@ -25,6 +25,16 @@ from collections import namedtuple
 
 from .cfg import gc
 
+def negated_opts(opts, data={}):
+	if data:
+		return data
+	else:
+		data.update(dict(
+			((k[3:] if k.startswith('no-') else f'no-{k}'), v)
+				for k, v in opts.items()
+					if len(k) > 1 and not v.has_parm))
+		return data
+
 def get_opt_by_substring(opt, opts):
 	matches = [o for o in opts if o.startswith(opt)]
 	if len(matches) == 1:
@@ -67,6 +77,12 @@ def process_uopts(opts_data, opts):
 						if parm:
 							die('CmdlineOptError', f'option --{_opt} requires no parameter')
 						yield (opts[_opt].name, True)
+				elif (
+						(_opt := opt) in negated_opts(opts)
+						or (_opt := get_opt_by_substring(_opt, negated_opts(opts)))):
+					if parm:
+						die('CmdlineOptError', f'option --{_opt} requires no parameter')
+					yield (negated_opts(opts)[_opt].name, False)
 				else:
 					die('CmdlineOptError', f'--{opt}: unrecognized option')
 			elif arg[0] == '-' and len(arg) > 1:

+ 19 - 0
test/cmdtest_d/ct_cfgfile.py

@@ -38,6 +38,8 @@ class CmdTestCfgFile(CmdTestBase):
 		('coin_specific_vars',       (40, 'setting coin-specific vars', [])),
 		('chain_names',              (40, 'setting chain names', [])),
 		('mnemonic_entry_modes',     (40, 'setting mnemonic entry modes', [])),
+		('opt_override1',            (40, 'cfg file opts not overridden', [])),
+		('opt_override2',            (40, 'negative cmdline opts overriding cfg file opts', [])),
 	)
 
 	def __init__(self, trunner, cfgs, spawn):
@@ -276,3 +278,20 @@ class CmdTestCfgFile(CmdTestBase):
 
 		t.skip_ok = True
 		return t
+
+	def opt_override1(self):
+		self.write_to_cfgfile('usr', ['no_license true', 'scroll true'])
+		t = self.spawn_test(
+			args = ['print_cfg', 'scroll', 'no_license'])
+		t.expect('scroll: True')
+		t.expect('no_license: True')
+		return t
+
+	def opt_override2(self):
+		self.write_to_cfgfile('usr', ['no_license true', 'scroll true'])
+		t = self.spawn_test(
+			args = ['print_cfg', 'scroll', 'no_license'],
+			opts = ['--no-scrol', '--lic'])
+		t.expect('scroll: False')
+		t.expect('no_license: False')
+		return t

+ 20 - 0
test/cmdtest_d/ct_opts.py

@@ -41,6 +41,11 @@ class CmdTestOpts(CmdTestBase):
 		('opt_good17',           (41, 'good cmdline opt (param with equals signs)', [])),
 		('opt_good18',           (41, 'good cmdline opt (param with equals signs)', [])),
 		('opt_good19',           (41, 'good cmdline opt (param with equals signs)', [])),
+		('opt_good20',           (41, 'good cmdline opt (opt + negated opt)', [])),
+		('opt_good21',           (41, 'good cmdline opt (negated negative opt)', [])),
+		('opt_good22',           (41, 'good cmdline opt (opt + negated opt [substring])', [])),
+		('opt_good23',           (41, 'good cmdline opt (negated negative opt [substring])', [])),
+		('opt_good24',           (41, 'good cmdline opt (negated opt + opt [substring])', [])),
 		('opt_bad_param',        (41, 'bad global opt (--pager=1)', [])),
 		('opt_bad_infile',       (41, 'bad infile parameter', [])),
 		('opt_bad_outdir',       (41, 'bad outdir parameter', [])),
@@ -208,6 +213,21 @@ class CmdTestOpts(CmdTestBase):
 	def opt_good19(self):
 		return self.check_vals(['-x', 'x=1,y=2,z=3'], (('cfg.point', 'x=1,y=2,z=3'),))
 
+	def opt_good20(self):
+		return self.check_vals(['--pager', '--no-pager'], (('cfg.pager', 'False'),))
+
+	def opt_good21(self):
+		return self.check_vals(['--foobleize'], (('cfg.no_foobleize', 'False'),))
+
+	def opt_good22(self):
+		return self.check_vals(['--quiet', '--no-q'], (('cfg.quiet', 'False'),))
+
+	def opt_good23(self):
+		return self.check_vals(['--foobl'], (('cfg.no_foobleize', 'False'),))
+
+	def opt_good24(self):
+		return self.check_vals(['--no-pag', '--pag'], (('cfg.pager', 'True'),))
+
 	def opt_bad_param(self):
 		return self.do_run(['--pager=1'], 'no parameter', 1)
 

+ 4 - 1
test/misc/cfg_main.py

@@ -21,7 +21,10 @@ msg(f'Sys cfg file:    {os.path.relpath(cf_sys.fn)}')
 msg(f'Sample cfg file: {os.path.relpath(cf_sample.fn)}')
 
 if op:
-	if op == 'parse_test':
+	if op == 'print_cfg':
+		for name in args:
+			msg('{} {}'.format(name+':', getattr(cfg, name)))
+	elif op == 'parse_test':
 		ps = cf_sample.get_lines()
 		msg(f'parsed chunks: {len(ps)}')
 		pu = cf_usr.get_lines()

+ 3 - 1
test/misc/opts_main.py

@@ -15,6 +15,7 @@ opts_data = {
 -d, --outdir=      d  Use outdir 'd'
 -C, --print-checksum  Print a checksum
 -E, --fee-estimate-mode=M Specify the network fee estimate mode.
+-F, --no-foobleize    Do not foobleize the output, even on user request
 -H, --hidden-incog-input-params=f,o  Read hidden incognito data from file
                       'f' at offset 'o' (comma-separated)
 -k, --keep-label      Reuse label of input wallet for output wallet
@@ -68,7 +69,8 @@ for k in (
 		'max_temp',
 		'coin',
 		'pager',
-		'point'):
+		'point',
+		'no_foobleize'):
 	msg('{:30} {}'.format(f'cfg.{k}:', getattr(cfg, k)))
 
 msg('')