opts, help: refactor, parse cmdline opts natively, filter global opts

- command-line options are now parsed natively, without use of the getopt module
- global options and --longhelp helpscreen are now contextual, depending on coin
  and executed command
- commands invoked with out-of-context global options (e.g. `mmgen-walletgen
  --coin=btc`) now fail with an ‘unrecognized option’ error

Testing:

    $ test/test-release.sh help
    $ test/cmdtest.py opts
This commit is contained in:
The MMGen Project 2024-10-08 12:56:02 +00:00
commit d7e3b55e3b
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
14 changed files with 785 additions and 565 deletions

View file

@ -51,7 +51,10 @@ class GlobalConstants(Lockable):
min_time_precision = 18
# must match CoinProtocol.coins
core_coins = ('btc','bch','ltc','eth','etc','zec','xmr')
core_coins = ('btc', 'bch', 'ltc', 'eth', 'etc', 'zec', 'xmr')
rpc_coins = ('btc', 'bch', 'ltc', 'eth', 'etc', 'xmr')
btc_fork_rpc_coins = ('btc', 'bch', 'ltc')
eth_fork_coins = ('eth', 'etc')
_cc = namedtuple('cmd_cap', ['proto', 'rpc', 'coin', 'caps', 'platforms'])
cmd_caps_data = {
@ -461,8 +464,8 @@ class Config(Lockable):
opts_data = opts_data,
init_opts = init_opts,
opt_filter = opt_filter,
parse_only = parse_only,
parsed_opts = parsed_opts )
parsed_opts = parsed_opts,
need_proto = need_proto)
self._uopt_desc = 'command-line option'
else:
if cfg is None:
@ -575,7 +578,8 @@ class Config(Lockable):
def _post_init(self):
if self.help or self.longhelp:
self._opts.init_bottom(self) # exits
from .help import print_help
print_help(self, self._opts) # exits
del self._opts
def _usage(self):

View file

@ -1 +1 @@
September 2024
October 2024

View file

@ -1 +1 @@
15.1.dev1
15.1.dev2

View file

@ -48,6 +48,7 @@ class FileNotFound(Exception): mmcode = 1
class InvalidPasswdFormat(Exception): mmcode = 1
class CfgFileParseError(Exception): mmcode = 1
class UserOptError(Exception): mmcode = 1
class CmdlineOptError(Exception): mmcode = 1
class NoLEDSupport(Exception): mmcode = 1
class MsgFileFailedSID(Exception): mmcode = 1
class TestSuiteException(Exception): mmcode = 1

View file

