From dc028988cbdd5c37fb08435e17a277cab4659415 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Sat, 15 Feb 2025 09:54:18 +0000 Subject: [PATCH] cfg, opts: improve contextual options handling --- mmgen/autosign.py | 3 -- mmgen/cfg.py | 49 ++++++++++++++-------------- mmgen/help/__init__.py | 12 +++---- mmgen/opts.py | 41 ++++++++++++++---------- test/cmdtest_d/ct_help.py | 67 ++++++++++++++++++++++----------------- 5 files changed, 95 insertions(+), 77 deletions(-) diff --git a/mmgen/autosign.py b/mmgen/autosign.py index 43756b9f..e256cacb 100755 --- a/mmgen/autosign.py +++ b/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: diff --git a/mmgen/cfg.py b/mmgen/cfg.py index 2c361f5c..30e95e4e 100755 --- a/mmgen/cfg.py +++ b/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 diff --git a/mmgen/help/__init__.py b/mmgen/help/__init__.py index 931eb377..bd28024f 100755 --- a/mmgen/help/__init__.py +++ b/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: diff --git a/mmgen/opts.py b/mmgen/opts.py index 6737aa4a..cb811a5b 100755 --- a/mmgen/opts.py +++ b/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) diff --git a/test/cmdtest_d/ct_help.py b/test/cmdtest_d/ct_help.py index dc98d15f..039c9b75 100755 --- a/test/cmdtest_d/ct_help.py +++ b/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:')