Browse Source

cfg, opts: improve contextual options handling

The MMGen Project 1 month ago
parent
commit
dc028988cb
5 changed files with 95 additions and 77 deletions
  1. 0 3
      mmgen/autosign.py
  2. 26 23
      mmgen/cfg.py
  3. 6 6
      mmgen/help/__init__.py
  4. 25 16
      mmgen/opts.py
  5. 38 29
      test/cmdtest_d/ct_help.py

+ 0 - 3
mmgen/autosign.py

@@ -516,9 +516,6 @@ class Autosign:
 		if any(k in cfg._uopts for k in ('help', 'longhelp')):
 			return
 
-		if 'coin' in cfg._uopts:
-			die(1, '--coin option not supported with this command.  Use --coins instead')
-
 		self.coins = cfg.coins.upper().split(',') if cfg.coins else []
 
 		if cfg.xmrwallets and not 'XMR' in self.coins:

+ 26 - 23
mmgen/cfg.py

@@ -56,29 +56,32 @@ class GlobalConstants(Lockable):
 	btc_fork_rpc_coins = ('btc', 'bch', 'ltc')
 	eth_fork_coins = ('eth', 'etc')
 
-	_cc = namedtuple('cmd_cap', ['proto', 'rpc', 'coin', 'caps', 'platforms'])
+	# ‘use_coin_opt’ must be False if ‘coin_codes’ is set
+	_cc = namedtuple('cmd_cap', ['proto', 'rpc', 'use_coin_opt', 'coin_codes', 'caps', 'platforms'])
 	cmd_caps_data = {
-		'addrgen':      _cc(True,  False, None,  [],      'lmw'),
-		'addrimport':   _cc(True,  True,  'R',   ['tw'],  'lmw'),
-		'autosign':     _cc(True,  True,  'r',   ['rpc'], 'lm'),
-		'keygen':       _cc(True,  False, None,  [],      'lmw'),
-		'msg':          _cc(True,  True,  'R',   ['msg'], 'lmw'),
-		'passchg':      _cc(False, False, None,  [],      'lmw'),
-		'passgen':      _cc(False, False, None,  [],      'lmw'),
-		'regtest':      _cc(True,  True,  'b',   ['tw'],  'lmw'),
-		'seedjoin':     _cc(False, False, None,  [],      'lmw'),
-		'seedsplit':    _cc(False, False, None,  [],      'lmw'),
-		'subwalletgen': _cc(False, False, None,  [],      'lmw'),
-		'tool':         _cc(True,  True,  None,  [],      'lmw'),
-		'txbump':       _cc(True,  True,  'R',   ['tw'],  'lmw'),
-		'txcreate':     _cc(True,  True,  'R',   ['tw'],  'lmw'),
-		'txdo':         _cc(True,  True,  'R',   ['tw'],  'lmw'),
-		'txsend':       _cc(True,  True,  'R',   ['tw'],  'lmw'),
-		'txsign':       _cc(True,  True,  'R',   ['tw'],  'lmw'),
-		'walletchk':    _cc(False, False, None,  [],      'lmw'),
-		'walletconv':   _cc(False, False, None,  [],      'lmw'),
-		'walletgen':    _cc(False, False, None,  [],      'lmw'),
-		'xmrwallet':    _cc(True,  True,  'xmr', ['rpc'], 'lmw'),
+		'addrgen':      _cc(True,  False, True,  None,    [],      'lmw'),
+		'addrimport':   _cc(True,  True,  True,  None,    ['tw'],  'lmw'),
+		'autosign':     _cc(True,  True,  False, '-rRb',  ['rpc'], 'lm'),
+		'keygen':       _cc(True,  False, True,  None,    [],      'lmw'),
+		'msg':          _cc(True,  True,  True,  None,    ['msg'], 'lmw'),
+		'passchg':      _cc(False, False, False, None,    [],      'lmw'),
+		'passgen':      _cc(False, False, False, None,    [],      'lmw'),
+		'regtest':      _cc(True,  True,  True,  None,    ['tw'],  'lmw'),
+		'seedjoin':     _cc(False, False, False, None,    [],      'lmw'),
+		'seedsplit':    _cc(False, False, False, None,    [],      'lmw'),
+		'subwalletgen': _cc(False, False, False, None,    [],      'lmw'),
+		'swaptxcreate': _cc(True,  True,  False, '-rRb',  ['tw'],  'lmw'),
+		'swaptxdo':     _cc(True,  True,  False, '-rRb',  ['tw'],  'lmw'),
+		'tool':         _cc(True,  True,  True,  None,    [],      'lmw'),
+		'txbump':       _cc(True,  True,  True,  None,    ['tw'],  'lmw'),
+		'txcreate':     _cc(True,  True,  True,  None,    ['tw'],  'lmw'),
+		'txdo':         _cc(True,  True,  True,  None,    ['tw'],  'lmw'),
+		'txsend':       _cc(True,  True,  True,  None,    ['tw'],  'lmw'),
+		'txsign':       _cc(True,  True,  True,  None,    ['tw'],  'lmw'),
+		'walletchk':    _cc(False, False, False, None,    [],      'lmw'),
+		'walletconv':   _cc(False, False, False, None,    [],      'lmw'),
+		'walletgen':    _cc(False, False, False, None,    [],      'lmw'),
+		'xmrwallet':    _cc(True,  True,  False, '-r',    ['rpc'], 'lmw'),
 	}
 
 	prog_name = os.path.basename(sys.argv[0])