@ -20,7 +20,7 @@
help: help notes for MMGen suite commands
"""
import sys
import sys, re
from ..cfg import gc
@ -60,261 +60,110 @@ def usage(cfg):
print(make_usage_str(cfg, caller='user'))
sys.exit(0)
def help_notes_func(proto,cfg,k):
class Help:
def fee_spec_letters(use_quotes=False):
cu = proto.coin_amt.units
sep,conj = ((',',' or '),("','","' or '"))[use_quotes]
return sep.join(u[0] for u in cu[:-1]) + ('',conj)[len(cu)>1] + cu[-1][0]
def make(self, cfg, opts, proto):
def fee_spec_names():
cu = proto.coin_amt.units
return ', '.join(cu[:-1]) + ('',' and ')[len(cu)>1] + cu[-1] + ('',',\nrespectively')[len(cu)>1]
def gen_arg_tuple(func, text):
def coind_exec():
from ..daemon import CoinDaemon
return (
CoinDaemon(cfg,proto.coin).exec_fn if proto.coin in CoinDaemon.coins else 'bitcoind' )
def help_notes(k):
import importlib
return getattr(importlib.import_module(
f'{opts.help_pkg}.help_notes').help_notes(proto, cfg), k)()
class help_notes:
def help_mod(modname):
import importlib
return importlib.import_module(
f'{opts.help_pkg}.{modname}').help(proto, cfg)
def dfl_twname():
from ..proto.btc.rpc import BitcoinRPCClient
return BitcoinRPCClient.dfl_twname
d = {
'proto': proto,
'help_notes': help_notes,
'help_mod': help_mod,
'cfg': cfg,
}
for arg in func.__code__.co_varnames:
yield d[arg] if arg in d else text
def MasterShareIdx():
from ..seedsplit import MasterShareIdx
return MasterShareIdx
def gen_output():
yield ' {} {}'.format(gc.prog_name.upper() + ':', text['desc'].strip())
yield make_usage_str(cfg, caller='help')
yield help_type.upper().replace('_', ' ') + ':'
def tool_help():
from ..tool.help import main_help
return main_help()
# process code for options
opts_text = nl.join(self.gen_text(opts))
if help_type in code:
yield code[help_type](*tuple(gen_arg_tuple(code[help_type], opts_text)))
else:
yield opts_text
def dfl_subseeds():
from ..subseed import SubSeedList
return str(SubSeedList.dfl_len)
# process code for notes
if help_type == 'options' and 'notes' in text:
if 'notes' in code:
yield from code['notes'](*tuple(gen_arg_tuple(code['notes'], text['notes']))).splitlines()
else:
yield from text['notes'].splitlines()
def dfl_seed_len():
from ..seed import Seed
return str(Seed.dfl_len)
text = opts.opts_data['text']
code = opts.opts_data['code']
help_type = self.help_type
nl = '\n '
def password_formats():
from ..passwdlist import PasswordList
pwi_fs = '{:8} {:1} {:26} {:<7} {:<7} {}'
return '\n '.join(
[pwi_fs.format('Code','','Description','Min Len','Max Len','Default Len')] +
[pwi_fs.format(k,'-',v.desc,v.min_len,v.max_len,v.dfl_len) for k,v in PasswordList.pw_info.items()]
)
return nl.join(gen_output()) + '\n'
def dfl_mmtype():
from ..addr import MMGenAddrType
return "'{}' or '{}'".format(
proto.dfl_mmtype,
MMGenAddrType.mmtypes[proto.dfl_mmtype].name )
class CmdHelp(Help):
def address_types():
from ..addr import MMGenAddrType
return '\n '.join([
"'{}','{:<12} - {}".format( k, v.name+"'", v.desc )
for k,v in MMGenAddrType.mmtypes.items()
])
help_type = 'options'
def fmt_codes():
from ..wallet import format_fmt_codes
return '\n '.join( format_fmt_codes().splitlines() )
def gen_text(self, opts):
opt_filter = opts.opt_filter
from ..opts import cmd_opts_pat
skipping = False
for line in opts.opts_data['text']['options'].strip().splitlines():
if m := cmd_opts_pat.match(line):
if opt_filter:
if m[1] in opt_filter:
skipping = False
else:
skipping = True
continue
yield '{} --{} {}'.format(
(f'-{m[1]},', ' ')[m[1] == '-'],
m[2],
m[4])
elif not skipping:
yield line
def coin_id():
return proto.coin_id
class GlobalHelp(Help):
def keygen_backends():
from ..keygen import get_backends
from ..addr import MMGenAddrType
backends = get_backends(
MMGenAddrType(proto,cfg.type or proto.dfl_mmtype).pubkey_type
)
return ' '.join( f'{n}:{k}{" [default]" if n==1 else ""}' for n,k in enumerate(backends,1) )
help_type = 'global_options'
def coind_exec():
return coind_exec()
def gen_text(self, opts):
from ..opts import global_opts_pat
for line in opts.global_opts_data['text'][1:-2].splitlines():
if m := global_opts_pat.match(line):
if m[1] in opts.global_opts_filter.coin and m[2] in opts.global_opts_filter.cmd:
yield ' --{} {}'.format(m[3], m[5])
skipping = False
else:
skipping = True
elif not skipping:
yield line[4:]
def coin_daemon_network_ids():
from ..daemon import CoinDaemon
from ..util import fmt_list
return fmt_list(CoinDaemon.get_network_ids(cfg),fmt='bare')
def print_help(cfg, opts):
def rel_fee_desc():
from ..tx import BaseTX
return BaseTX(cfg=cfg,proto=proto).rel_fee_desc
from ..protocol import init_proto_from_cfg
proto = init_proto_from_cfg(cfg, need_amt=True)
def fee_spec_letters():
return fee_spec_letters()
if not 'code' in opts.opts_data:
opts.opts_data['code'] = {}
def fee():
from ..tx import BaseTX
return """
FEE SPECIFICATION
if cfg.help:
cls = CmdHelp
else:
opts.opts_data['code']['global_options'] = opts.global_opts_data['code']
cls = GlobalHelp
Transaction fees, both on the command line and at the interactive prompt, may
be specified as either absolute {c} amounts, using a plain decimal number, or
as {r}, using an integer followed by '{l}', for {u}.
""".format(
c = proto.coin,
r = BaseTX(cfg=cfg,proto=proto).rel_fee_desc,
l = fee_spec_letters(use_quotes=True),
u = fee_spec_names() )
def passwd():
return """
PASSPHRASE NOTE:
For passphrases all combinations of whitespace are equal, and leading and
trailing space are ignored. This permits reading passphrase or brainwallet
data from a multi-line file with free spacing and indentation.
""".strip()
def brainwallet():
return """
BRAINWALLET NOTE:
To thwart dictionary attacks, its recommended to use a strong hash preset
with brainwallets. For a brainwallet passphrase to generate the correct
seed, the same seed length and hash preset parameters must always be used.
""".strip()
def txcreate_examples():
mmtype = 'B' if 'B' in proto.mmtypes else proto.mmtypes[0]
from ..tool.coin import tool_cmd
t = tool_cmd(cfg, mmtype=mmtype)
addr = t.privhex2addr('bead' * 16)
sample_addr = addr.views[addr.view_pref]
return f"""
EXAMPLES:
Send 0.123 {proto.coin} to an external {proto.name} address, returning the change to a
specific MMGen address in the tracking wallet:
$ {gc.prog_name} {sample_addr},0.123 01ABCDEF:{mmtype}:7
Same as above, but select the change address automatically:
$ {gc.prog_name} {sample_addr},0.123 01ABCDEF:{mmtype}
Same as above, but select the change address automatically by address type:
$ {gc.prog_name} {sample_addr},0.123 {mmtype}
Same as above, but reduce verbosity and specify fee of 20 satoshis
per byte:
$ {gc.prog_name} -q -f 20s {sample_addr},0.123 {mmtype}
Send entire balance of selected inputs minus fee to an external {proto.name}
address:
$ {gc.prog_name} {sample_addr}
Send entire balance of selected inputs minus fee to first unused wallet
address of specified type:
$ {gc.prog_name} {mmtype}
"""
def txcreate():
return f"""
The transactions outputs are listed on the command line, while its inputs
are chosen from a list of the wallets unspent outputs via an interactive
menu. Alternatively, inputs may be specified using the --inputs option.
All addresses on the command line can be either {proto.name} addresses or MMGen
IDs in the form <seed ID>:<address type letter>:<index>.
Outputs are specified in the form <address>,<amount>, with the change output
specified by address only. Alternatively, the change output may be an
addrlist ID in the form <seed ID>:<address type letter>, in which case the
first unused address in the tracking wallet matching the requested ID will
be automatically selected as the change output.
If the transaction fee is not specified on the command line (see FEE
SPECIFICATION below), it will be calculated dynamically using network fee
estimation for the default (or user-specified) number of confirmations.
If network fee estimation fails, the user will be prompted for a fee.
Network-estimated fees will be multiplied by the value of --fee-adjust, if
specified.
To send the value of all inputs (minus TX fee) to a single output, specify
a single address with no amount on the command line. Alternatively, an
addrlist ID may be specified, and the address will be chosen automatically
as described above for the change output.
"""
def txsign():
from ..proto.btc.params import mainnet
return """
Transactions may contain both {pnm} or non-{pnm} input addresses.
To sign non-{pnm} inputs, a {wd}flat key list is used
as the key source (--keys-from-file option).
To sign {pnm} inputs, key data is generated from a seed as with the
{pnl}-addrgen and {pnl}-keygen commands. Alternatively, a key-address file
may be used (--mmgen-keys-from-file option).
Multiple wallets or other seed files can be listed on the command line in
any order. If the seeds required to sign the transactions inputs are not
found in these files (or in the default wallet), the user will be prompted
for seed data interactively.
To prevent an attacker from crafting transactions with bogus {pnm}-to-{pnu}
address mappings, all outputs to {pnm} addresses are verified with a seed
source. Therefore, seed files or a key-address file for all {pnm} outputs
must also be supplied on the command line if the data cant be found in the
default wallet.
""".format(
wd = (f'{coind_exec()} wallet dump or ' if isinstance(proto,mainnet) else ''),
pnm = gc.proj_name,
pnu = proto.name,
pnl = gc.proj_name.lower() )
def subwallet():
from ..subseed import SubSeedIdxRange
return f"""
SUBWALLETS:
Subwallets (subseeds) are specified by a Subseed Index consisting of:
a) an integer in the range 1-{SubSeedIdxRange.max_idx}, plus
b) an optional single letter, L or S
The letter designates the length of the subseed. If omitted, L is assumed.
Long (L) subseeds are the same length as their parent wallets seed
(typically 256 bits), while short (S) subseeds are always 128-bit.
The long and short subseeds for a given index are derived independently,
so both may be used.
MMGen Wallet has no notion of depth, and to an outside observer subwallets
are identical to ordinary wallets. This is a feature rather than a bug, as
it denies an attacker any way of knowing whether a given wallet has a parent.
Since subwallets are just wallets, they may be used to generate other
subwallets, leading to hierarchies of arbitrary depth. However, this is
inadvisable in practice for two reasons: Firstly, it creates accounting
complexity, requiring the user to independently keep track of a derivation
tree. More importantly, however, it leads to the danger of Seed ID
collisions between subseeds at different levels of the hierarchy, as
MMGen checks and avoids ID collisions only among sibling subseeds.
An exception to this caveat would be a multi-user setup where sibling
subwallets are distributed to different users as their default wallets.
Since the subseeds derived from these subwallets are private to each user,
Seed ID collisions among them doesnt present a problem.
A safe rule of thumb, therefore, is for *each user* to derive all of his/her
subwallets from a single parent. This leaves each user with a total of two
million subwallets, which should be enough for most practical purposes.
""".strip()
return getattr(help_notes,k)()
from ..ui import do_pager
do_pager(cls().make(cfg, opts, proto))
sys.exit(0)

268
mmgen/help/help_notes.py Executable file
View file

@ -0,0 +1,268 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen-wallet
# https://gitlab.com/mmgen/mmgen-wallet
"""
help: help notes functions for MMGen suite commands
"""
from ..cfg import gc
class help_notes:
def __init__(self, proto, cfg):
self.proto = proto
self.cfg = cfg
def fee_spec_letters(self, use_quotes=False):
cu = self.proto.coin_amt.units
sep, conj = ((',', ' or '), ("','", "' or '"))[use_quotes]
return sep.join(u[0] for u in cu[:-1]) + ('', conj)[len(cu)>1] + cu[-1][0]
def fee_spec_names(self):
cu = self.proto.coin_amt.units
return ', '.join(cu[:-1]) + ('', ' and ')[len(cu)>1] + cu[-1] + ('', ',\nrespectively')[len(cu)>1]
def coind_exec(self):
from ..daemon import CoinDaemon
return (
CoinDaemon(self.cfg, self.proto.coin).exec_fn if self.proto.coin in CoinDaemon.coins else 'bitcoind')
def dfl_twname(self):
from ..proto.btc.rpc import BitcoinRPCClient
return BitcoinRPCClient.dfl_twname
def MasterShareIdx(self):
from ..seedsplit import MasterShareIdx
return MasterShareIdx
def tool_help(self):
from ..tool.help import main_help
return main_help()
def dfl_subseeds(self):
from ..subseed import SubSeedList
return str(SubSeedList.dfl_len)
def dfl_seed_len(self):
from ..seed import Seed
return str(Seed.dfl_len)
def password_formats(self):
from ..passwdlist import PasswordList
pwi_fs = '{:8} {:1} {:26} {:<7} {:<7} {}'
return '\n '.join(
[pwi_fs.format('Code', '','Description', 'Min Len', 'Max Len', 'Default Len')] +
[pwi_fs.format(k, '-', v.desc, v.min_len, v.max_len, v.dfl_len)
for k, v in PasswordList.pw_info.items()]
)
def dfl_mmtype(self):
from ..addr import MMGenAddrType
return "'{}' or '{}'".format(self.proto.dfl_mmtype, MMGenAddrType.mmtypes[self.proto.dfl_mmtype].name)
def address_types(self):
from ..addr import MMGenAddrType
return '\n '.join([
"'{}','{:<12} - {}".format(k, v.name + "'", v.desc)
for k, v in MMGenAddrType.mmtypes.items()
])
def fmt_codes(self):
from ..wallet import format_fmt_codes
return '\n '.join(format_fmt_codes().splitlines())
def coin_id(self):
return self.proto.coin_id
def keygen_backends(self):
from ..keygen import get_backends
from ..addr import MMGenAddrType
backends = get_backends(
MMGenAddrType(self.proto, self.cfg.type or self.proto.dfl_mmtype).pubkey_type
)
return ' '.join('{n}:{k}{t}'.format(n=n, k=k, t=('', ' [default]')[n == 1])
for n, k in enumerate(backends, 1))
def coin_daemon_network_ids(self):
from ..daemon import CoinDaemon
from ..util import fmt_list
return fmt_list(CoinDaemon.get_network_ids(self.cfg), fmt='bare')
def rel_fee_desc(self):
from ..tx import BaseTX
return BaseTX(cfg=self.cfg, proto=self.proto).rel_fee_desc
def fee(self):
from ..tx import BaseTX
return """
FEE SPECIFICATION
Transaction fees, both on the command line and at the interactive prompt, may
be specified as either absolute {c} amounts, using a plain decimal number, or
as {r}, using an integer followed by '{l}', for {u}.
""".format(
c = self.proto.coin,
r = BaseTX(cfg=self.cfg, proto=self.proto).rel_fee_desc,
l = self.fee_spec_letters(use_quotes=True),
u = self.fee_spec_names() )
def passwd(self):
return """
PASSPHRASE NOTE:
For passphrases all combinations of whitespace are equal, and leading and
trailing space are ignored. This permits reading passphrase or brainwallet
data from a multi-line file with free spacing and indentation.
""".strip()
def brainwallet(self):
return """
BRAINWALLET NOTE:
To thwart dictionary attacks, its recommended to use a strong hash preset
with brainwallets. For a brainwallet passphrase to generate the correct
seed, the same seed length and hash preset parameters must always be used.
""".strip()
def txcreate_examples(self):
mmtype = 'B' if 'B' in self.proto.mmtypes else self.proto.mmtypes[0]
from ..tool.coin import tool_cmd
t = tool_cmd(self.cfg, mmtype=mmtype)
addr = t.privhex2addr('bead' * 16)
sample_addr = addr.views[addr.view_pref]
return f"""
EXAMPLES:
Send 0.123 {self.proto.coin} to an external {self.proto.name} address, returning the change to a
specific MMGen address in the tracking wallet:
$ {gc.prog_name} {sample_addr},0.123 01ABCDEF:{mmtype}:7
Same as above, but select the change address automatically:
$ {gc.prog_name} {sample_addr},0.123 01ABCDEF:{mmtype}
Same as above, but select the change address automatically by address type:
$ {gc.prog_name} {sample_addr},0.123 {mmtype}
Same as above, but reduce verbosity and specify fee of 20 satoshis
per byte:
$ {gc.prog_name} -q -f 20s {sample_addr},0.123 {mmtype}
Send entire balance of selected inputs minus fee to an external {self.proto.name}
address:
$ {gc.prog_name} {sample_addr}
Send entire balance of selected inputs minus fee to first unused wallet
address of specified type:
$ {gc.prog_name} {mmtype}
"""
def txcreate(self):
return f"""
The transactions outputs are listed on the command line, while its inputs
are chosen from a list of the wallets unspent outputs via an interactive
menu. Alternatively, inputs may be specified using the --inputs option.
All addresses on the command line can be either {self.proto.name} addresses or MMGen
IDs in the form <seed ID>:<address type letter>:<index>.
Outputs are specified in the form <address>,<amount>, with the change output
specified by address only. Alternatively, the change output may be an
addrlist ID in the form <seed ID>:<address type letter>, in which case the
first unused address in the tracking wallet matching the requested ID will
be automatically selected as the change output.
If the transaction fee is not specified on the command line (see FEE
SPECIFICATION below), it will be calculated dynamically using network fee
estimation for the default (or user-specified) number of confirmations.
If network fee estimation fails, the user will be prompted for a fee.
Network-estimated fees will be multiplied by the value of --fee-adjust, if
specified.
To send the value of all inputs (minus TX fee) to a single output, specify
a single address with no amount on the command line. Alternatively, an
addrlist ID may be specified, and the address will be chosen automatically
as described above for the change output.
"""
def txsign(self):
from ..proto.btc.params import mainnet
return """
Transactions may contain both {pnm} or non-{pnm} input addresses.
To sign non-{pnm} inputs, a {wd}flat key list is used
as the key source (--keys-from-file option).
To sign {pnm} inputs, key data is generated from a seed as with the
{pnl}-addrgen and {pnl}-keygen commands. Alternatively, a key-address file
may be used (--mmgen-keys-from-file option).
Multiple wallets or other seed files can be listed on the command line in
any order. If the seeds required to sign the transactions inputs are not
found in these files (or in the default wallet), the user will be prompted
for seed data interactively.
To prevent an attacker from crafting transactions with bogus {pnm}-to-{pnu}
address mappings, all outputs to {pnm} addresses are verified with a seed
source. Therefore, seed files or a key-address file for all {pnm} outputs
must also be supplied on the command line if the data cant be found in the
default wallet.
""".format(
wd = f'{self.coind_exec()} wallet dump or ' if isinstance(self.proto, mainnet) else '',
pnm = gc.proj_name,
pnu = self.proto.name,
pnl = gc.proj_name.lower())
def subwallet(self):
from ..subseed import SubSeedIdxRange
return f"""
SUBWALLETS:
Subwallets (subseeds) are specified by a Subseed Index consisting of:
a) an integer in the range 1-{SubSeedIdxRange.max_idx}, plus
b) an optional single letter, L or S
The letter designates the length of the subseed. If omitted, L is assumed.
Long (L) subseeds are the same length as their parent wallets seed
(typically 256 bits), while short (S) subseeds are always 128-bit.
The long and short subseeds for a given index are derived independently,
so both may be used.
MMGen Wallet has no notion of depth, and to an outside observer subwallets
are identical to ordinary wallets. This is a feature rather than a bug, as
it denies an attacker any way of knowing whether a given wallet has a parent.
Since subwallets are just wallets, they may be used to generate other
subwallets, leading to hierarchies of arbitrary depth. However, this is
inadvisable in practice for two reasons: Firstly, it creates accounting
complexity, requiring the user to independently keep track of a derivation
tree. More importantly, however, it leads to the danger of Seed ID
collisions between subseeds at different levels of the hierarchy, as
MMGen checks and avoids ID collisions only among sibling subseeds.
An exception to this caveat would be a multi-user setup where sibling
subwallets are distributed to different users as their default wallets.
Since the subseeds derived from these subwallets are private to each user,
Seed ID collisions among them doesnt present a problem.
A safe rule of thumb, therefore, is for *each user* to derive all of his/her
subwallets from a single parent. This leaves each user with a total of two
million subwallets, which should be enough for most practical purposes.
""".strip()

View file

@ -17,13 +17,128 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
opts: MMGen-specific command-line options processing after generic processing by share.Opts
opts: command-line options processing for the MMGen Project
"""
import sys,os
from .share import Opts
import sys, os, re
from collections import namedtuple
from .cfg import gc
def get_opt_by_substring(opt, opts):
matches = [o for o in opts if o.startswith(opt)]
if len(matches) == 1:
return matches[0]
if len(matches) > 1:
from .util import die
die('CmdlineOptError', f'--{opt}: ambiguous option (not unique substring)')
def process_uopts(opts_data, opts):
from .util import die
def get_uopts():
nonlocal uargs
idx = 1
argv_len = len(sys.argv)
while idx < argv_len:
arg = sys.argv[idx]
if len(arg) > 4096:
raise RuntimeError(f'{len(arg)} bytes: command-line argument too long')
if arg.startswith('--'):
if len(arg) == 2:
uargs = sys.argv[idx+1:]
return
opt, parm = arg[2:].split('=') if '=' in arg else (arg[2:], None)
if len(opt) < 2:
die('CmdlineOptError', f'--{opt}: option name must be at least two characters long')
if opt in opts or (opt := get_opt_by_substring(opt, opts)):
if opts[opt].has_parm:
if parm:
yield (opts[opt].name, parm)
else:
idx += 1
if idx == argv_len or (parm := sys.argv[idx]).startswith('-'):
die('CmdlineOptError', f'missing parameter for option --{opt}')
yield (opts[opt].name, parm)
else:
if parm:
die('CmdlineOptError', f'option --{opt} requires no parameter')
yield (opts[opt].name, True)
else:
opt, parm = arg[2:].split('=') if '=' in arg else (arg[2:], None)
die('CmdlineOptError', f'--{opt}: unrecognized option')
elif arg[0] == '-' and len(arg) > 1:
for j, sopt in enumerate(arg[1:]):
if sopt in opts:
if opts[sopt].has_parm:
if j > 0:
die('CmdlineOptError', f'{arg}: short option with parameters cannot be combined')
if arg[2:]:
yield (opts[sopt].name, arg[2:])
else:
idx += 1
if idx == argv_len or (parm := sys.argv[idx]).startswith('-'):
die('CmdlineOptError', f'missing parameter for option -{sopt}')
yield (opts[sopt].name, parm)
break
else:
yield (opts[sopt].name, True)
else:
die('CmdlineOptError', f'-{sopt}: unrecognized option')
else:
uargs = sys.argv[idx:]
return
idx += 1
uargs = []
uopts = dict(get_uopts())
if 'sets' in opts_data:
for a_opt, a_val, b_opt, b_val in opts_data['sets']:
if a_opt in uopts:
u_val = uopts[a_opt]
if (u_val and a_val == bool) or u_val == a_val:
if b_opt in uopts and uopts[b_opt] != b_val:
die(1,
'Option conflict:'
+ '\n --{}={}, with'.format(b_opt.replace('_', '-'), uopts[b_opt])
+ '\n --{}={}\n'.format(a_opt.replace('_', '-'), uopts[a_opt]))
else:
uopts[b_opt] = b_val
return uopts, uargs
cmd_opts_pat = re.compile(r'^-([a-zA-Z0-9-]), --([a-zA-Z0-9-]{2,64})(=| )(.+)')
global_opts_pat = re.compile(r'^\t\t\t(.)(.) --([a-zA-Z0-9-]{2,64})(=| )(.+)')
ao = namedtuple('opt', ['name', 'has_parm'])
def parse_opts(opts_data, opt_filter, global_opts_data, global_opts_filter):
def parse_cmd_opts_text():
for line in opts_data['text']['options'].strip().splitlines():
m = cmd_opts_pat.match(line)
if m and (not opt_filter or m[1] in opt_filter):
ret = ao(m[2].replace('-', '_'), m[3] == '=')
yield (m[1], ret)
yield (m[2], ret)
def parse_global_opts_text():
for line in global_opts_data['text'].splitlines():
m = global_opts_pat.match(line)
if m and m[1] in global_opts_filter.coin and m[2] in global_opts_filter.cmd:
yield (m[3], ao(m[3].replace('-', '_'), m[4] == '='))
opts = tuple(parse_cmd_opts_text()) + tuple(parse_global_opts_text())
uopts, uargs = process_uopts(opts_data, dict(opts))
return namedtuple('parsed_cmd_opts', ['user_opts', 'cmd_args', 'opts'])(
uopts, # dict
uargs, # list, callers can pop
tuple(v.name for k,v in opts if len(k) > 1)
)
def opt_preproc_debug(po):
d = (
('Cmdline', ' '.join(sys.argv), False),
@ -37,48 +152,6 @@ def opt_preproc_debug(po):
for label,data,pretty in d:
Msg(' {:<20}: {}'.format(label,'\n' + fmt_list(data,fmt='col',indent=' '*8) if pretty else data))
long_opts_data = {
'text': """
--, --accept-defaults Accept defaults at all prompts
--, --coin=c Choose coin unit. Default: BTC. Current choice: {cu_dfl}
--, --token=t Specify an ERC20 token by address or symbol
--, --cashaddr=0|1 Display BCH addresses in cashaddr format (default: 1)
--, --color=0|1 Disable or enable color output (default: 1)
--, --columns=N Force N columns of output with certain commands
--, --scroll Use the curses-like scrolling interface for
tracking wallet views
--, --force-256-color Force 256-color output when color is enabled
--, --pager Pipe output of certain commands to pager (WIP)
--, --data-dir=path Specify {pnm} data directory location
--, --daemon-data-dir=path Specify coin daemon data directory location
--, --daemon-id=ID Specify the coin daemon ID
--, --ignore-daemon-version Ignore coin daemon version check
--, --http-timeout=t Set HTTP timeout in seconds for JSON-RPC connections
--, --no-license Suppress the GPL license prompt
--, --rpc-host=HOST Communicate with coin daemon running on host HOST
--, --rpc-port=PORT Communicate with coin daemon listening on port PORT
--, --rpc-user=USER Authenticate to coin daemon using username USER
--, --rpc-password=PASS Authenticate to coin daemon using password PASS
--, --rpc-backend=backend Use backend 'backend' for JSON-RPC communications
--, --aiohttp-rpc-queue-len=N Use N simultaneous RPC connections with aiohttp
--, --regtest=0|1 Disable or enable regtest mode
--, --testnet=0|1 Disable or enable testnet
--, --tw-name=NAME Specify alternate name for the BTC/LTC/BCH tracking
wallet (default: {tw_name})
--, --skip-cfg-file Skip reading the configuration file
--, --usage Print usage information and exit
--, --version Print version information and exit
--, --bob Specify user Bob in MMGen regtest mode
--, --alice Specify user Alice in MMGen regtest mode
--, --carol Specify user Carol in MMGen regtest mode
""",
'code': lambda proto,help_notes,s: s.format(
pnm = gc.proj_name,
cu_dfl = proto.coin,
tw_name = help_notes('dfl_twname')
)
}
opts_data_dfl = {
'text': {
'desc': '',
@ -90,7 +163,22 @@ opts_data_dfl = {
}
}
class UserOpts:
def get_coin():
for n, arg in enumerate(sys.argv[1:]):
if len(arg) > 4096:
raise RuntimeError(f'{len(arg)} bytes: command-line argument too long')
if arg.startswith('--coin='):
return arg.removeprefix('--coin=').lower()
if arg == '--coin':
if len(sys.argv) < n + 3:
from .util import die
die('CmdlineOptError', f'{arg}: missing parameter')
return sys.argv[n + 2].lower()
if arg == '-' or not arg.startswith('-'): # stop at first non-option
return 'btc'
return 'btc'
class Opts:
def __init__(
self,
@ -98,16 +186,19 @@ class UserOpts:
opts_data,
init_opts, # dict containing opts to pre-initialize
opt_filter, # whitelist of opt letters; all others are skipped
parse_only,
parsed_opts):
parsed_opts,
need_proto):
self.opts_data = od = opts_data or opts_data_dfl
if len(sys.argv) > 257:
raise RuntimeError(f'{len(sys.argv) - 1}: too many command-line arguments')
opts_data = opts_data or opts_data_dfl
self.opt_filter = opt_filter
od['text']['long_options'] = long_opts_data['text']
self.global_opts_filter = self.get_global_opts_filter(need_proto)
self.opts_data = opts_data
# po: (user_opts,cmd_args,opts,filtered_opts)
po = parsed_opts or Opts.parse_opts(od,opt_filter=opt_filter)
po = parsed_opts or parse_opts(opts_data, opt_filter, self.global_opts_data, self.global_opts_filter)
cfg._args = po.cmd_args
cfg._uopts = uopts = po.user_opts
@ -127,60 +218,90 @@ class UserOpts:
if os.getenv('MMGEN_DEBUG_OPTS'):
opt_preproc_debug(po)
for funcname in ('usage', 'version', 'show_hash_presets'):
for funcname in self.info_funcs:
if funcname in uopts:
import importlib
getattr(importlib.import_module('mmgen.help'), funcname)(cfg) # exits
getattr(importlib.import_module(self.help_pkg), funcname)(cfg) # exits
if parse_only:
return
class UserOpts(Opts):
def init_bottom(self,cfg):
# print help screen only after globals initialized and locked:
if cfg.help or cfg.longhelp:
self.print_help(cfg) # exits
help_pkg = 'mmgen.help'
info_funcs = ('usage', 'version', 'show_hash_presets')
def usage(self):
from .util import Die
Die(1,Opts.make_usage_str(gc.prog_name,'user',self.usage_data))
global_opts_data = {
# coin code : cmd code : opt : opt param : text
'text': """
-- --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}
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
Rr --scroll Use the curses-like scrolling interface for
+ tracking wallet views
-- --force-256-color Force 256-color output when color is enabled
-- --pager Pipe output of certain commands to pager (WIP)
-- --data-dir=path Specify {pnm} data directory location
rr --daemon-data-dir=path Specify coin daemon data directory location
Rr --daemon-id=ID Specify the coin daemon ID
rr --ignore-daemon-version Ignore coin daemon version check
rr --http-timeout=t Set HTTP timeout in seconds for JSON-RPC connections
-- --no-license Suppress the GPL license prompt
rr --rpc-host=HOST Communicate with coin daemon running on host HOST
rr --rpc-port=PORT Communicate with coin daemon listening on port PORT
rr --rpc-user=USER Authenticate to coin daemon using username USER
rr --rpc-password=PASS Authenticate to coin daemon using password PASS
Rr --rpc-backend=backend Use backend 'backend' for JSON-RPC communications
Rr --aiohttp-rpc-queue-len=N Use N simultaneous RPC connections with aiohttp
-p --regtest=0|1 Disable or enable regtest mode
-- --testnet=0|1 Disable or enable testnet
br --tw-name=NAME Specify alternate name for the BTC/LTC/BCH tracking
+ wallet (default: {tw_name})
-- --skip-cfg-file Skip reading the configuration file
-- --version Print version information and exit
-- --usage Print usage information and exit
b- --bob Specify user Bob in MMGen regtest mode
b- --alice Specify user Alice in MMGen regtest mode
b- --carol Specify user Carol in MMGen regtest mode
""",
'code': lambda proto, help_notes, s: s.format(
pnm = gc.proj_name,
cu_dfl = proto.coin,
tw_name = help_notes('dfl_twname'))
}
def version(self):
from .util import Die,fmt
Die(0,fmt(f"""
{gc.prog_name.upper()} version {gc.version}
Part of {gc.proj_name} Wallet, an online/offline cryptocurrency wallet for the
command line. Copyright (C){gc.Cdates} {gc.author} {gc.email}
""",indent=' ').rstrip())
def print_help(self,cfg):
if not 'code' in self.opts_data:
self.opts_data['code'] = {}
from .protocol import init_proto_from_cfg
proto = init_proto_from_cfg(cfg,need_amt=True)
if getattr(cfg,'longhelp',None):
self.opts_data['code']['long_options'] = long_opts_data['code']
def remove_unneeded_long_opts():
d = self.opts_data['text']['long_options']
if proto.base_proto != 'Ethereum':
d = '\n'.join(''+i for i in d.split('\n') if not '--token' in i)
self.opts_data['text']['long_options'] = d
remove_unneeded_long_opts()
from .ui import do_pager
do_pager(Opts.make_help( cfg, proto, self.opts_data, self.opt_filter ))
sys.exit(0)
def show_hash_presets(self):
fs = ' {:<6} {:<3} {:<2} {}'
from .util import msg
from .crypto import Crypto
msg(' Available parameters for scrypt.hash():')
msg(fs.format('Preset','N','r','p'))
for i in sorted(Crypto.hash_presets.keys()):
msg(fs.format(i,*Crypto.hash_presets[i]))
msg(' N = memory usage (power of two)\n p = iterations (rounds)')
sys.exit(0)
@staticmethod
def get_global_opts_filter(need_proto):
"""
Coin codes:
'b' - Bitcoin or Bitcoin code fork supporting RPC
'R' - Bitcoin or Ethereum code fork supporting RPC
'e' - Ethereum or Ethereum code fork
'r' - coin supporting RPC
'h' - Bitcoin Cash
'-' - other coin
Cmd codes:
'p' - proto required
'r' - RPC required
'-' - no capabilities required
"""
ret = namedtuple('global_opts_filter', ['coin', 'cmd'])
if caps := gc.cmd_caps:
coin = caps.coin if caps.coin and len(caps.coin) > 1 else get_coin()
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
('-')),
cmd = (
['-']
+ (['r'] if caps.rpc else [])
+ (['p'] if caps.proto else [])
))
else:
return ret(
coin = ('-', 'r', 'R', 'b', 'h', 'e'),
cmd = ('-', 'r', 'p')
)

