cfg, opts: improve contextual options handling

This commit is contained in:
The MMGen Project 2025-02-15 09:54:18 +00:00
commit dc028988cb
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
5 changed files with 95 additions and 77 deletions

View file

@ -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:

View file

@ -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

View file

@ -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:

View file

@ -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)

View file

@ -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:')