@@ -475,7 +478,7 @@ class Config(Lockable):
 			'_data_dir_root_override',
 			self._uopts.pop('data_dir', None))
 
-		if parse_only and not any(k in self._uopts for k in ['help', 'longhelp']):
+		if parse_only and not any(k in self._uopts for k in ['help', 'longhelp', 'usage']):
 			return
 
 		# Step 2: set cfg from user-supplied data, skipping auto opts; set type from corresponding

+ 6 - 6
mmgen/help/__init__.py

@@ -140,14 +140,14 @@ class CmdHelp_v2(CmdHelp_v1):
 	def gen_text(self, opts):
 		from ..opts import cmd_opts_v2_help_pat
 		skipping = False
-		coin_filter_codes = opts.global_filter_codes.coin
-		cmd_filter_codes = opts.opts_data['filter_codes']
+		coin_codes = opts.global_filter_codes.coin
+		cmd_codes = opts.opts_data['filter_codes']
 		for line in opts.opts_data['text']['options'][1:].rstrip().splitlines():
 			m = cmd_opts_v2_help_pat.match(line)
 			if m[1] == '+':
 				if not skipping:
 					yield line[6:]
-			elif m[1] in coin_filter_codes and m[2] in cmd_filter_codes:
+			elif (coin_codes is None or m[1] in coin_codes) and m[2] in cmd_codes:
 				yield '{} --{} {}'.format(
 					(f'-{m[3]},', '   ')[m[3] == '-'],
 					m[4],
@@ -165,14 +165,14 @@ class GlobalHelp(Help):
 	def gen_text(self, opts):
 		from ..opts import global_opts_help_pat
 		skipping = False
-		coin_filter_codes = opts.global_filter_codes.coin
-		cmd_filter_codes = opts.global_filter_codes.cmd
+		coin_codes = opts.global_filter_codes.coin
+		cmd_codes = opts.global_filter_codes.cmd
 		for line in opts.global_opts_data['text']['options'][1:].rstrip().splitlines():
 			m = global_opts_help_pat.match(line)
 			if m[1] == '+':
 				if not skipping:
 					yield line[4:]
-			elif m[1] in coin_filter_codes and m[2] in cmd_filter_codes:
+			elif (coin_codes is None or m[1] in coin_codes) and (cmd_codes is None or m[2] in cmd_codes):
 				yield '  --{} {}'.format(m[3], m[5]) if m[3] else m[5]
 				skipping = False
 			else:

+ 25 - 16
mmgen/opts.py

@@ -171,17 +171,22 @@ def parse_opts(cfg, opts_data, global_opts_data, global_filter_codes, need_proto
 
 	def parse_v2():
 		cmd_filter_codes = opts_data['filter_codes']
+		coin_codes = global_filter_codes.coin
 		for line in opts_data['text']['options'].splitlines():
 			m = cmd_opts_v2_pat.match(line)
-			if m and m[1] in global_filter_codes.coin and m[2] in cmd_filter_codes:
+			if m and (coin_codes is None or m[1] in coin_codes) and m[2] in cmd_filter_codes:
 				ret = opt_tuple(m[4].replace('-', '_'), m[5] == '=')
 				yield (m[3], ret)
 				yield (m[4], ret)
 
 	def parse_global():
+		coin_codes = global_filter_codes.coin
+		cmd_codes = global_filter_codes.cmd
 		for line in global_opts_data['text']['options'].splitlines():
 			m = global_opts_pat.match(line)
-			if m and m[1] in global_filter_codes.coin and m[2] in global_filter_codes.cmd:
+			if m and (
+					(coin_codes is None or m[1] in coin_codes) and
+					(cmd_codes is None or m[2] in cmd_codes)):
 				yield (m[3], opt_tuple(m[3].replace('-', '_'), m[4] == '='))
 
 	opts = tuple((parse_v2 if 'filter_codes' in opts_data else parse_v1)()) + tuple(parse_global())
@@ -294,7 +299,7 @@ class UserOpts(Opts):
 			'options': """
 			-- --accept-defaults      Accept defaults at all prompts
 			hp --cashaddr=0|1         Display addresses in cashaddr format (default: 1)
-			-p --coin=c               Choose coin unit. Default: BTC. Current choice: {cu_dfl}
+			-c --coin=c               Choose coin unit. Default: BTC. Current choice: {cu_dfl}
 			er --token=t              Specify an ERC20 token by address or symbol
 			-- --color=0|1            Disable or enable color output (default: 1)
 			-- --columns=N            Force N columns of output with certain commands
@@ -351,6 +356,10 @@ class UserOpts(Opts):
 	@staticmethod
 	def get_global_filter_codes(need_proto):
 		"""
+		Enable options based on the value of --coin and name of executable
+
+		Both must produce a matching code list, or None, for the option to be enabled
+
 		Coin codes:
 		  'b' - Bitcoin or Bitcoin code fork supporting RPC
 		  'R' - Bitcoin or Ethereum code fork supporting RPC
@@ -360,26 +369,26 @@ class UserOpts(Opts):
 		  '-' - other coin
 		Cmd codes:
 		  'p' - proto required
+		  'c' - proto required, --coin recognized
 		  'r' - RPC required
 		  '-' - no capabilities required
 		"""
 		ret = namedtuple('global_filter_codes', ['coin', 'cmd'])
 		if caps := gc.cmd_caps:
-			coin = caps.coin if caps.coin and len(caps.coin) > 1 else get_coin()
+			coin = get_coin() if caps.use_coin_opt else None
+			# a return value of None removes the filter, enabling all options for the given criterion
 			return ret(
-				coin = (
-					('-', 'r', 'R', 'b', 'h') if coin == 'bch' else
-					('-', 'r', 'R', 'b') if coin in gc.btc_fork_rpc_coins else
-					('-', 'r', 'R', 'e') if coin in gc.eth_fork_coins else
-					('-', 'r') if coin in gc.rpc_coins else
-					('-')),
+				coin = caps.coin_codes or (
+					None if coin is None else
+					['-', 'r', 'R', 'b', 'h'] if coin == 'bch' else
+					['-', 'r', 'R', 'b'] if coin in gc.btc_fork_rpc_coins else
+					['-', 'r', 'R', 'e'] if coin in gc.eth_fork_coins else
+					['-', 'r'] if coin in gc.rpc_coins else
+					['-']),
 				cmd = (
 					['-']
 					+ (['r'] if caps.rpc else [])
-					+ (['p'] if caps.proto else [])
+					+ (['p', 'c'] if caps.proto and caps.use_coin_opt else ['p'] if caps.proto else [])
 				))
-		else:
-			return ret(
-				coin = ('-', 'r', 'R', 'b', 'h', 'e'),
-				cmd = ('-', 'r', 'p')
-			)
+		else: # unmanaged command: enable everything
+			return ret(None, None)

+ 38 - 29
test/cmdtest_d/ct_help.py

@@ -41,26 +41,27 @@ class CmdTestHelp(CmdTestBase):
 	)
 
 	def usage1(self):
-		t = self.spawn('mmgen-walletgen', ['--usage'], no_passthru_opts=True)
-		t.expect('USAGE: mmgen-walletgen')
-		return t
+		return self._usage('walletgen', ['--usage'], True, False, 0)
 
 	def usage2(self):
-		cmd = 'xmrwallet' if self.coin == 'xmr' else 'txcreate'
-		t = self.spawn(f'mmgen-{cmd}', ['--usage', f'--coin={self.coin}'], no_passthru_opts=True)
-		t.expect(f'USAGE: mmgen-{cmd}')
-		return t
+		return self._usage('tool' if self.coin == 'xmr' else 'txcreate', ['--usage'], True, True, 0)
 
 	def usage3(self):
-		t = self.spawn('mmgen-walletgen', ['foo'], exit_val=1, no_passthru_opts=True)
-		t.expect('USAGE: mmgen-walletgen')
-		return t
+		return self._usage('walletgen', ['foo'], True, False, 1)
 
 	def usage4(self):
-		cmd = 'xmrwallet' if self.coin == 'xmr' else 'addrgen'
-		t = self.spawn(f'mmgen-{cmd}', [f'--coin={self.coin}'], exit_val=1, no_passthru_opts=True)
-		t.expect(f'USAGE: mmgen-{cmd}')
-		return t
+		return self._usage('tool' if self.coin == 'xmr' else 'addrgen', [], True, True, 1)
+
+	def _usage(self, cmd_arg, args, no_passthru_opts, add_coin_opt, exit_val):
+		if cmd := (None if self._gen_skiplist(cmd_arg) else cmd_arg):
+			t = self.spawn(
+				f'mmgen-{cmd}',
+				([f'--coin={self.coin}'] if add_coin_opt else []) + args,
+				exit_val = exit_val,
+				no_passthru_opts = no_passthru_opts)
+			t.expect(f'USAGE: mmgen-{cmd}')
+			return t
+		return 'skip'
 
 	def version(self):
 		t = self.spawn('mmgen-tool', ['--version'], exit_val=0)
@@ -97,29 +98,37 @@ class CmdTestHelp(CmdTestBase):
 		t.skip_ok = True
 		return t
 
+	def _gen_skiplist(self, scripts):
+		def gen(scripts):
+			if isinstance(scripts, str):
+				scripts = [scripts]
+			for script in scripts:
+				d = gc.cmd_caps_data[script]
+				if sys.platform == 'win32' and 'w' not in d.platforms:
+					yield script
+				elif not (d.use_coin_opt or self.proto.coin.lower() == 'btc'):
+					yield script
+				else:
+					for cap in d.caps:
+						if cap not in self.proto.mmcaps:
+							yield script
+							break
+		return set(gen(scripts))
+
 	def helpscreens(self, arg='--help', scripts=(), expect='USAGE:.*OPTIONS:', pager=True):
 
 		scripts = list(scripts or gc.cmd_caps_data)
 
-		def gen_skiplist():
-			for script in scripts:
-				d = gc.cmd_caps_data[script]
-				for cap in d.caps:
-					if cap not in self.proto.mmcaps:
-						yield script
-						break
-				else:
-					if sys.platform == 'win32' and 'w' not in d.platforms:
-						yield script
-					elif d.coin and len(d.coin) > 1 and self.proto.coin.lower() not in (d.coin, 'btc'):
-						yield script
+		cmdlist = sorted(set(scripts) - self._gen_skiplist(scripts))
 
-		for cmdname in sorted(set(scripts) - set(list(gen_skiplist()))):
+		for cmdname in cmdlist:
+			cmd_caps = gc.cmd_caps_data[cmdname]
+			assert cmd_caps, cmdname
 			t = self.spawn(
 				f'mmgen-{cmdname}',
 				[arg],
 				extra_desc       = f'(mmgen-{cmdname})',
-				no_passthru_opts = not gc.cmd_caps_data[cmdname].proto)
+				no_passthru_opts = not cmd_caps.use_coin_opt)
 			t.expect(expect, regex=True)
 			if pager and t.pexpect_spawn:
 				time.sleep(0.2)
@@ -128,7 +137,7 @@ class CmdTestHelp(CmdTestBase):
 			t.ok()
 			t.skip_ok = True
 
-		return t
+		return 'silent'
 
 	def longhelpscreens(self):
 		return self.helpscreens(arg='--longhelp', expect='USAGE:.*GLOBAL OPTIONS:')