View file

@ -1,182 +0,0 @@
#!/usr/bin/env python3
#
# Opts.py, an options parsing library for Python.
# Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
share.Opts: Generic options parsing
"""
import sys,re
from collections import namedtuple
pat = re.compile(r'^-([a-zA-Z0-9-]), --([a-zA-Z0-9-]{2,64})(=| )(.+)')
def make_usage_str(prog_name,caller,data):
lines = [data.strip()] if isinstance(data,str) else data
indent,col1_w = {
'help': (2,len(prog_name)+1),
'user': (0,len('USAGE:')),
}[caller]
def gen():
ulbl = 'USAGE:'
for line in lines:
yield f'{ulbl:{col1_w}} {prog_name} {line}'
ulbl = ''
return ('\n'+(' '*indent)).join(gen())
def usage(opts_data):
print(make_usage_str(
prog_name = opts_data['prog_name'],
caller = 'user',
data = opts_data['text'].get('usage2') or opts_data['text']['usage'] ))
sys.exit(1)
def print_help(*args):
print(make_help(*args))
sys.exit(0)
def make_help(cfg,proto,opts_data,opt_filter):
def parse_lines(text):
filtered = False
for line in text.strip().splitlines():
m = pat.match(line)
if m:
filtered = bool(opt_filter and m[1] not in opt_filter)
if not filtered:
yield fs.format( ('-'+m[1]+',','')[m[1]=='-'], m[2], m[4] )
elif not filtered:
yield line
opts_type,fs = ('options','{:<3} --{} {}') if cfg.help else ('long_options','{} --{} {}')
t = opts_data['text']
c = opts_data['code']
nl = '\n '
pn = opts_data['prog_name']
from ..help import help_notes_func
def help_notes(k):
return help_notes_func(proto,cfg,k)
def help_mod(modname):
import importlib
return importlib.import_module('mmgen.help.'+modname).help(proto,cfg)
def gen_arg_tuple(func,text):
d = {
'proto': proto,
'help_notes': help_notes,
'help_mod': help_mod,
'cfg': cfg,
}
for arg in func.__code__.co_varnames:
yield d[arg] if arg in d else text
def gen_text():
yield ' {} {}'.format(pn.upper()+':',t['desc'].strip())
yield make_usage_str(pn,'help',t.get('usage2') or t['usage'])
yield opts_type.upper().replace('_',' ') + ':'
# process code for options
opts_text = nl.join(parse_lines(t[opts_type]))
if opts_type in c:
arg_tuple = tuple(gen_arg_tuple(c[opts_type],opts_text))
yield c[opts_type](*arg_tuple)
else:
yield opts_text
# process code for notes
if opts_type == 'options' and 'notes' in t:
notes_text = t['notes']
if 'notes' in c:
arg_tuple = tuple(gen_arg_tuple(c['notes'],notes_text))
notes_text = c['notes'](*arg_tuple)
yield from notes_text.splitlines()
return nl.join(gen_text()) + '\n'
def process_uopts(opts_data,short_opts,long_opts):
import os,getopt
opts_data['prog_name'] = os.path.basename(sys.argv[0])
try:
cl_uopts,uargs = getopt.getopt(sys.argv[1:],''.join(short_opts),long_opts)
except getopt.GetoptError as e:
print(e.args[0])
sys.exit(1)
def get_uopts():
for uopt,uparm in cl_uopts:
if uopt.startswith('--'):
lo = uopt[2:]
if lo in long_opts:
yield (lo.replace('-','_'), True)
else: # lo+'=' in long_opts
yield (lo.replace('-','_'), uparm)
else: # uopt.startswith('-')
so = uopt[1]
if so in short_opts:
yield (long_opts[short_opts.index(so)].replace('-','_'), True)
else: # so+':' in short_opts
yield (long_opts[short_opts.index(so+':')][:-1].replace('-','_'), uparm)
uopts = dict(get_uopts())
if 'sets' in opts_data:
for a_opt,a_val,b_opt,b_val in opts_data['sets']:
if a_opt in uopts:
u_val = uopts[a_opt]
if (u_val and a_val == bool) or u_val == a_val:
if b_opt in uopts and uopts[b_opt] != b_val:
sys.stderr.write(
'Option conflict:'
+ '\n --{}={}, with'.format(b_opt.replace('_','-'),uopts[b_opt])
+ '\n --{}={}\n'.format(a_opt.replace('_','-'),uopts[a_opt]) )
sys.exit(1)
else:
uopts[b_opt] = b_val
return uopts,uargs
def parse_opts(opts_data,opt_filter=None):
short_opts,long_opts,filtered_opts = [],[],[]
def parse_lines(opts_type):
for line in opts_data['text'][opts_type].strip().splitlines():
m = pat.match(line)
if m:
if opt_filter and m[1] not in opt_filter:
filtered_opts.append(m[2])
else:
if opts_type == 'options':
short_opts.append(m[1] + ('',':')[m[3] == '='])
long_opts.append(m[2] + ('','=')[m[3] == '='])
parse_lines('options')
if 'long_options' in opts_data['text']:
parse_lines('long_options')
uopts,uargs = process_uopts(opts_data,short_opts,long_opts)
return namedtuple('parsed_cmd_opts',['user_opts','cmd_args','opts','filtered_opts'])(
uopts, # dict
uargs, # list, callers can pop
tuple(o.replace('-','_').rstrip('=') for o in long_opts),
tuple(o.replace('-','_') for o in filtered_opts),
)

View file

@ -82,4 +82,6 @@ ignored-classes = [ # ignored for no-member, otherwise checked
"GenTool",
"VirtBlockDeviceBase",
"SwapMgrBase",
"Opts",
"Help",
]

View file

@ -86,7 +86,6 @@ packages =
mmgen.proto.secp256k1
mmgen.proto.xmr
mmgen.proto.zec
mmgen.share
mmgen.tool
mmgen.tx
mmgen.tw

View file

@ -117,7 +117,7 @@ class CmdTestHelp(CmdTestBase):
return t
def longhelpscreens(self):
return self.helpscreens(arg='--longhelp',expect='USAGE:.*LONG OPTIONS:')
return self.helpscreens(arg='--longhelp',expect='USAGE:.*GLOBAL OPTIONS:')
def show_hash_presets(self):
return self.helpscreens(

View file

@ -16,17 +16,44 @@ from ..include.common import cfg
from .ct_base import CmdTestBase
class CmdTestOpts(CmdTestBase):
'options processing'
'command-line options parsing and processing'
networks = ('btc',)
tmpdir_nums = [41]
cmd_group = (
('opt_helpscreen', (41,"helpscreen output", [])),
('opt_noargs', (41,"invocation with no user options or arguments", [])),
('opt_good', (41,"good opts", [])),
('opt_bad_infile', (41,"bad infile parameter", [])),
('opt_bad_outdir', (41,"bad outdir parameter", [])),
('opt_bad_incompatible', (41,"incompatible opts", [])),
('opt_bad_autoset', (41,"invalid autoset value", [])),
('opt_helpscreen', (41, 'helpscreen output', [])),
('opt_noargs', (41, 'invocation with no user options or arguments', [])),
('opt_good1', (41, 'good opts (long opts only)', [])),
('opt_good2', (41, 'good opts (mixed short and long opts)', [])),
('opt_good3', (41, 'good opts (max arg count)', [])),
('opt_good4', (41, 'good opts (maxlen arg)', [])),
('opt_good5', (41, 'good opts (long opt substring)', [])),
('opt_good6', (41, 'good global opt (--coin=xmr)', [])),
('opt_good7', (41, 'good global opt (--coin xmr)', [])),
('opt_good8', (41, 'good global opt (--pager)', [])),
('opt_good9', (41, 'good cmdline arg ‘-’', [])),
('opt_good10', (41, 'good cmdline arg ‘-’ with arg', [])),
('opt_good11', (41, 'good cmdline arg ‘-’ with option', [])),
('opt_bad_param', (41, 'bad global opt (--pager=1)', [])),
('opt_bad_infile', (41, 'bad infile parameter', [])),
('opt_bad_outdir', (41, 'bad outdir parameter', [])),
('opt_bad_incompatible', (41, 'incompatible opts', [])),
('opt_bad_autoset', (41, 'invalid autoset value', [])),
('opt_invalid_1', (41, 'invalid cmdline opt ‘--x’', [])),
('opt_invalid_2', (41, 'invalid cmdline opt ‘---’', [])),
('opt_invalid_3', (41, 'invalid cmdline opt (combined short opt with param)', [])),
('opt_invalid_4', (41, 'invalid cmdline opt (combined short opt with param)', [])),
('opt_invalid_5', (41, 'invalid cmdline opt (missing parameter)', [])),
('opt_invalid_6', (41, 'invalid cmdline opt (missing parameter)', [])),
('opt_invalid_7', (41, 'invalid cmdline opt (parameter not required)', [])),
('opt_invalid_8', (41, 'invalid cmdline opt (non-existent option)', [])),
('opt_invalid_9', (41, 'invalid cmdline opt (non-existent option)', [])),
('opt_invalid_10', (41, 'invalid cmdline opt (missing parameter)', [])),
('opt_invalid_11', (41, 'invalid cmdline opt (missing parameter)', [])),
('opt_invalid_12', (41, 'invalid cmdline opt (non-existent option)', [])),
('opt_invalid_13', (41, 'invalid cmdline opt (ambiguous long opt substring)', [])),
('opt_invalid_14', (41, 'invalid cmdline opt (long opt substring too short)', [])),
('opt_invalid_15', (41, 'invalid cmdline (too many args)', [])),
('opt_invalid_16', (41, 'invalid cmdline (overlong arg)', [])),
)
def spawn_prog(self, args, exit_val=None):
@ -65,10 +92,12 @@ class CmdTestOpts(CmdTestBase):
('cfg.outdir', ''), # check_outdir()
('cfg.cached_balances', 'False'),
('cfg.minconf', '1'),
('cfg.coin', 'BTC'),
('cfg.pager', 'False'),
('cfg.fee_estimate_mode', 'conservative'), # _autoset_opts
))
def opt_good(self):
def opt_good1(self):
pf_base = 'testfile'
pf = os.path.join(self.tmpdir,pf_base)
self.write_to_tmpfile(pf_base,'')
@ -90,6 +119,68 @@ class CmdTestOpts(CmdTestBase):
('cfg.fee_estimate_mode', 'economical'),
))
def opt_good2(self):
return self.check_vals(
[
'--print-checksum',
'-qX',
f'--outdir={self.tmpdir}',
'-p5',
'-m', '0',
'--seed-len=256',
'-L--my-label',
'--seed-len', '128',
'--min-temp=-30',
'-T-10',
'--',
'x', 'y', '12345'
], (
('cfg.print_checksum', 'True'),
('cfg.quiet', 'True'),
('cfg.outdir', self.tmpdir),
('cfg.cached_balances', 'True'),
('cfg.minconf', '0'),
('cfg.keep_label', 'None'),
('cfg.seed_len', '128'),
('cfg.hash_preset', '5'),
('cfg.label', '--my-label'),
('cfg.min_temp', '-30'),
('cfg.max_temp', '-10'),
('arg1', 'x'),
('arg2', 'y'),
('arg3', '12345'),
))
def opt_good3(self):
return self.check_vals(['m'] * 256, (('arg256', 'm'),))
def opt_good4(self):
return self.check_vals(['e' * 4096], (('arg1', 'e' * 4096),))
def opt_good5(self):
return self.check_vals(['--minc=7'], (('cfg.minconf', '7'),))
def opt_good6(self):
return self.check_vals(['--coin=xmr'], (('cfg.coin', 'XMR'),))
def opt_good7(self):
return self.check_vals(['--coin', 'xmr'], (('cfg.coin', 'XMR'),))
def opt_good8(self):
return self.check_vals(['--pager'], (('cfg.pager', 'True'),))
def opt_good9(self):
return self.check_vals(['-'], (('arg1', '-'),))
def opt_good10(self):
return self.check_vals(['-', '-x'], (('arg1', '-'), ('arg2', '-x')))
def opt_good11(self):
return self.check_vals(['-q', '-', '-x'], (('arg1', '-'), ('arg2', '-x')))
def opt_bad_param(self):
return self.do_run(['--pager=1'], 'no parameter', 1)
def opt_bad_infile(self):
pf = os.path.join(self.tmpdir,'fubar')
return self.do_run(['--passwd-file='+pf],'not found',1)
@ -103,3 +194,56 @@ class CmdTestOpts(CmdTestBase):
def opt_bad_autoset(self):
return self.do_run(['--fee-estimate-mode=Fubar'],'not unique substring',1)
def opt_invalid(self, args, expect, exit_val):
t = self.spawn_prog(args, exit_val=exit_val)
t.expect(expect)
return t
def opt_invalid_1(self):
return self.opt_invalid(['--x'], 'must be at least', 1)
def opt_invalid_2(self):
return self.opt_invalid(['---'], 'must be at least', 1)
def opt_invalid_3(self):
return self.opt_invalid(['-kl3'], 'short option with parameters', 1)
def opt_invalid_4(self):
return self.opt_invalid(['-kl 3'], 'short option with parameters', 1)
def opt_invalid_5(self):
return self.opt_invalid(['-l'], 'missing parameter', 1)
def opt_invalid_6(self):
return self.opt_invalid(['-l', '-k'], 'missing parameter', 1)
def opt_invalid_7(self):
return self.opt_invalid(['--quiet=1'], 'requires no parameter', 1)
def opt_invalid_8(self):
return self.opt_invalid(['-x'], 'unrecognized option', 1)
def opt_invalid_9(self):
return self.opt_invalid(['--frobnicate'], 'unrecognized option', 1)
def opt_invalid_10(self):
return self.opt_invalid(['--label', '-q'], 'missing parameter', 1)
def opt_invalid_11(self):
return self.opt_invalid(['-T', '-10'], 'missing parameter', 1)
def opt_invalid_12(self):
return self.opt_invalid(['-q', '-10'], 'unrecognized option', 1)
def opt_invalid_13(self):
return self.opt_invalid(['--mi=3'], 'ambiguous option', 1)
def opt_invalid_14(self):
return self.opt_invalid(['--m=3'], 'must be at least', 1)
def opt_invalid_15(self):
return self.opt_invalid(['m'] * 257, 'too many', 1)
def opt_invalid_16(self):
return self.opt_invalid(['e' * 4097], 'too long', 1)

View file

@ -25,6 +25,8 @@ opts_data = {
-p, --hash-preset= p Use the scrypt hash parameters defined by preset 'p'
-P, --passwd-file= f Get wallet passphrase from file 'f'
-q, --quiet Be quieter
-t, --min-temp= t Minimum temperature (in degrees Celsius)
-T, --max-temp= t Maximum temperature (in degrees Celsius)
-X, --cached-balances Use cached balances (Ethereum only)
-v, --verbose Be more verbose
sample help_note: {kgs}
@ -57,6 +59,14 @@ for k in (
'cached_balances', # opt_sets_global
'minconf', # global_sets_opt
'hidden_incog_input_params',
'keep_label',
'seed_len',
'hash_preset',
'label',
'min_temp',
'max_temp',
'coin',
'pager',
):
msg('{:30} {}'.format( f'cfg.{k}:', getattr(cfg,k) ))
@ -72,3 +82,7 @@ for k in (
'fee_estimate_mode', # _autoset_opts
):
msg('{:30} {}'.format( f'cfg.{k}:', getattr(cfg,k) ))
msg('')
for n, k in enumerate(cfg._args, 1):
msg(f'arg{n}: {k}')