16 Commits 8edc7da5a2 ... d7e3b55e3b

Author SHA1 Message Date
  The MMGen Project d7e3b55e3b opts, help: refactor, parse cmdline opts natively, filter global opts 1 month ago
  The MMGen Project 307e6fb541 README.md: update command help links 1 month ago
  The MMGen Project e1e1f07995 whitespace, minor cleanups 1 month ago
  The MMGen Project d95cdf49b9 cfg.py: `do_post_init` -> `caller_post_init` 1 month ago
  The MMGen Project 5cc4a20724 cfg.py: add `cmd_caps` attr 1 month ago
  The MMGen Project f6843a3fcd opts.py: refactor `usage()`, `version()`, `show_hash_presets()` 1 month ago
  The MMGen Project a3354aed74 cmdtest.py: add `ref3_pw` test 1 month ago
  The MMGen Project d520e31e53 test-release.sh: add separate `help` test 1 month ago
  The MMGen Project d8aca5bb6c test suite: whitespace, minor cleanups 1 month ago
  The MMGen Project 7fbb50db92 scripts/gendiff.py: support diff options 1 month ago
  The MMGen Project 66521a07f2 update --longhelp text 1 month ago
  The MMGen Project 3b3d06fb51 cmdtest.py: remove unneeded opts from spawn cmdline 1 month ago
  The MMGen Project 647c7a2601 cmdtest.py regtest: cleanups 1 month ago
  The MMGen Project fddb5b73b2 cmdtest.py: autosign_eth, ethdev: cleanups 1 month ago
  The MMGen Project 75051cd831 regtest: remove coin component from data_dir path 1 month ago
  The MMGen Project 93e586c9f0 test suite: whitespace, string formatting 1 month ago
67 changed files with 1388 additions and 998 deletions
  1. 8 8
      README.md
  2. 1 1
      examples/halving-calculator.py
  3. 1 1
      mmgen/altcoin/params.py
  4. 4 3
      mmgen/base_obj.py
  5. 56 15
      mmgen/cfg.py
  6. 1 1
      mmgen/data/release_date
  7. 1 1
      mmgen/data/version
  8. 1 0
      mmgen/exception.py
  9. 2 2
      mmgen/fileutil.py
  10. 145 257
      mmgen/help/__init__.py
  11. 268 0
      mmgen/help/help_notes.py
  12. 2 2
      mmgen/main_addrgen.py
  13. 1 1
      mmgen/main_addrimport.py
  14. 3 3
      mmgen/main_autosign.py
  15. 5 5
      mmgen/main_msg.py
  16. 2 2
      mmgen/main_passgen.py
  17. 2 2
      mmgen/main_regtest.py
  18. 2 2
      mmgen/main_seedjoin.py
  19. 1 1
      mmgen/main_split.py
  20. 2 2
      mmgen/main_tool.py
  21. 1 1
      mmgen/main_txbump.py
  22. 1 1
      mmgen/main_txcreate.py
  23. 1 1
      mmgen/main_txdo.py
  24. 2 2
      mmgen/main_txsend.py
  25. 2 2
      mmgen/main_txsign.py
  26. 4 4
      mmgen/main_wallet.py
  27. 6 7
      mmgen/main_xmrwallet.py
  28. 233 122
      mmgen/opts.py
  29. 1 1
      mmgen/proto/btc/params.py
  30. 4 3
      mmgen/proto/btc/regtest.py
  31. 1 1
      mmgen/proto/eth/params.py
  32. 1 1
      mmgen/proto/xmr/params.py
  33. 1 1
      mmgen/proto/zec/params.py
  34. 5 3
      mmgen/protocol.py
  35. 0 182
      mmgen/share/Opts.py
  36. 0 0
      mmgen/share/__init__.py
  37. 2 0
      pyproject.toml
  38. 1 1
      scripts/create-bip-hd-chain-params.py
  39. 1 1
      scripts/create-token.py
  40. 10 5
      scripts/gendiff.py
  41. 1 1
      scripts/tx-v2-to-v3.py
  42. 0 1
      setup.cfg
  43. 1 1
      test/clean.py
  44. 1 2
      test/cmdtest.py
  45. 2 1
      test/cmdtest_py_d/cfg.py
  46. 0 15
      test/cmdtest_py_d/ct_automount.py
  47. 19 12
      test/cmdtest_py_d/ct_automount_eth.py
  48. 1 1
      test/cmdtest_py_d/ct_base.py
  49. 17 18
      test/cmdtest_py_d/ct_ethdev.py
  50. 187 0
      test/cmdtest_py_d/ct_help.py
  51. 12 9
      test/cmdtest_py_d/ct_main.py
  52. 4 155
      test/cmdtest_py_d/ct_misc.py
  53. 156 15
      test/cmdtest_py_d/ct_opts.py
  54. 10 2
      test/cmdtest_py_d/ct_ref.py
  55. 98 71
      test/cmdtest_py_d/ct_ref_3seed.py
  56. 19 14
      test/cmdtest_py_d/ct_regtest.py
  57. 9 4
      test/cmdtest_py_d/ct_shared.py
  58. 2 2
      test/gentest.py
  59. 2 2
      test/include/coin_daemon_control.py
  60. 14 0
      test/include/common.py
  61. 15 1
      test/misc/opts_main.py
  62. 1 1
      test/objattrtest.py
  63. 1 1
      test/objtest.py
  64. 1 1
      test/scrambletest.py
  65. 28 18
      test/test-release.d/cfg.sh
  66. 1 1
      test/tooltest.py
  67. 1 1
      test/tooltest2.py

+ 8 - 8
README.md

@@ -211,23 +211,23 @@ Donate:
 [bw]: https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt
 [fl]: https://en.wiktionary.org/wiki/Wiktionary:Frequency_lists/Contemporary_poetry
 [U]:  ../../wiki/Subwallets
-[X]:  ../../wiki/autosign-[MMGen-command-help]
-[xm]: ../../wiki/xmrwallet-[MMGen-command-help]
-[G]:  ../../wiki/passgen-[MMGen-command-help]
-[MS]: ../../wiki/msg-[MMGen-command-help]
+[X]:  ../../wiki/command-help-autosign
+[xm]: ../../wiki/command-help-xmrwallet
+[G]:  ../../wiki/command-help-passgen
+[MS]: ../../wiki/command-help-msg
 [T]:  ../../wiki/Getting-Started-with-MMGen-Wallet#a_ct
 [E]:  ../../wiki/Altcoin-and-Forkcoin-Support#a_tx
-[ag]: ../../wiki/addrgen-[MMGen-command-help]
+[ag]: ../../wiki/command-help-addrgen
 [bx]: ../../wiki/Altcoin-and-Forkcoin-Support#a_bch
 [mx]: ../../wiki/Altcoin-and-Forkcoin-Support#a_xmr
 [zx]: ../../wiki/Altcoin-and-Forkcoin-Support#a_zec
 [ax]: ../../wiki/Altcoin-and-Forkcoin-Support#a_kg
 [M]:  ../../wiki/Getting-Started-with-MMGen-Wallet#a_fee
 [R]:  ../../wiki/Getting-Started-with-MMGen-Wallet#a_rbf
-[B]:  ../../wiki/txbump-[MMGen-command-help]
+[B]:  ../../wiki/command-help-txbump
 [69]: https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki
 [O]:  ../../wiki/XOR-Seed-Splitting:-Theory-and-Practice
-[ms]: ../../wiki/seedsplit-[MMGen-command-help]
+[ms]: ../../wiki/command-help-seedsplit
 [ta]: ../../wiki/Tool-API
 [ts]: ../../wiki/Test-Suite
-[L]:  ../../wiki/tool-[MMGen-command-help].md
+[L]:  ../../wiki/command-help-tool

+ 1 - 1
examples/halving-calculator.py

@@ -23,7 +23,7 @@ opts_data = {
 		'usage':'[opts]',
 		'options': """
 -h, --help          Print this help message
---, --longhelp      Print help message for long options (common options)
+--, --longhelp      Print help message for long (global) options
 -s, --sample-size=N Specify sample block range for block discovery time
                     estimate
 """,

+ 1 - 1
mmgen/altcoin/params.py

@@ -292,7 +292,7 @@ def make_proto(e,testnet=False):
 				'wif_ver_num': { 'std': num2hexstr(e.wif_ver_num) },
 				'mmtypes':    ('L','C','S') if e.has_segwit else ('L','C'),
 				'dfl_mmtype': 'L',
-				'mmcaps':     ('key','addr'),
+				'mmcaps':     (),
 			},
 		)
 	)

+ 4 - 3
mmgen/base_obj.py

@@ -50,6 +50,7 @@ class AttrCtrl(metaclass=AttrCtrlMeta):
 	_use_class_attr = False
 	_default_to_none = False
 	_skip_type_check = ()
+	_delete_ok = ()
 
 	def _lock(self):
 		self._locked = True
@@ -87,10 +88,10 @@ class AttrCtrl(metaclass=AttrCtrlMeta):
 
 		return object.__setattr__(self,name,value)
 
-	def __delattr__(self,name):
-		if self._locked:
+	def __delattr__(self, name):
+		if self._locked and not name in self._delete_ok:
 			raise AttributeError('attribute cannot be deleted')
-		return object.__delattr__(self,name)
+		return object.__delattr__(self, name)
 
 class Lockable(AttrCtrl):
 	"""

+ 56 - 15
mmgen/cfg.py

@@ -34,12 +34,13 @@ def die2(exit_val,s):
 
 class GlobalConstants(Lockable):
 	"""
-	These values are non-configurable.  They’re constant for a given machine,
+	These values are non-runtime-configurable.  They’re constant for a given machine,
 	user, executable and MMGen release.
 	"""
 	_autolock = True
 
 	proj_name          = 'MMGen'
+	proj_id            = 'mmgen'
 	proj_url           = 'https://github.com/mmgen/mmgen-wallet'
 	author             = 'The MMGen Project'
 	email              = '<mmgen@tuta.io>'
@@ -50,10 +51,39 @@ 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 = {
+		'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'),
+	}
 
 	prog_name = os.path.basename(sys.argv[0])
-	is_txprog = prog_name == 'mmgen-regtest' or prog_name.startswith('mmgen-tx')
+	prog_id = prog_name.removeprefix(f'{proj_id}-')
+	cmd_caps = cmd_caps_data.get(prog_id)
 
 	if sys.platform not in ('linux', 'win32', 'darwin'):
 		die2(1,f'{sys.platform!r}: platform not supported by {proj_name}')
@@ -116,6 +146,7 @@ class Config(Lockable):
 	_autolock = False
 	_set_ok = ('usr_randchars','_proto')
 	_reset_ok = ('accept_defaults',)
+	_delete_ok = ('_opts',)
 	_use_class_attr = True
 	_default_to_none = True
 
@@ -399,7 +430,7 @@ class Config(Lockable):
 		"""
 		if not hasattr(self,'_data_dir'):
 			self._data_dir = os.path.normpath(os.path.join(*{
-				'regtest': (self.data_dir_root, 'regtest', self.coin.lower(), (self.regtest_user or 'none') ),
+				'regtest': (self.data_dir_root, 'regtest', (self.regtest_user or 'none')),
 				'testnet': (self.data_dir_root, 'testnet'),
 				'mainnet': (self.data_dir_root,),
 			}[self.network] ))
@@ -415,8 +446,8 @@ class Config(Lockable):
 			parsed_opts  = None,
 			need_proto   = True,
 			need_amt     = True,
-			do_post_init = False,
-			process_opts = False ):
+			caller_post_init = False,
+			process_opts = False):
 
 		# Step 1: get user-supplied configuration data from
 		#           a) command line, or
@@ -433,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:
@@ -499,7 +530,7 @@ class Config(Lockable):
 		# Step 6: set auto typeset opts from user-supplied data or cfgfile data, in that order:
 		self._set_auto_typeset_opts( self._cfgfile_opts.auto_typeset )
 
-		if self.regtest or self.bob or self.alice or self.carol or gc.prog_name == 'mmgen-regtest':
+		if self.regtest or self.bob or self.alice or self.carol or gc.prog_name == f'{gc.proj_id}-regtest':
 			self.network = 'regtest'
 			self.regtest_user = 'bob' if self.bob else 'alice' if self.alice else 'carol' if self.carol else None
 		else:
@@ -535,14 +566,27 @@ class Config(Lockable):
 			from .protocol import init_proto_from_cfg, warn_trustlevel
 			# requires the default-to-none behavior, so do after the lock:
 			self._proto = init_proto_from_cfg(self,need_amt=need_amt)
-			warn_trustlevel(self) # do this after initializing proto
 
-		if self._opts and not do_post_init:
-			self._opts.init_bottom(self)
+		if self._opts and not caller_post_init:
+			self._post_init()
 
 		# Check user-set opts without modifying them
 		check_opts(self)
 
+		if need_proto:
+			warn_trustlevel(self) # do this only after proto is initialized
+
+	def _post_init(self):
+		if self.help or self.longhelp:
+			from .help import print_help
+			print_help(self, self._opts) # exits
+		del self._opts
+
+	def _usage(self):
+		from .help import make_usage_str
+		print(make_usage_str(self, caller='user'))
+		sys.exit(1) # called only on bad invocation
+
 	def _set_cfg_from_env(self):
 		for name,val in ((k,v) for k,v in os.environ.items() if k.startswith('MMGEN_')):
 			if name == 'MMGEN_DEBUG_ALL':
@@ -691,9 +735,6 @@ class Config(Lockable):
 			elif key in cfgfile_auto_typeset_opts:
 				do_set(key, cfgfile_auto_typeset_opts[key], ref_type)
 
-	def _post_init(self):
-		return self._opts.init_bottom(self)
-
 	def _die_on_incompatible_opts(self):
 		for group in self._incompatible_opts:
 			bad = [k for k in self.__dict__ if k in group and getattr(self,k) is not None]

+ 1 - 1
mmgen/data/release_date

@@ -1 +1 @@
-September 2024
+October 2024

+ 1 - 1
mmgen/data/version

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

+ 1 - 0
mmgen/exception.py

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

+ 2 - 2
mmgen/fileutil.py

@@ -129,9 +129,9 @@ def get_seed_file(cfg,nargs,wallets=None,invoked_as=None):
 	if len(wallets) + (wd_from_opt or bool(wf)) < nargs:
 		if not wf:
 			msg('No default wallet found, and no other seed source was specified')
-		cfg._opts.usage()
+		cfg._usage()
 	elif len(wallets) > nargs:
-		cfg._opts.usage()
+		cfg._usage()
 	elif len(wallets) == nargs and wf and invoked_as != 'gen':
 		cfg._util.qmsg('Warning: overriding default wallet with user-supplied wallet')
 

+ 145 - 257
mmgen/help/__init__.py

@@ -20,262 +20,150 @@
 help: help notes for MMGen suite commands
 """
 
-from ..cfg import gc
-
-def help_notes_func(proto,cfg,k):
-
-	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 fee_spec_names():
-		cu = proto.coin_amt.units
-		return ', '.join(cu[:-1]) + ('',' and ')[len(cu)>1] + cu[-1] + ('',',\nrespectively')[len(cu)>1]
-
-	def coind_exec():
-		from ..daemon import CoinDaemon
-		return (
-			CoinDaemon(cfg,proto.coin).exec_fn if proto.coin in CoinDaemon.coins else 'bitcoind' )
-
-	class help_notes:
-
-		def dfl_twname():
-			from ..proto.btc.rpc import BitcoinRPCClient
-			return BitcoinRPCClient.dfl_twname
-
-		def MasterShareIdx():
-			from ..seedsplit import MasterShareIdx
-			return MasterShareIdx
-
-		def tool_help():
-			from ..tool.help import main_help
-			return main_help()
-
-		def dfl_subseeds():
-			from ..subseed import SubSeedList
-			return str(SubSeedList.dfl_len)
-
-		def dfl_seed_len():
-			from ..seed import Seed
-			return str(Seed.dfl_len)
-
-		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()]
-			)
-
-		def dfl_mmtype():
-			from ..addr import MMGenAddrType
-			return "'{}' or '{}'".format(
-				proto.dfl_mmtype,
-				MMGenAddrType.mmtypes[proto.dfl_mmtype].name )
-
-		def address_types():
-			from ..addr import MMGenAddrType
-			return '\n  '.join([
-				"'{}','{:<12} - {}".format( k, v.name+"'", v.desc )
-					for k,v in MMGenAddrType.mmtypes.items()
-			])
-
-		def fmt_codes():
-			from ..wallet import format_fmt_codes
-			return '\n  '.join( format_fmt_codes().splitlines() )
-
-		def coin_id():
-			return proto.coin_id
-
-		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) )
-
-		def coind_exec():
-			return coind_exec()
-
-		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 rel_fee_desc():
-			from ..tx import BaseTX
-			return BaseTX(cfg=cfg,proto=proto).rel_fee_desc
-
-		def fee_spec_letters():
-			return fee_spec_letters()
-
-		def fee():
-			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 = 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, it’s 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 = 'S' if 'segwit' in proto.caps else 'C'
-			from ..tool.coin import tool_cmd
-			t = tool_cmd(cfg,mmtype=mmtype)
-			sample_addr = t.privhex2addr('bead'*16)
-
-			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
+import sys, re
 
-  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 transaction’s outputs are listed on the command line, while its inputs
-are chosen from a list of the wallet’s 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 transaction’s 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 can’t 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 wallet’s 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 doesn’t 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()
+from ..cfg import gc
 
-	return getattr(help_notes,k)()
+def version(cfg):
+	from ..util import fmt
+	print(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())
+	sys.exit(0)
+
+def show_hash_presets(cfg):
+	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)
+
+def make_usage_str(cfg, caller):
+	indent, col1_w = {
+		'help': (2, len(gc.prog_name) + 1),
+		'user': (0, len('USAGE:')),
+	}[caller]
+	def gen():
+		ulbl = 'USAGE:'
+		for line in [cfg._usage_data.strip()] if isinstance(cfg._usage_data, str) else cfg._usage_data:
+			yield f'{ulbl:{col1_w}} {gc.prog_name} {line}'
+			ulbl = ''
+	return ('\n' + (' ' * indent)).join(gen())
+
+def usage(cfg):
+	print(make_usage_str(cfg, caller='user'))
+	sys.exit(0)
+
+class Help:
+
+	def make(self, cfg, opts, proto):
+
+		def gen_arg_tuple(func, text):
+
+			def help_notes(k):
+				import importlib
+				return getattr(importlib.import_module(
+					f'{opts.help_pkg}.help_notes').help_notes(proto, cfg), k)()
+
+			def help_mod(modname):
+				import importlib
+				return importlib.import_module(
+					f'{opts.help_pkg}.{modname}').help(proto, cfg)
+
+			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_output():
+			yield '  {} {}'.format(gc.prog_name.upper() + ':', text['desc'].strip())
+			yield make_usage_str(cfg, caller='help')
+			yield help_type.upper().replace('_', ' ') + ':'
+
+			# 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
+
+			# 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()
+
+		text = opts.opts_data['text']
+		code = opts.opts_data['code']
+		help_type = self.help_type
+		nl = '\n  '
+
+		return nl.join(gen_output()) + '\n'
+
+class CmdHelp(Help):
+
+	help_type = 'options'
+
+	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
+
+class GlobalHelp(Help):
+
+	help_type = 'global_options'
+
+	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 print_help(cfg, opts):
+
+	from ..protocol import init_proto_from_cfg
+	proto = init_proto_from_cfg(cfg, need_amt=True)
+
+	if not 'code' in opts.opts_data:
+		opts.opts_data['code'] = {}
+
+	if cfg.help:
+		cls = CmdHelp
+	else:
+		opts.opts_data['code']['global_options'] = opts.global_opts_data['code']
+		cls = GlobalHelp
+
+	from ..ui import do_pager
+	do_pager(cls().make(cfg, opts, proto))
+	sys.exit(0)

+ 268 - 0
mmgen/help/help_notes.py

@@ -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, it’s 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 transaction’s outputs are listed on the command line, while its inputs
+are chosen from a list of the wallet’s 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 transaction’s 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 can’t 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 wallet’s 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 doesn’t 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()

+ 2 - 2
mmgen/main_addrgen.py

@@ -49,7 +49,7 @@ opts_data = {
 		'usage':'[opts] [seed source] <index list or range(s)>',
 		'options': """
 -h, --help            Print this help message
---, --longhelp        Print help message for long options (common options)
+--, --longhelp        Print help message for long (global) options
 -A, --no-addresses    Print only secret keys, no addresses
 -c, --print-checksum  Print address list checksum and exit
 -d, --outdir=      d  Output files to directory 'd' instead of working dir
@@ -134,7 +134,7 @@ addr_type = MMGenAddrType(
 	errmsg = f'{cfg.type!r}: invalid parameter for --type option' )
 
 if len(cfg._args) < 1:
-	cfg._opts.usage()
+	cfg._usage()
 
 if cfg.keygen_backend:
 	from .keygen import check_backend

+ 1 - 1
mmgen/main_addrimport.py

@@ -32,7 +32,7 @@ opts_data = {
 		'usage':'[opts] [MMGen address file]',
 		'options': """
 -h, --help         Print this help message
---, --longhelp     Print help message for long options (common options)
+--, --longhelp     Print help message for long (global) options
 -a, --address=a    Import the single coin address 'a'
 -b, --batch        Import all addresses in one RPC call
 -l, --addrlist     Address source is a flat list of non-MMGen coin addresses

+ 3 - 3
mmgen/main_autosign.py

@@ -33,7 +33,7 @@ opts_data = {
 		'usage':'[opts] [operation]',
 		'options': """
 -h, --help            Print this help message
---, --longhelp        Print help message for long options (common options)
+--, --longhelp        Print help message for long (global) options
 -c, --coins=c         Coins to sign for (comma-separated list)
 -I, --no-insert-check Don’t check for device insertion
 -l, --seed-len=N      Specify wallet seed length of ‘N’ bits (for setup only)
@@ -192,9 +192,9 @@ cfg = Config(
 		'hash_preset': '1',
 		'label': 'Autosign Wallet',
 	},
-	do_post_init = True )
+	caller_post_init = True)
 
-cmd = cfg._args[0] if len(cfg._args) == 1 else 'sign' if not cfg._args else cfg._opts.usage()
+cmd = cfg._args[0] if len(cfg._args) == 1 else 'sign' if not cfg._args else cfg._usage()
 
 if cmd not in Autosign.cmds + Autosign.util_cmds:
 	die(1,f'‘{cmd}’: unrecognized command')

+ 5 - 5
mmgen/main_msg.py

@@ -102,7 +102,7 @@ opts_data = {
 		],
 		'options': """
 -h, --help           Print this help message
---, --longhelp       Print help message for long options (common options)
+--, --longhelp       Print help message for long (global) options
 -d, --outdir=d       Output file to directory 'd' instead of working dir
 -t, --msghash-type=T Specify the message hash type.  Supported values:
                      'eth_sign' (ETH default), 'raw' (non-ETH default)
@@ -207,7 +207,7 @@ cfg = Config( opts_data=opts_data, need_amt=False )
 cmd_args = cfg._args
 
 if len(cmd_args) < 2:
-	cfg._opts.usage()
+	cfg._usage()
 
 op = cmd_args.pop(0)
 
@@ -217,15 +217,15 @@ if cfg.msghash_type and op != 'create':
 async def main():
 	if op == 'create':
 		if len(cmd_args) < 2:
-			cfg._opts.usage()
+			cfg._usage()
 		MsgOps.create( cmd_args[0], ' '.join(cmd_args[1:]) )
 	elif op == 'sign':
 		if len(cmd_args) < 1:
-			cfg._opts.usage()
+			cfg._usage()
 		await MsgOps.sign( cmd_args[0], cmd_args[1:] )
 	elif op in ('verify','export'):
 		if len(cmd_args) not in (1,2):
-			cfg._opts.usage()
+			cfg._usage()
 		await getattr(MsgOps,op)( cmd_args[0], cmd_args[1] if len(cmd_args) == 2 else None )
 	else:
 		die(1,f'{op!r}: unrecognized operation')

+ 2 - 2
mmgen/main_passgen.py

@@ -42,7 +42,7 @@ opts_data = {
 		'usage':'[opts] [seed source] <ID string> <index list or range(s)>',
 		'options': """
 -h, --help            Print this help message
---, --longhelp        Print help message for long options (common options)
+--, --longhelp        Print help message for long (global) options
 -d, --outdir=      d  Output files to directory 'd' instead of working dir
 -e, --echo-passphrase Echo passphrase or mnemonic to screen upon entry
 -f, --passwd-fmt=  f  Generate passwords of format 'f'.  Default: {pl.dfl_pw_fmt}.
@@ -138,7 +138,7 @@ FMT CODES:
 cfg = Config(opts_data=opts_data)
 
 if len(cfg._args) < 2:
-	cfg._opts.usage()
+	cfg._usage()
 
 pw_idxs = AddrIdxList(fmt_str=cfg._args.pop())
 

+ 2 - 2
mmgen/main_regtest.py

@@ -31,7 +31,7 @@ opts_data = {
 		'usage':   '[opts] <command>',
 		'options': """
 -h, --help          Print this help message
---, --longhelp      Print help message for long options (common options)
+--, --longhelp      Print help message for long (global) options
 -b, --bdb-wallet    Create and use a legacy Berkeley DB coin daemon wallet
 -e, --empty         Don't fund Bob and Alice's wallets on setup
 -n, --setup-no-stop-daemon  Don't stop daemon after setup is finished
@@ -77,7 +77,7 @@ def check_num_args():
 		die(1,m.format(args,'many','more',amax))
 
 if not cmd_args:
-	cfg._opts.usage()
+	cfg._usage()
 elif cmd_args[0] not in MMGenRegtest.usr_cmds:
 	die(1,f'{cmd_args[0]!r}: invalid command')
 elif cmd_args[0] not in ('cli','wallet_cli','balances'):

+ 2 - 2
mmgen/main_seedjoin.py

@@ -35,7 +35,7 @@ opts_data = {
 		'usage': '[options] share1 share2 [...shareN]',
 		'options': """
 -h, --help            Print this help message
---, --longhelp        Print help message for long options (common options)
+--, --longhelp        Print help message for long (global) options
 -d, --outdir=      d  Output file to directory 'd' instead of working dir
 -e, --echo-passphrase Echo passphrases and other user input to screen
 -i, --id-str=      s  ID String of split (required for master share join only)
@@ -111,7 +111,7 @@ def print_shares_info():
 cfg = Config(opts_data=opts_data)
 
 if len(cfg._args) + bool(cfg.hidden_incog_input_params) < 2:
-	cfg._opts.usage()
+	cfg._usage()
 
 if cfg.master_share:
 	master_idx = MasterShareIdx(cfg.master_share)

+ 1 - 1
mmgen/main_split.py

@@ -35,7 +35,7 @@ opts_data = {
 		'usage':'[opts] [output addr1] [output addr2]',
 		'options': """
 -h, --help           Print this help message
---, --longhelp       Print help message for long options (common options)
+--, --longhelp       Print help message for long (global) options
 -f, --tx-fees=     f The transaction fees for each chain (comma-separated)
 -c, --other-coin=  c The coin symbol of the other chain (default: {oc})
 -B, --no-blank       Don't blank screen before displaying unspent outputs

+ 2 - 2
mmgen/main_tool.py

@@ -32,7 +32,7 @@ opts_data = {
 		'options': """
 -d, --outdir=       d  Specify an alternate directory 'd' for output
 -h, --help             Print this help message
---, --longhelp         Print help message for long options (common options)
+--, --longhelp         Print help message for long (global) options
 -e, --echo-passphrase  Echo passphrase or mnemonic to screen upon entry
 -k, --use-internal-keccak-module Force use of the internal keccak module
 -K, --keygen-backend=n Use backend 'n' for public key generation.  Options
@@ -359,7 +359,7 @@ if gc.prog_name.endswith('-tool'):
 		sys.exit(0)
 
 	if len(po.cmd_args) < 1:
-		cfg._opts.usage()
+		cfg._usage()
 
 	cmd = po.cmd_args[0]
 

+ 1 - 1
mmgen/main_txbump.py

@@ -36,7 +36,7 @@ opts_data = {
 		'usage':   f'[opts] [{gc.proj_name} TX file] [seed source] ...',
 		'options': """
 -h, --help             Print this help message
---, --longhelp         Print help message for long options (common options)
+--, --longhelp         Print help message for long (global) options
 -a, --autosign         Bump the most recent transaction created and sent with
                        the --autosign option. The removable device is mounted
                        and unmounted automatically.  The transaction file

+ 1 - 1
mmgen/main_txcreate.py

@@ -31,7 +31,7 @@ opts_data = {
 		'usage':   '[opts]  [<addr,amt> ...] <change addr, addrlist ID or addr type> [addr file ...]',
 		'options': """
 -h, --help            Print this help message
---, --longhelp        Print help message for long options (common options)
+--, --longhelp        Print help message for long (global) options
 -a, --autosign        Create a transaction for offline autosigning (see
                       ‘mmgen-autosign’). The removable device is mounted and
                       unmounted automatically

+ 1 - 1
mmgen/main_txdo.py

@@ -32,7 +32,7 @@ opts_data = {
 					'[seed source ...]',
 		'options': """
 -h, --help             Print this help message
---, --longhelp         Print help message for long options (common options)
+--, --longhelp         Print help message for long (global) options
 -A, --fee-adjust=    f Adjust transaction fee by factor 'f' (see below)
 -b, --brain-params=l,p Use seed length 'l' and hash preset 'p' for
                        brainwallet input

+ 2 - 2
mmgen/main_txsend.py

@@ -35,7 +35,7 @@ opts_data = {
 		'usage':   '[opts] [signed transaction file]',
 		'options': """
 -h, --help      Print this help message
---, --longhelp  Print help message for long options (common options)
+--, --longhelp  Print help message for long (global) options
 -a, --autosign  Send an autosigned transaction created by ‘mmgen-txcreate
                 --autosign’.  The removable device is mounted and unmounted
                 automatically. The transaction file argument must be omitted
@@ -78,7 +78,7 @@ elif not cfg._args and cfg.autosign:
 		infile = si.get_unsent()
 		cfg._util.qmsg(f'Got signed transaction file ‘{infile}’')
 else:
-	cfg._opts.usage()
+	cfg._usage()
 
 if not cfg.status:
 	from .ui import do_license_msg

+ 2 - 2
mmgen/main_txsign.py

@@ -33,7 +33,7 @@ opts_data = {
 		'usage':   '[opts] <transaction file>... [seed source]...',
 		'options': """
 -h, --help            Print this help message
---, --longhelp        Print help message for long options (common options)
+--, --longhelp        Print help message for long (global) options
 -b, --brain-params=l,p Use seed length 'l' and hash preset 'p' for
                       brainwallet input
 -d, --outdir=      d  Specify an alternate directory 'd' for output
@@ -102,7 +102,7 @@ cfg = Config(opts_data=opts_data)
 infiles = cfg._args
 
 if not infiles:
-	cfg._opts.usage()
+	cfg._usage()
 
 from .fileutil import check_infile
 for i in infiles:

+ 4 - 4
mmgen/main_wallet.py

@@ -85,7 +85,7 @@ opts_data = {
 		'usage': usage,
 		'options': """
 -h, --help            Print this help message
---, --longhelp        Print help message for long options (common options)
+--, --longhelp        Print help message for long (global) options
 -d, --outdir=      d  Output files to directory 'd' instead of working dir
 -e, --echo-passphrase Echo passphrases and other user input to screen
 -f, --force-update    Force update of wallet even if nothing has changed
@@ -166,17 +166,17 @@ elif invoked_as == 'seedsplit':
 				m2 = 'To generate a master share, omit the seed split specifier.'
 				die(1,m1+'  '+m2)
 		elif not sss:
-			cfg._opts.usage()
+			cfg._usage()
 	elif master_share:
 		sss = SeedSplitSpecifier('1:2')
 	else:
-		cfg._opts.usage()
+		cfg._usage()
 
 from .fileutil import check_infile,get_seed_file
 
 if cmd_args:
 	if invoked_as == 'gen' or len(cmd_args) > 1:
-		cfg._opts.usage()
+		cfg._usage()
 	check_infile(cmd_args[0])
 
 sf = get_seed_file(cfg,nargs,invoked_as=invoked_as)

+ 6 - 7
mmgen/main_xmrwallet.py

@@ -54,8 +54,7 @@ opts_data = {
 		],
 		'options': """
 -h, --help                       Print this help message
---, --longhelp                   Print help message for long options (common
-                                 options)
+--, --longhelp                   Print help message for long (global) options
 -a, --autosign                   Use appropriate outdir and other params for
                                  autosigning operations (implies --watch-only).
                                  When this option is in effect, filename argu-
@@ -129,7 +128,7 @@ if cmd_args and cfg.autosign and (
 	cmd_args.insert(1,None)
 
 if len(cmd_args) < 2:
-	cfg._opts.usage()
+	cfg._usage()
 
 op     = cmd_args.pop(0)
 infile = cmd_args.pop(0)
@@ -137,22 +136,22 @@ wallets = spec = None
 
 if op in ('relay', 'submit', 'resubmit', 'abort'):
 	if len(cmd_args) != 0:
-		cfg._opts.usage()
+		cfg._usage()
 elif op in ('txview','txlist'):
 	infile = [infile] + cmd_args
 elif op in ('create','sync','list','view','listview','dump','restore'): # kafile_arg_ops
 	if len(cmd_args) > 1:
-		cfg._opts.usage()
+		cfg._usage()
 	wallets = cmd_args.pop(0) if cmd_args else None
 elif op in ('new', 'transfer', 'sweep', 'sweep_all', 'label'):
 	if len(cmd_args) != 1:
-		cfg._opts.usage()
+		cfg._usage()
 	spec = cmd_args[0]
 elif op in ('export-outputs', 'export-outputs-sign', 'import-key-images'):
 	if not cfg.autosign: # --autosign only for now - TODO
 		die(f'--autosign must be used with command {op!r}')
 	if len(cmd_args) > 1:
-		cfg._opts.usage()
+		cfg._usage()
 	wallets = cmd_args.pop(0) if cmd_args else None
 else:
 	die(1,f'{op!r}: unrecognized operation')

+ 233 - 122
mmgen/opts.py

@@ -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,59 +152,33 @@ 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
---, --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': '',
 		'usage':'[options]',
 		'options': """
 -h, --help         Print this help message
---, --longhelp     Print help message for long (common) options
+--, --longhelp     Print help message for long (global) options
 """
 	}
 }
 
-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,
@@ -97,19 +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
-		self.opt_filter = opt_filter
+		if len(sys.argv) > 257:
+			raise RuntimeError(f'{len(sys.argv) - 1}: too many command-line arguments')
 
-		od['text']['long_options'] = long_opts_data['text']
+		opts_data = opts_data or opts_data_dfl
+		self.opt_filter = opt_filter
 
-		# Make this available to usage()
-		self.usage_data = od['text'].get('usage2') or od['text']['usage']
+		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
@@ -123,74 +212,96 @@ class UserOpts:
 		cfg._parsed_opts = po
 		cfg._use_env = True
 		cfg._use_cfg_file = not 'skip_cfg_file' in uopts
+		# Make this available to usage()
+		cfg._usage_data = opts_data['text'].get('usage2') or opts_data['text']['usage']
 
 		if os.getenv('MMGEN_DEBUG_OPTS'):
 			opt_preproc_debug(po)
 
-		if 'version' in uopts:
-			self.version() # exits
-
-		if 'show_hash_presets' in uopts:
-			self.show_hash_presets() # exits
-
-		if parse_only:
-			return
-
-	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
-
-		# delete unneeded data:
-		for k in ('text','notes','code'):
-			if k in self.opts_data:
-				del self.opts_data[k]
-		del Opts.make_help
-		del Opts.process_uopts
-		del Opts.parse_opts
-
-	def usage(self):
-		from .util import Die
-		Die(1,Opts.make_usage_str(gc.prog_name,'user',self.usage_data))
-
-	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)
+		for funcname in self.info_funcs:
+			if funcname in uopts:
+				import importlib
+				getattr(importlib.import_module(self.help_pkg), funcname)(cfg) # exits
+
+class UserOpts(Opts):
+
+	help_pkg = 'mmgen.help'
+	info_funcs = ('usage', 'version', 'show_hash_presets')
+
+	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'))
+	}
+
+	@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')
+			)

+ 1 - 1
mmgen/proto/btc/params.py

@@ -35,7 +35,7 @@ class mainnet(CoinProtocol.Secp256k1): # chainparams.cpp
 		_finfo(478559,'00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148','BCH',False),
 	]
 	caps            = ('rbf','segwit')
-	mmcaps          = ('key','addr','rpc_init','tx')
+	mmcaps          = ('rpc', 'rpc_init', 'tw', 'msg')
 	base_proto      = 'Bitcoin'
 	base_proto_coin = 'BTC'
 	base_coin       = 'BTC'

+ 4 - 3
mmgen/proto/btc/regtest.py

@@ -169,7 +169,7 @@ class MMGenRegtest(MMGenObject):
 
 		# BCH and LTC daemons refuse to set HD seed with empty blockchain ("in IBD" error),
 		# so generate a block:
-		await self.generate(1,silent=False)
+		await self.generate(1)
 
 		# Unfortunately, we don’t get deterministic output with BCH and LTC even with fixed
 		# hdseed, as their 'sendtoaddress' calls produce non-deterministic TXIDs due to random
@@ -182,8 +182,9 @@ class MMGenRegtest(MMGenObject):
 				wallet = 'miner')
 
 		# Broken litecoind can only mine 431 blocks in regtest mode, so generate just enough
-		# blocks to fund the test suite
-		await self.generate(392,silent=True)
+		# blocks to fund the test suite.  Generation is slow, so divide into chunks:
+		for n in (100, 100, 100, 92): # 392 blocks
+			await self.generate(n)
 
 		gmsg('Setup complete')
 

+ 1 - 1
mmgen/proto/eth/params.py

@@ -30,7 +30,7 @@ class mainnet(CoinProtocol.DummyWIF,CoinProtocol.Secp256k1):
 	chain_names   = ['ethereum','foundation']
 	sign_mode     = 'standalone'
 	caps          = ('token',)
-	mmcaps        = ('key','addr','rpc_init','tx')
+	mmcaps        = ('rpc', 'rpc_init', 'tw', 'msg')
 	base_proto    = 'Ethereum'
 	base_proto_coin = 'ETH'
 	base_coin     = 'ETH'

+ 1 - 1
mmgen/proto/xmr/params.py

@@ -36,7 +36,7 @@ class mainnet(CoinProtocol.DummyWIF,CoinProtocol.Base):
 	pubkey_type    = 'monero' # required by DummyWIF
 	avg_bdi        = 120
 	privkey_len    = 32
-	mmcaps         = ('key','addr')
+	mmcaps         = ('rpc',)
 	ignore_daemon_version = False
 	coin_amt       = 'XMRAmt'
 	sign_mode      = 'standalone'

+ 1 - 1
mmgen/proto/zec/params.py

@@ -25,7 +25,7 @@ class mainnet(mainnet):
 	wif_ver_num    = { 'std': '80', 'zcash_z': 'ab36' }
 	pubkey_types   = ('std','zcash_z')
 	mmtypes        = ('L','C','Z')
-	mmcaps         = ('key','addr')
+	mmcaps         = ()
 	dfl_mmtype     = 'L'
 	avg_bdi        = 75
 

+ 5 - 3
mmgen/protocol.py

@@ -84,9 +84,11 @@ class CoinProtocol(MMGenObject):
 				self.addr_fmt_to_ver_bytes = {v:k for k,v in self.addr_ver_bytes.items()}
 				self.addr_ver_bytes_len = len(list(self.addr_ver_bytes)[0])
 
-			if 'tx' not in self.mmcaps and gc.is_txprog:
-				from .util import die
-				die(2,f'Command {gc.prog_name!r} not supported for coin {self.coin}')
+			if gc.cmd_caps:
+				for cap in gc.cmd_caps.caps:
+					if cap not in self.mmcaps:
+						from .util import die
+						die(2, f'Command {gc.prog_name!r} not supported for coin {self.coin}')
 
 			if self.chain_names:
 				self.chain_name = self.chain_names[0] # first chain name is default

+ 0 - 182
mmgen/share/Opts.py

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

+ 0 - 0
mmgen/share/__init__.py


+ 2 - 0
pyproject.toml

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

+ 1 - 1
scripts/create-bip-hd-chain-params.py

@@ -100,7 +100,7 @@ def main():
 	global slip44_data, bip_utils_data
 
 	if len(cfg._args) != 1:
-		cfg._opts.usage()
+		cfg._usage()
 
 	with open(cfg._args[0]) as fh:
 		slip44_data = json.loads(fh.read())

+ 1 - 1
scripts/create-token.py

@@ -275,7 +275,7 @@ def main():
 		die(1,'--coin option must be ETH or ETC')
 
 	if not len(cfg._args) == 1:
-		cfg._opts.usage()
+		cfg._usage()
 
 	code = create_src( cfg, solidity_code_template, token_data )
 

+ 10 - 5
scripts/gendiff.py

@@ -18,7 +18,8 @@ The cleaned source files are saved with the .clean extension.
 import sys,re
 from difflib import unified_diff
 
-fns = sys.argv[1:]
+fns = sys.argv[1:3]
+diff_opts = sys.argv[4:] if sys.argv[3:4] == ['--'] else None
 
 translate = {
 	'\r': None,
@@ -55,7 +56,11 @@ cleaned_texts = [cleanup_file(fn) for fn in fns]
 if len(fns) == 2:
 	# chunk headers have trailing newlines, hence the rstrip()
 	sys.stderr.write('Generating diff\n')
-	print(
-		f'diff a/{fns[0]} b/{fns[1]}\n' +
-		'\n'.join(a.rstrip() for a in unified_diff(*cleaned_texts,fromfile=f'a/{fns[0]}',tofile=f'b/{fns[1]}'))
-	)
+	if diff_opts:
+		from subprocess import run
+		run(['diff', '-u'] + [f'{fn}.clean' for fn in fns])
+	else:
+		print(
+			f'diff a/{fns[0]} b/{fns[1]}\n' +
+			'\n'.join(a.rstrip() for a in unified_diff(*cleaned_texts,fromfile=f'a/{fns[0]}',tofile=f'b/{fns[1]}'))
+		)

+ 1 - 1
scripts/tx-v2-to-v3.py

@@ -29,7 +29,7 @@ opts_data = {
 cfg = Config(opts_data=opts_data)
 
 if len(cfg._args) != 1:
-	cfg._opts.usage()
+	cfg._usage()
 
 tx = asyncio.run(CompletedTX(cfg._args[0],quiet_open=True))
 tx.file.write(ask_tty=False,ask_overwrite=not cfg.quiet,ask_write=not cfg.quiet)

+ 0 - 1
setup.cfg

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

+ 1 - 1
test/clean.py

@@ -26,7 +26,7 @@ opts_data = {
 		'usage':'[options]',
 		'options': """
 -h, --help           Print this help message
---, --longhelp       Print help message for long options (common options)
+--, --longhelp       Print help message for long (global) options
 """,
 	},
 }

+ 1 - 2
test/cmdtest.py

@@ -119,7 +119,7 @@ opts_data = {
 		'usage':'[options] [command [..command]] | [command_group[.command_subgroup][:command]]',
 		'options': """
 -h, --help           Print this help message
---, --longhelp       Print help message for long options (common options)
+--, --longhelp       Print help message for long (global) options
 -a, --no-altcoin     Skip altcoin tests (WIP)
 -A, --no-daemon-autostart Don't start and stop daemons automatically
 -B, --bech32         Generate and use Bech32 addresses
@@ -574,7 +574,6 @@ class CmdTestRunner:
 			([] if no_exec_wrapper else ['scripts/exec_wrapper.py']) +
 			[cmd_path] +
 			([] if no_passthru_opts else self.passthru_opts) +
-			self.tg.extra_spawn_args +
 			args )
 
 		try:

+ 2 - 1
test/cmdtest_py_d/cfg.py

@@ -21,12 +21,13 @@ cmd_groups_dfl = {
 	'misc':             ('CmdTestMisc',{}),
 	'opts':             ('CmdTestOpts',{'full_data':True}),
 	'cfgfile':          ('CmdTestCfgFile',{'full_data':True}),
-	'helpscreens':      ('CmdTestHelp',{'modname':'misc','full_data':True}),
+	'help':             ('CmdTestHelp',{'full_data':True}),
 	'main':             ('CmdTestMain',{'full_data':True}),
 	'conv':             ('CmdTestWalletConv',{'is3seed':True,'modname':'wallet'}),
 	'ref':              ('CmdTestRef',{}),
 	'ref3':             ('CmdTestRef3Seed',{'is3seed':True,'modname':'ref_3seed'}),
 	'ref3_addr':        ('CmdTestRef3Addr',{'is3seed':True,'modname':'ref_3seed'}),
+	'ref3_pw':          ('CmdTestRef3Passwd',{'is3seed':True,'modname':'ref_3seed'}),
 	'ref_altcoin':      ('CmdTestRefAltcoin',{}),
 	'seedsplit':        ('CmdTestSeedSplit',{}),
 	'tool':             ('CmdTestTool',{'full_data':True}),

+ 0 - 15
test/cmdtest_py_d/ct_automount.py

@@ -24,7 +24,6 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtestBDBWallet)
 
 	networks = ('btc', 'bch', 'ltc')
 	tmpdir_nums = [49]
-	extra_spawn_args = []
 
 	rtFundAmt = None # pylint
 	rt_data = {
@@ -41,7 +40,6 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtestBDBWallet)
 		('alice_bal1',                       'checking Alice’s balance'),
 		('alice_txcreate1',                  'creating a transaction'),
 		('alice_txcreate_bad_have_unsigned', 'creating the transaction again (error)'),
-		('copy_wallet',                      'copying Alice’s wallet'),
 		('alice_run_autosign_setup',         'running ‘autosign setup’ (with default wallet)'),
 		('wait_loop_start',                  'starting autosign wait loop'),
 		('alice_txstatus1',                  'getting transaction status (unsigned)'),
@@ -165,19 +163,6 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtestBDBWallet)
 	def alice_txcreate_bad_have_unsent(self):
 		return self._alice_txcreate(chg_addr='C:5', exit_val=2, expect_str='unsent transaction')
 
-	def copy_wallet(self):
-		self.spawn('', msg_only=True)
-		if cfg.coin == 'BTC':
-			return 'skip_msg'
-		src  = Path(self.tr.data_dir, 'regtest', cfg.coin.lower(), 'alice')
-		dest = Path(self.tr.data_dir, 'regtest', 'btc', 'alice')
-		dest.mkdir(parents=True, exist_ok=True)
-		wf = Path(get_file_with_ext(src, 'mmdat')).absolute()
-		link_path = dest / wf.name
-		if not link_path.exists():
-			link_path.symlink_to(wf)
-		return 'ok'
-
 	def alice_run_autosign_setup(self):
 		return self.run_setup(mn_type='default', use_dfl_wallet=True, passwd=rt_pw)
 

+ 19 - 12
test/cmdtest_py_d/ct_automount_eth.py

@@ -59,7 +59,7 @@ class CmdTestAutosignETH(CmdTestAutosignThreaded, CmdTestEthdev):
 		CmdTestAutosignThreaded.__init__(self, trunner, cfgs, spawn)
 		CmdTestEthdev.__init__(self, trunner, cfgs, spawn)
 
-		self.txcreate_args = ['--quiet']
+		self.txop_opts = ['--autosign', '--regtest=1', '--quiet']
 
 	def fund_mmgen_address(self):
 		keyfile = os.path.join(self.tmpdir, parity_devkey_fn)
@@ -80,11 +80,15 @@ class CmdTestAutosignETH(CmdTestAutosignThreaded, CmdTestEthdev):
 
 	def create_tx(self):
 		self.insert_device_online()
-		t = self.txcreate(
-			args = ['--autosign', '98831F3A:E:11,54.321'],
-			menu = [],
-			print_listing = False,
-			acct = '1')
+		t = self.spawn('mmgen-txcreate', self.txop_opts + ['-B', '98831F3A:E:11,54.321'])
+		t = self.txcreate_ui_common(
+			t,
+			caller            = 'txcreate',
+			input_sels_prompt = 'to spend from',
+			inputs            = '1',
+			file_desc         = 'transaction',
+			interactive_fee   = '50G',
+			fee_desc          = 'transaction fee or gas price')
 		t.read()
 		self.remove_device_online()
 		return t
@@ -95,7 +99,7 @@ class CmdTestAutosignETH(CmdTestAutosignThreaded, CmdTestEthdev):
 	def send_tx(self, add_args=[]):
 		self._wait_signed('transaction')
 		self.insert_device_online()
-		t = self.spawn('mmgen-txsend', ['--quiet', '--autosign'] + add_args)
+		t = self.spawn('mmgen-txsend', self.txop_opts + add_args)
 		t.view_tx('t')
 		t.expect('(y/N): ', 'n')
 		self._do_confirm_send(t, quiet=True)
@@ -117,17 +121,20 @@ class CmdTestAutosignETH(CmdTestAutosignThreaded, CmdTestEthdev):
 		return self.token_bal(pat=r':E:11\s+998.76544\s+54.318\d+\s+.*:E:12\s+1\.23456\s+')
 
 	def token_bal(self, pat):
-		t = self.spawn('mmgen-tool', ['--quiet', '--token=mm1', 'twview', 'wide=1'])
+		t = self.spawn('mmgen-tool', ['--regtest=1', '--token=mm1', 'twview', 'wide=1'])
 		text = t.read(strip_color=True)
 		assert re.search(pat, text, re.DOTALL), f'output failed to match regex {pat}'
 		return t
 
 	def create_token_tx(self):
 		self.insert_device_online()
-		t = self.token_txcreate(
-			args      = ['--autosign', '98831F3A:E:12,1.23456'],
-			token     = 'MM1',
-			file_desc = 'Unsigned automount transaction')
+		t = self.txcreate_ui_common(
+			self.spawn(
+				'mmgen-txcreate',
+				self.txop_opts + ['--token=MM1', '-B', '--fee=50G', '98831F3A:E:12,1.23456']),
+			inputs            = '1',
+			input_sels_prompt = 'to spend from',
+			file_desc         = 'Unsigned automount transaction')
 		t.read()
 		self.remove_device_online()
 		return t

+ 1 - 1
test/cmdtest_py_d/ct_base.py

@@ -32,7 +32,6 @@ class CmdTestBase:
 	'initializer class for the cmdtest.py test suite'
 	base_passthru_opts = ('data_dir','skip_cfg_file')
 	passthru_opts = ()
-	extra_spawn_args = []
 	networks = ()
 	segwit_opts_ok = False
 	color = False
@@ -56,6 +55,7 @@ class CmdTestBase:
 		self.coin = self.proto.coin.lower()
 		self.fork = 'btc' if self.coin == 'bch' and not cfg.cashaddr else self.coin
 		self.altcoin_pfx = '' if self.fork == 'btc' else f'-{self.proto.coin}'
+		self.testnet_opt = [f'--testnet=1'] if cfg.testnet else []
 		if len(self.tmpdir_nums) == 1:
 			self.tmpdir_num = self.tmpdir_nums[0]
 		if self.tr:

+ 17 - 18
test/cmdtest_py_d/ct_ethdev.py

@@ -163,7 +163,6 @@ class CmdTestEthdev(CmdTestBase,CmdTestShared):
 	'Ethereum transacting, token deployment and tracking wallet operations'
 	networks = ('eth','etc')
 	passthru_opts = ('coin','daemon_id','http_timeout','rpc_backend')
-	extra_spawn_args = ['--regtest=1']
 	tmpdir_nums = [22]
 	color = True
 	cmd_group_in = (
@@ -402,9 +401,8 @@ class CmdTestEthdev(CmdTestBase,CmdTestShared):
 		if trunner is None:
 			return
 
-		self.txcreate_args    = [f'--outdir={self.tmpdir}', '--quiet']
-		self.eth_args         = [f'--outdir={self.tmpdir}', '--quiet']
-		self.eth_args_noquiet = [f'--outdir={self.tmpdir}']
+		self.eth_args         = [f'--outdir={self.tmpdir}', '--regtest=1', '--quiet']
+		self.eth_args_noquiet = [f'--outdir={self.tmpdir}', '--regtest=1']
 
 		from mmgen.protocol import init_proto
 		self.proto = init_proto( cfg, cfg.coin, network='regtest', need_amt=True )
@@ -611,7 +609,7 @@ class CmdTestEthdev(CmdTestBase,CmdTestShared):
 			exit_val  = None):
 		ext = ext.format('-α' if cfg.debug_utf8 else '')
 		fn = self.get_file_with_ext(ext,no_dot=True,delete=False)
-		t = self.spawn('mmgen-addrimport', self.eth_args[1:-1] + add_args + [fn], exit_val=exit_val)
+		t = self.spawn('mmgen-addrimport', ['--regtest=1'] + add_args + [fn], exit_val=exit_val)
 		if bad_input:
 			return t
 		t.expect('Importing')
@@ -619,7 +617,7 @@ class CmdTestEthdev(CmdTestBase,CmdTestShared):
 		return t
 
 	def addrimport_one_addr(self,addr=None,extra_args=[]):
-		t = self.spawn('mmgen-addrimport', self.eth_args[1:] + extra_args + ['--address='+addr])
+		t = self.spawn('mmgen-addrimport', ['--regtest=1', '--quiet', f'--address={addr}'] + extra_args)
 		t.expect('OK')
 		return t
 
@@ -641,7 +639,7 @@ class CmdTestEthdev(CmdTestBase,CmdTestShared):
 			print_listing   = True,
 			tweaks          = []):
 		fee_info_pat = r'\D{}\D.*{c} .*\D{}\D.*gas price in Gwei'.format( *fee_info_data, c=self.proto.coin )
-		t = self.spawn('mmgen-'+caller, self.txcreate_args + ['-B'] + args)
+		t = self.spawn(f'mmgen-{caller}', self.eth_args + ['-B'] + args)
 		if print_listing:
 			t.expect(r'add \[l\]abel, .*?:.','p', regex=True)
 			t.written_to_file('Account balances listing')
@@ -691,7 +689,7 @@ class CmdTestEthdev(CmdTestBase,CmdTestShared):
 	def txview(self,ext_fs):
 		ext = ext_fs.format('-α' if cfg.debug_utf8 else '')
 		txfile = self.get_file_with_ext(ext,no_dot=True)
-		return self.spawn( 'mmgen-tool',['--verbose','txview',txfile] )
+		return self.spawn('mmgen-tool', ['--verbose', 'txview', txfile])
 
 	def fund_dev_address(self):
 		"""
@@ -806,26 +804,26 @@ class CmdTestEthdev(CmdTestBase,CmdTestShared):
 		return 'ok'
 
 	def msgcreate(self,add_args=[]):
-		t = self.spawn('mmgen-msg', self.eth_args_noquiet + add_args + [ 'create', self.message, '98831F3A:E:1' ])
+		t = self.spawn('mmgen-msg', self.eth_args_noquiet + add_args + ['create', self.message, '98831F3A:E:1'])
 		t.written_to_file('Unsigned message data')
 		return t
 
 	def msgsign(self):
 		fn = get_file_with_ext(self.tmpdir,'rawmsg.json')
-		t = self.spawn('mmgen-msg', self.eth_args_noquiet + [ 'sign', fn, dfl_words_file ])
+		t = self.spawn('mmgen-msg', self.eth_args_noquiet + ['sign', fn, dfl_words_file])
 		t.written_to_file('Signed message data')
 		return t
 
 	def msgverify(self,fn=None,msghash_type='eth_sign'):
 		fn = fn or get_file_with_ext(self.tmpdir,'sigmsg.json')
-		t = self.spawn('mmgen-msg', self.eth_args_noquiet + [ 'verify', fn ])
+		t = self.spawn('mmgen-msg', self.eth_args_noquiet + ['verify', fn])
 		t.expect(msghash_type)
 		t.expect('1 signature verified')
 		return t
 
 	def msgexport(self):
 		fn = get_file_with_ext(self.tmpdir,'sigmsg.json')
-		t = self.spawn('mmgen-msg', self.eth_args_noquiet + [ 'export', fn ])
+		t = self.spawn('mmgen-msg', self.eth_args_noquiet + ['export', fn])
 		t.written_to_file('Signature data')
 		return t
 
@@ -1160,7 +1158,7 @@ class CmdTestEthdev(CmdTestBase,CmdTestShared):
 
 	def token_txcreate(self,args=[],token='',inputs='1',fee='50G',file_desc='Unsigned transaction'):
 		return self.txcreate_ui_common(
-			self.spawn('mmgen-txcreate', self.txcreate_args + ['--token='+token,'-B','--fee='+fee] + args),
+			self.spawn('mmgen-txcreate', self.eth_args + [f'--token={token}', '-B', f'--fee={fee}'] + args),
 			menu              = [],
 			inputs            = inputs,
 			input_sels_prompt = 'to spend from',
@@ -1311,7 +1309,7 @@ class CmdTestEthdev(CmdTestBase,CmdTestShared):
 		if total_coin is None:
 			total_coin = self.proto.coin
 
-		t = self.spawn('mmgen-txcreate', self.txcreate_args + args)
+		t = self.spawn('mmgen-txcreate', self.eth_args + args)
 		for n in bals:
 			t.expect('[R]efresh balance:\b','R')
 			t.expect(' main menu): ',n+'\n')
@@ -1374,7 +1372,8 @@ class CmdTestEthdev(CmdTestBase,CmdTestShared):
 			comment_text  = None,
 			changed       = False,
 			pexpect_spawn = None):
-		t = self.spawn('mmgen-txcreate', self.txcreate_args + args + ['-B','-i'],pexpect_spawn=pexpect_spawn)
+
+		t = self.spawn('mmgen-txcreate', self.eth_args + args + ['-B', '-i'], pexpect_spawn=pexpect_spawn)
 
 		menu_prompt = 'efresh balance:\b'
 
@@ -1451,9 +1450,9 @@ class CmdTestEthdev(CmdTestBase,CmdTestShared):
 
 	def twimport(self,add_args=[],expect_str=None):
 		from mmgen.tw.json import TwJSON
-		fn = joinpath( self.tmpdir, TwJSON.Base(cfg,self.proto).dump_fn )
-		t = self.spawn('mmgen-tool',self.eth_args_noquiet + ['twimport',fn] + add_args)
-		t.expect('(y/N): ','y')
+		fn = joinpath(self.tmpdir, TwJSON.Base(cfg,self.proto).dump_fn)
+		t = self.spawn('mmgen-tool', self.eth_args_noquiet + ['twimport', fn] + add_args)
+		t.expect('(y/N): ', 'y')
 		if expect_str:
 			t.expect(expect_str)
 		t.written_to_file('tracking wallet data')

+ 187 - 0
test/cmdtest_py_d/ct_help.py

@@ -0,0 +1,187 @@
+#!/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
+
+"""
+test.cmdtest_py_d.ct_help: helpscreen test group for the cmdtest.py test suite
+"""
+
+import sys, os, time
+
+from mmgen.util import ymsg
+from mmgen.cfg import gc
+
+from ..include.common import cfg
+from .ct_base import CmdTestBase
+
+class CmdTestHelp(CmdTestBase):
+	'help, info and usage screens'
+	networks = ('btc', 'ltc', 'bch', 'eth', 'xmr', 'doge')
+	passthru_opts = ('daemon_data_dir','rpc_port','coin','testnet')
+	cmd_group = (
+		('usage1',                (1,'usage message (via --usage)',[])),
+		('usage2',                (1,'usage message (via bad invocation)',[])),
+		('version',               (1,'version message',[])),
+		('license',               (1,'license message',[])),
+		('helpscreens',           (1,'help screens',             [])),
+		('longhelpscreens',       (1,'help screens (--longhelp)',[])),
+		('show_hash_presets',     (1,'info screen (--show-hash-presets)',[])),
+		('tool_help',             (1,"'mmgen-tool' usage screen",[])),
+		('tool_cmd_usage',        (1,"'mmgen-tool' usage screen",[])),
+		('test_help',             (1,"'cmdtest.py' help screens",[])),
+		('tooltest_help',         (1,"'tooltest.py' help screens",[])),
+	)
+
+	def usage1(self):
+		t = self.spawn('mmgen-txsend', ['--usage'], no_passthru_opts=True)
+		t.expect('USAGE: mmgen-txsend')
+		return t
+
+	def usage2(self):
+		t = self.spawn('mmgen-walletgen', ['foo'], exit_val=1, no_passthru_opts=True)
+		t.expect('USAGE: mmgen-walletgen')
+		return t
+
+	def version(self):
+		t = self.spawn('mmgen-tool', ['--version'], exit_val=0)
+		t.expect('MMGEN-TOOL version')
+		return t
+
+	def license(self):
+		t = self.spawn(
+			'mmgen-walletconv',
+			['--stdout', '--in-fmt=hex', '--out-fmt=hex'],
+			env = {'MMGEN_NO_LICENSE':''},
+			no_passthru_opts = True)
+		t.expect('to continue: ', 'w')
+		t.expect('TERMS AND CONDITIONS') # start of GPL text
+		if cfg.pexpect_spawn:
+			t.send('G')
+		t.expect('return for a fee.')    # end of GPL text
+		if cfg.pexpect_spawn:
+			t.send('q')
+		t.expect('to continue: ', 'c')
+		t.expect('data: ','beadcafe'*4 + '\n')
+		t.expect('to confirm: ', 'YES\n')
+		return t
+
+	def spawn_chk_expect(self,*args,**kwargs):
+		expect = kwargs.pop('expect')
+		t = self.spawn(*args,**kwargs)
+		t.expect(expect)
+		if t.pexpect_spawn:
+			time.sleep(0.4)
+			t.send('q')
+		t.read()
+		t.ok()
+		t.skip_ok = True
+		return t
+
+	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
+
+		for cmdname in sorted(set(scripts) - set(list(gen_skiplist()))):
+			t = self.spawn(
+				f'mmgen-{cmdname}',
+				[arg],
+				extra_desc       = f'(mmgen-{cmdname})',
+				no_passthru_opts = not gc.cmd_caps_data[cmdname].proto)
+			t.expect(expect,regex=True)
+			if pager and t.pexpect_spawn:
+				time.sleep(0.2)
+				t.send('q')
+			t.read()
+			t.ok()
+			t.skip_ok = True
+
+		return t
+
+	def longhelpscreens(self):
+		return self.helpscreens(arg='--longhelp',expect='USAGE:.*GLOBAL OPTIONS:')
+
+	def show_hash_presets(self):
+		return self.helpscreens(
+			arg = '--show-hash-presets',
+			scripts = (
+					'walletgen','walletconv','walletchk','passchg','subwalletgen',
+					'addrgen','keygen','passgen',
+					'txsign','txdo','txbump'),
+			expect = 'Available parameters.*Preset',
+			pager  = False )
+
+	def tool_help(self):
+
+		if os.getenv('PYTHONOPTIMIZE') == '2':
+			ymsg('Skipping tool help with PYTHONOPTIMIZE=2 (no docstrings)')
+			return 'skip'
+
+		for arg in (
+			'help',
+			'usage',
+		):
+			t = self.spawn_chk_expect(
+				'mmgen-tool',
+				[arg],
+				extra_desc = f'(mmgen-tool {arg})',
+				expect = 'GENERAL USAGE' )
+		return t
+
+	def tool_cmd_usage(self):
+
+		if os.getenv('PYTHONOPTIMIZE') == '2':
+			ymsg('Skipping tool cmd usage with PYTHONOPTIMIZE=2 (no docstrings)')
+			return 'skip'
+
+		from mmgen.main_tool import mods
+
+		for cmdlist in mods.values():
+			for cmd in cmdlist:
+				t = self.spawn_chk( 'mmgen-tool', ['help',cmd], extra_desc=f'({cmd})' )
+		return t
+
+	def test_help(self):
+		for arg,expect in (
+			('--help','USAGE'),
+			('--list-cmds','AVAILABLE COMMANDS'),
+			('--list-cmd-groups','AVAILABLE COMMAND GROUPS')
+		):
+			t = self.spawn_chk_expect(
+				'cmdtest.py',
+				[arg],
+				cmd_dir = 'test',
+				extra_desc = f'(cmdtest.py {arg})',
+				expect = expect )
+		return t
+
+	def tooltest_help(self):
+		for arg,expect in (
+			('--list-cmds','Available commands'),
+			('--testing-status','Testing status')
+		):
+			t = self.spawn_chk_expect(
+				'tooltest.py',
+				[arg],
+				cmd_dir = 'test',
+				extra_desc = f'(tooltest.py {arg})',
+				expect = expect )
+		return t

+ 12 - 9
test/cmdtest_py_d/ct_main.py

@@ -374,7 +374,7 @@ class CmdTestMain(CmdTestBase,CmdTestShared):
 			args += ['-d',self.tmpdir]
 		if seed_len:
 			args += ['-l',str(seed_len)]
-		t = self.spawn('mmgen-walletgen', args + [self.usr_rand_arg])
+		t = self.spawn('mmgen-walletgen', self.testnet_opt + args + [self.usr_rand_arg], no_passthru_opts=True)
 		t.license()
 		t.usr_rand(self.usr_rand_chars)
 		wcls = MMGenWallet
@@ -390,7 +390,7 @@ class CmdTestMain(CmdTestBase,CmdTestShared):
 		args = [self.usr_rand_arg,'-p1','-d',self.tr.trash_dir,'-L','Label']
 		if wf != 'default':
 			args += [wf]
-		t = self.spawn('mmgen-subwalletgen', args + ['10s'])
+		t = self.spawn('mmgen-subwalletgen', self.testnet_opt + args + ['10s'], no_passthru_opts=True)
 		t.license()
 		wcls = MMGenWallet
 		t.passphrase(wcls.desc,self.cfgs['1']['wpasswd'])
@@ -406,7 +406,7 @@ class CmdTestMain(CmdTestBase,CmdTestShared):
 		icls = get_wallet_cls(ext=get_extension(wf))
 		ocls = get_wallet_cls('words')
 		args = [self.usr_rand_arg,'-p1','-d',self.tr.trash_dir,'-o',ocls.fmt_codes[0],wf,'3L']
-		t = self.spawn('mmgen-subwalletgen', args)
+		t = self.spawn('mmgen-subwalletgen', self.testnet_opt + args, no_passthru_opts=True)
 		t.license()
 		t.passphrase(icls.desc,self.cfgs['1']['wpasswd'])
 		t.expect(r'Generating subseed.*\D3L',regex=True)
@@ -424,7 +424,10 @@ class CmdTestMain(CmdTestBase,CmdTestShared):
 			'keep':    ['-d',self.tr.trash_dir,'--keep-label'],
 			'user':    ['-d',self.tr.trash_dir]
 		}[label_action]
-		t = self.spawn('mmgen-passchg', add_args + [self.usr_rand_arg, '-p2'] + ([wf] if wf else []))
+		t = self.spawn(
+			'mmgen-passchg',
+			self.testnet_opt + add_args + [self.usr_rand_arg, '-p2'] + ([wf] if wf else []),
+			no_passthru_opts = True)
 		t.license()
 		wcls = MMGenWallet
 		t.passphrase(wcls.desc,self.cfgs['1']['wpasswd'],pwtype='old')
@@ -712,7 +715,7 @@ class CmdTestMain(CmdTestBase,CmdTestShared):
 	def _walletconv_export(self,wf,uargs=[],out_fmt='w',pf=None):
 		opts = ['-d',self.tmpdir,'-o',out_fmt] + uargs + \
 			([],[wf])[bool(wf)] + ([],['-P',pf])[bool(pf)]
-		t = self.spawn('mmgen-walletconv',opts)
+		t = self.spawn('mmgen-walletconv', self.testnet_opt + opts, no_passthru_opts=True)
 		t.license()
 
 		if not pf:
@@ -779,9 +782,9 @@ class CmdTestMain(CmdTestBase,CmdTestShared):
 		stdout = wcls.type == 'seed' # capture output to screen once
 		t = self.spawn(
 			'mmgen-addrgen',
-			(['-S'] if stdout else []) +
-			self.segwit_arg +
-			['-i' + in_fmt, '-d', self.tmpdir, wf, self.addr_idx_list],
+			(['-S'] if stdout else [])
+			+ self.segwit_arg
+			+ ['-i' + in_fmt, '-d', self.tmpdir, wf, self.addr_idx_list],
 			exit_val = None if stdout else 1)
 		t.license()
 		t.expect_getend(f'Valid {wcls.desc} for Seed ID ')
@@ -888,7 +891,7 @@ class CmdTestMain(CmdTestBase,CmdTestShared):
 		make_brainwallet_file(bwf)
 		seed_len = str(self.seed_len)
 		args = ['-d',self.tmpdir,'-p1',self.usr_rand_arg,'-l'+seed_len,'-ibw']
-		t = self.spawn('mmgen-walletconv', args + [bwf])
+		t = self.spawn('mmgen-walletconv', self.testnet_opt + args + [bwf], no_passthru_opts=True)
 		t.license()
 		wcls = MMGenWallet
 		t.passphrase_new('new ' +wcls.desc,self.wpasswd)

+ 4 - 155
test/cmdtest_py_d/ct_misc.py

@@ -20,12 +20,12 @@
 test.cmdtest_py_d.ct_misc: Miscellaneous test groups for the cmdtest.py test suite
 """
 
-import sys,os,re,time
+import sys, re
 
-from mmgen.util import ymsg, die
+from mmgen.util import die
 
-from ..include.common import cfg,start_test_daemons,stop_test_daemons,imsg
-from .common import get_file_with_ext,dfl_words_file
+from ..include.common import cfg, start_test_daemons, stop_test_daemons, imsg
+from .common import get_file_with_ext, dfl_words_file
 from .ct_base import CmdTestBase
 from .ct_main import CmdTestMain
 
@@ -179,157 +179,6 @@ class CmdTestMisc(CmdTestBase):
 			return 'skip'
 		return self.spawn('test/misc/term_ni.py',['cleanup'],cmd_dir='.',pexpect_spawn=True)
 
-class CmdTestHelp(CmdTestBase):
-	'help, info and usage screens'
-	networks = ('btc','ltc','bch','eth','xmr')
-	passthru_opts = ('daemon_data_dir','rpc_port','coin','testnet')
-	cmd_group = (
-		('usage',                 (1,'usage message',[])),
-		('version',               (1,'version message',[])),
-		('license',               (1,'license message',[])),
-		('helpscreens',           (1,'help screens',             [])),
-		('longhelpscreens',       (1,'help screens (--longhelp)',[])),
-		('show_hash_presets',     (1,'info screen (--show-hash-presets)',[])),
-		('tool_help',             (1,"'mmgen-tool' usage screen",[])),
-		('tool_cmd_usage',        (1,"'mmgen-tool' usage screen",[])),
-		('test_help',             (1,"'cmdtest.py' help screens",[])),
-		('tooltest_help',         (1,"'tooltest.py' help screens",[])),
-	)
-
-	def usage(self):
-		t = self.spawn('mmgen-walletgen', ['foo'], exit_val=1)
-		t.expect('USAGE: mmgen-walletgen')
-		return t
-
-	def version(self):
-		t = self.spawn('mmgen-tool', ['--version'], exit_val=0)
-		t.expect('MMGEN-TOOL version')
-		return t
-
-	def license(self):
-		t = self.spawn(
-			'mmgen-walletconv',
-			['--stdout','--in-fmt=hex','--out-fmt=hex'],
-			env = {'MMGEN_NO_LICENSE':''} )
-		t.expect('to continue: ', 'w')
-		t.expect('TERMS AND CONDITIONS') # start of GPL text
-		if cfg.pexpect_spawn:
-			t.send('G')
-		t.expect('return for a fee.')    # end of GPL text
-		if cfg.pexpect_spawn:
-			t.send('q')
-		t.expect('to continue: ', 'c')
-		t.expect('data: ','beadcafe'*4 + '\n')
-		t.expect('to confirm: ', 'YES\n')
-		return t
-
-	def spawn_chk_expect(self,*args,**kwargs):
-		expect = kwargs.pop('expect')
-		t = self.spawn(*args,**kwargs)
-		t.expect(expect)
-		if t.pexpect_spawn:
-			time.sleep(0.4)
-			t.send('q')
-		t.read()
-		t.ok()
-		t.skip_ok = True
-		return t
-
-	def helpscreens(self,arg='--help',scripts=(),expect='USAGE:.*OPTIONS:',pager=True):
-
-		scripts = list(scripts) or [s.replace('mmgen-','') for s in os.listdir('cmds')]
-
-		if 'tx' not in self.proto.mmcaps:
-			scripts = [s for s in scripts if not (s == 'regtest' or s.startswith('tx'))]
-
-		if 'xmrwallet' in scripts and (cfg.no_altcoin or not self.proto.coin in ('BTC','XMR')):
-			scripts.remove('xmrwallet')
-
-		if 'autosign' in scripts and sys.platform == 'win32':
-			scripts.remove('autosign')
-
-		for s in sorted(scripts):
-			t = self.spawn(f'mmgen-{s}',[arg],extra_desc=f'(mmgen-{s})')
-			t.expect(expect,regex=True)
-			if pager and t.pexpect_spawn:
-				time.sleep(0.2)
-				t.send('q')
-			t.read()
-			t.ok()
-			t.skip_ok = True
-
-		return t
-
-	def longhelpscreens(self):
-		return self.helpscreens(arg='--longhelp',expect='USAGE:.*LONG OPTIONS:')
-
-	def show_hash_presets(self):
-		return self.helpscreens(
-			arg = '--show-hash-presets',
-			scripts = (
-					'walletgen','walletconv','walletchk','passchg','subwalletgen',
-					'addrgen','keygen','passgen',
-					'txsign','txdo','txbump'),
-			expect = 'Available parameters.*Preset',
-			pager  = False )
-
-	def tool_help(self):
-
-		if os.getenv('PYTHONOPTIMIZE') == '2':
-			ymsg('Skipping tool help with PYTHONOPTIMIZE=2 (no docstrings)')
-			return 'skip'
-
-		for arg in (
-			'help',
-			'usage',
-		):
-			t = self.spawn_chk_expect(
-				'mmgen-tool',
-				[arg],
-				extra_desc = f'(mmgen-tool {arg})',
-				expect = 'GENERAL USAGE' )
-		return t
-
-	def tool_cmd_usage(self):
-
-		if os.getenv('PYTHONOPTIMIZE') == '2':
-			ymsg('Skipping tool cmd usage with PYTHONOPTIMIZE=2 (no docstrings)')
-			return 'skip'
-
-		from mmgen.main_tool import mods
-
-		for cmdlist in mods.values():
-			for cmd in cmdlist:
-				t = self.spawn_chk( 'mmgen-tool', ['help',cmd], extra_desc=f'({cmd})' )
-		return t
-
-	def test_help(self):
-		for arg,expect in (
-			('--help','USAGE'),
-			('--list-cmds','AVAILABLE COMMANDS'),
-			('--list-cmd-groups','AVAILABLE COMMAND GROUPS')
-		):
-			t = self.spawn_chk_expect(
-				'cmdtest.py',
-				[arg],
-				cmd_dir = 'test',
-				extra_desc = f'(cmdtest.py {arg})',
-				expect = expect )
-		return t
-
-	def tooltest_help(self):
-		for arg,expect in (
-			('--list-cmds','Available commands'),
-			('--testing-status','Testing status')
-		):
-			t = self.spawn_chk_expect(
-				'tooltest.py',
-				[arg],
-				cmd_dir = 'test',
-				extra_desc = f'(tooltest.py {arg})',
-				expect = expect )
-		return t
-
 class CmdTestOutput(CmdTestBase):
 	'screen output'
 	networks = ('btc',)

+ 156 - 15
test/cmdtest_py_d/ct_opts.py

@@ -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,11 +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,'')
@@ -81,8 +109,7 @@ class CmdTestOpts(CmdTestBase):
 					'--outdir='+self.tmpdir,
 					'--cached-balances',
 					f'--hidden-incog-input-params={pf},123',
-				],
-				(
+				], (
 					('cfg.print_checksum',           'True'),
 					('cfg.quiet',                    'True'), # set by print_checksum
 					('cfg.passwd_file',              pf),
@@ -90,8 +117,69 @@ class CmdTestOpts(CmdTestBase):
 					('cfg.cached_balances',          'True'),
 					('cfg.hidden_incog_input_params', pf+',123'),
 					('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')
@@ -106,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)

+ 10 - 2
test/cmdtest_py_d/ct_ref.py

@@ -188,14 +188,22 @@ class CmdTestRef(CmdTestBase,CmdTestShared):
 		ocls = get_wallet_cls('words')
 		args = ['-d',self.tr.trash_dir,'-o',ocls.fmt_codes[-1],wf,ss_idx]
 
-		t = self.spawn('mmgen-subwalletgen',args,extra_desc='(generate subwallet)')
+		t = self.spawn(
+			'mmgen-subwalletgen',
+			self.testnet_opt + args,
+			extra_desc       = '(generate subwallet)',
+			no_passthru_opts = True)
 		t.expect(f'Generating subseed {ss_idx}')
 		chk_sid = self.chk_data['ref_subwallet_sid'][f'98831F3A:{ss_idx}']
 		fn = t.written_to_file(capfirst(ocls.desc))
 		assert chk_sid in fn,f'incorrect filename: {fn} (does not contain {chk_sid})'
 		ok()
 
-		t = self.spawn('mmgen-walletchk',[fn],extra_desc='(check subwallet)')
+		t = self.spawn(
+			'mmgen-walletchk',
+			self.testnet_opt + [fn],
+			extra_desc       = '(check subwallet)',
+			no_passthru_opts = True)
 		t.expect(r'Valid MMGen native mnemonic data for Seed ID ([0-9A-F]*)\b',regex=True)
 		sid = t.p.match.group(1)
 		assert sid == chk_sid,f'subseed ID {sid} does not match expected value {chk_sid}'

+ 98 - 71
test/cmdtest_py_d/ct_ref_3seed.py

@@ -151,7 +151,7 @@ class CmdTestRef3Seed(CmdTestBase,CmdTestShared):
 		args = ['-d',self.tmpdir,hp_arg,sl_arg,'-ibw','-L',label]
 		self.write_to_tmpfile(bf,ref_wallet_brainpass)
 		self.write_to_tmpfile(pwfile,self.wpasswd)
-		t = self.spawn('mmgen-walletconv', args + [self.usr_rand_arg])
+		t = self.spawn('mmgen-walletconv', self.testnet_opt + args + [self.usr_rand_arg], no_passthru_opts=True)
 		t.license()
 		t.expect('Enter brainwallet: ', ref_wallet_brainpass+'\n')
 		ocls = get_wallet_cls('mmgen')
@@ -213,7 +213,7 @@ class CmdTestRef3Seed(CmdTestBase,CmdTestShared):
 		return self.ref_walletconv_incog(ofmt='incog_hex',ext='mmincox')
 
 class CmdTestRef3Addr(CmdTestRef3Seed):
-	'generated reference address, key and password files for 128-, 192- and 256-bit seeds'
+	'generated reference address and key-address files for 128-, 192- and 256-bit seeds'
 	networks = ('btc', 'btc_tn', 'ltc', 'ltc_tn', 'bch', 'bch_tn')
 	passthru_opts = ('coin', 'testnet', 'cashaddr')
 	tmpdir_nums = [26, 27, 28]
@@ -258,15 +258,6 @@ class CmdTestRef3Addr(CmdTestRef3Seed):
 			'btc': ('934F 1C33 6C06 B18C','A283 5BAB 7AF3 3EA4'),
 			'ltc': ('A6AD DF53 5968 7B6A','9572 43E0 A4DC 0B2E'),
 		},
-		'refpasswdgen_1':     'EB29 DC4F 924B 289F',
-		'refpasswdgen_half_1':'D310 2593 B5D9 2E88',
-		'ref_b32passwdgen_1': '37B6 C218 2ABC 7508',
-		'ref_hexpasswdgen_1': '8E99 E696 84CE E7D5',
-		'ref_hexpasswdgen_half_1': '8E99 E696 84CE E7D5',
-		'ref_bip39_12_passwdgen_1': '834F CF45 0B33 8AF0',
-		'ref_bip39_18_passwdgen_1': '834F CF45 0B33 8AF0',
-		'ref_bip39_24_passwdgen_1': '834F CF45 0B33 8AF0',
-		'ref_hex2bip39_24_passwdgen_1': '91AF E735 A31D 72A0',
 		'refaddrgen_legacy_2': {
 			'btc': ('8C17 A5FA 0470 6E89','764C 66F9 7502 AAEA'),
 			'bch': ('8117 24B6 3FDA 6B40','E58C A8A4 C371 66AE'),
@@ -303,15 +294,6 @@ class CmdTestRef3Addr(CmdTestRef3Seed):
 			'btc': ('4A6B 3762 DF30 9368','12DD 1888 36BA 85F7'),
 			'ltc': ('5C12 FDD4 17AB F179','E195 B28C 59C4 C5EC'),
 		},
-		'refpasswdgen_2':     'ADEA 0083 094D 489A',
-		'refpasswdgen_half_2':'12B3 4929 9506 76E0',
-		'ref_b32passwdgen_2': '2A28 C5C7 36EC 217A',
-		'ref_hexpasswdgen_2': '88F9 0D48 3A7E 7CC2',
-		'ref_hexpasswdgen_half_2': '59F3 8F48 861E 1186',
-		'ref_bip39_12_passwdgen_2': 'D32D B8D7 A840 250B',
-		'ref_bip39_18_passwdgen_2': '0FAA 78DD A6BA 31AD',
-		'ref_bip39_24_passwdgen_2': '0FAA 78DD A6BA 31AD',
-		'ref_hex2bip39_24_passwdgen_2': '0E8E 23C9 923F 7C2D',
 		'refaddrgen_legacy_3': {
 			'btc': ('6FEF 6FB9 7B13 5D91','424E 4326 CFFE 5F51'),
 			'bch': ('E580 43BB 0F96 AA93','630E 174A 8DDE 1BCE'),
@@ -348,38 +330,18 @@ class CmdTestRef3Addr(CmdTestRef3Seed):
 			'btc': ('D0DD BDE3 87BE 15AE','7552 D70C AAB8 DEAA'),
 			'ltc': ('74A0 7DD5 963B 6326','2CDA A007 4B9F E9A5'),
 		},
-		'refpasswdgen_3':     '2D6D 8FBA 422E 1315',
-		'refpasswdgen_half_3':'272C B770 0176 D7EA',
-		'ref_b32passwdgen_3': 'F6C1 CDFB 97D9 FCAE',
-		'ref_hexpasswdgen_3': 'BD4F A0AC 8628 4BE4',
-		'ref_hexpasswdgen_half_3': 'FBDD F733 FFB9 21C1',
-		'ref_bip39_12_passwdgen_3': 'A86E EA14 974A 1B0E',
-		'ref_bip39_18_passwdgen_3': 'EF87 9904 88E2 5884',
-		'ref_bip39_24_passwdgen_3': 'EBE8 2A8F 8F8C 7DBD',
-		'ref_hex2bip39_24_passwdgen_3': '93FA 5EFD 33F3 760E',
-		'ref_xmrseed_25_passwdgen_3': '91AE E76A 2827 C8CC',
 	}
 
 	cmd_group = (
-		('ref_walletgen_brain',       ([],'generating new reference wallet + filename check (brain)')),
-		('refaddrgen_legacy',         ([],'new refwallet addr chksum (uncompressed)')),
-		('refaddrgen_compressed',     ([],'new refwallet addr chksum (compressed)')),
-		('refaddrgen_segwit',         ([],'new refwallet addr chksum (segwit)')),
-		('refaddrgen_bech32',         ([],'new refwallet addr chksum (bech32)')),
-		('refkeyaddrgen_legacy',      ([],'new refwallet key-addr chksum (uncompressed)')),
-		('refkeyaddrgen_compressed',  ([],'new refwallet key-addr chksum (compressed)')),
-		('refkeyaddrgen_segwit',      ([],'new refwallet key-addr chksum (segwit)')),
-		('refkeyaddrgen_bech32',      ([],'new refwallet key-addr chksum (bech32)')),
-		('refpasswdgen',              ([],'new refwallet passwd file chksum')),
-		('refpasswdgen_half',         ([],'new refwallet passwd file chksum (half-length)')),
-		('ref_b32passwdgen',          ([],'new refwallet passwd file chksum (base32)')),
-		('ref_hexpasswdgen',          ([],'new refwallet passwd file chksum (hex)')),
-		('ref_hexpasswdgen_half',     ([],'new refwallet passwd file chksum (hex, half-length)')),
-		('ref_bip39_12_passwdgen',    ([],'new refwallet passwd file chksum (BIP39, 12 words)')),
-		('ref_bip39_18_passwdgen',    ([],'new refwallet passwd file chksum (BIP39, up to 18 words)')),
-		('ref_bip39_24_passwdgen',    ([],'new refwallet passwd file chksum (BIP39, up to 24 words)')),
-		('ref_xmrseed_25_passwdgen',  ([],'new refwallet passwd file chksum (Monero 25-word mnemonic)')),
-		('ref_hex2bip39_24_passwdgen',([],'new refwallet passwd file chksum (hex-to-BIP39, up to 24 words)')),
+		('ref_walletgen_brain',       ([], 'generating new reference wallet + filename check (brain)')),
+		('refaddrgen_legacy',         ([], 'new refwallet addr chksum (uncompressed)')),
+		('refaddrgen_compressed',     ([], 'new refwallet addr chksum (compressed)')),
+		('refaddrgen_segwit',         ([], 'new refwallet addr chksum (segwit)')),
+		('refaddrgen_bech32',         ([], 'new refwallet addr chksum (bech32)')),
+		('refkeyaddrgen_legacy',      ([], 'new refwallet key-addr chksum (uncompressed)')),
+		('refkeyaddrgen_compressed',  ([], 'new refwallet key-addr chksum (compressed)')),
+		('refkeyaddrgen_segwit',      ([], 'new refwallet key-addr chksum (segwit)')),
+		('refkeyaddrgen_bech32',      ([], 'new refwallet key-addr chksum (bech32)')),
 	)
 
 	def call_addrgen(self, mmtype, name='addrgen'):
@@ -388,29 +350,87 @@ class CmdTestRef3Addr(CmdTestRef3Seed):
 
 	def refaddrgen_legacy(self):
 		return self.call_addrgen('legacy')
+
 	def refaddrgen_compressed(self):
 		return self.call_addrgen('compressed')
+
 	def refaddrgen_segwit(self):
-		if cfg.coin == 'BCH':
-			return 'skip'
-		return self.call_addrgen('segwit')
+		if self.proto.cap('segwit'):
+			return self.call_addrgen('segwit')
+		return 'skip'
+
 	def refaddrgen_bech32(self):
-		if cfg.coin == 'BCH':
-			return 'skip'
-		return self.call_addrgen('bech32')
+		if self.proto.cap('segwit'):
+			return self.call_addrgen('bech32')
+		return 'skip'
 
 	def refkeyaddrgen_legacy(self):
 		return self.call_addrgen('legacy', 'keyaddrgen')
+
 	def refkeyaddrgen_compressed(self):
 		return self.call_addrgen('compressed', 'keyaddrgen')
+
 	def refkeyaddrgen_segwit(self):
-		if cfg.coin == 'BCH':
-			return 'skip'
-		return self.call_addrgen('segwit', 'keyaddrgen')
+		if self.proto.cap('segwit'):
+			return self.call_addrgen('segwit', 'keyaddrgen')
+		return 'skip'
+
 	def refkeyaddrgen_bech32(self):
-		if cfg.coin == 'BCH':
-			return 'skip'
-		return self.call_addrgen('bech32', 'keyaddrgen')
+		if self.proto.cap('segwit'):
+			return self.call_addrgen('bech32', 'keyaddrgen')
+		return 'skip'
+
+class CmdTestRef3Passwd(CmdTestRef3Seed):
+	'generated reference password files for 128-, 192- and 256-bit seeds'
+	tmpdir_nums = [26, 27, 28]
+	shared_deps = ['mmdat', pwfile]
+
+	chk_data = {
+		'lens': (128, 192, 256),
+		'sids': ('FE3C6545', '1378FC64', '98831F3A'),
+		'refpasswdgen_1':               'EB29 DC4F 924B 289F',
+		'refpasswdgen_half_1':          'D310 2593 B5D9 2E88',
+		'ref_b32passwdgen_1':           '37B6 C218 2ABC 7508',
+		'ref_hexpasswdgen_1':           '8E99 E696 84CE E7D5',
+		'ref_hexpasswdgen_half_1':      '8E99 E696 84CE E7D5',
+		'ref_bip39_12_passwdgen_1':     '834F CF45 0B33 8AF0',
+		'ref_bip39_18_passwdgen_1':     '834F CF45 0B33 8AF0',
+		'ref_bip39_24_passwdgen_1':     '834F CF45 0B33 8AF0',
+		'ref_hex2bip39_24_passwdgen_1': '91AF E735 A31D 72A0',
+		'refpasswdgen_2':               'ADEA 0083 094D 489A',
+		'refpasswdgen_half_2':          '12B3 4929 9506 76E0',
+		'ref_b32passwdgen_2':           '2A28 C5C7 36EC 217A',
+		'ref_hexpasswdgen_2':           '88F9 0D48 3A7E 7CC2',
+		'ref_hexpasswdgen_half_2':      '59F3 8F48 861E 1186',
+		'ref_bip39_12_passwdgen_2':     'D32D B8D7 A840 250B',
+		'ref_bip39_18_passwdgen_2':     '0FAA 78DD A6BA 31AD',
+		'ref_bip39_24_passwdgen_2':     '0FAA 78DD A6BA 31AD',
+		'ref_hex2bip39_24_passwdgen_2': '0E8E 23C9 923F 7C2D',
+		'refpasswdgen_3':               '2D6D 8FBA 422E 1315',
+		'refpasswdgen_half_3':          '272C B770 0176 D7EA',
+		'ref_b32passwdgen_3':           'F6C1 CDFB 97D9 FCAE',
+		'ref_hexpasswdgen_3':           'BD4F A0AC 8628 4BE4',
+		'ref_hexpasswdgen_half_3':      'FBDD F733 FFB9 21C1',
+		'ref_bip39_12_passwdgen_3':     'A86E EA14 974A 1B0E',
+		'ref_bip39_18_passwdgen_3':     'EF87 9904 88E2 5884',
+		'ref_bip39_24_passwdgen_3':     'EBE8 2A8F 8F8C 7DBD',
+		'ref_hex2bip39_24_passwdgen_3': '93FA 5EFD 33F3 760E',
+		'ref_xmrseed_25_passwdgen_3':   '91AE E76A 2827 C8CC',
+	}
+
+	cmd_group = (
+		('ref_walletgen_brain',        ([], 'generating new reference wallet + filename check (brain)')),
+		('refpasswdgen',               ([], 'new refwallet passwd file chksum')),
+		('refpasswdgen_half',          ([], 'new refwallet passwd file chksum (half-length)')),
+		('ref_b32passwdgen',           ([], 'new refwallet passwd file chksum (base32)')),
+		('ref_hexpasswdgen',           ([], 'new refwallet passwd file chksum (hex)')),
+		('ref_hexpasswdgen_half',      ([], 'new refwallet passwd file chksum (hex, half-length)')),
+		('ref_bip39_12_passwdgen',     ([], 'new refwallet passwd file chksum (BIP39, 12 words)')),
+		('ref_bip39_18_passwdgen',     ([], 'new refwallet passwd file chksum (BIP39, up to 18 words)')),
+		('ref_bip39_24_passwdgen',     ([], 'new refwallet passwd file chksum (BIP39, up to 24 words)')),
+		('ref_xmrseed_25_passwdgen',   ([], 'new refwallet passwd file chksum (Monero 25-word mnemonic)')),
+		('ref_hex2bip39_24_passwdgen', ([], 'new refwallet passwd file chksum (hex-to-BIP39, up to 24 words)')),
+	)
 
 	def pwgen(self, ftype, id_str, pwfmt=None, pwlen=None, extra_opts=[], stdout=False):
 		wf = self.get_file_with_ext('mmdat')
@@ -422,18 +442,21 @@ class CmdTestRef3Addr(CmdTestRef3Seed):
 			ftype      = ftype,
 			id_str     = id_str,
 			extra_opts = pwfmt + pwlen + extra_opts,
-			stdout     = stdout)
+			stdout     = stdout,
+			no_passthru_opts = True)
 
 	def refpasswdgen(self):
 		return self.pwgen('pass','alice@crypto.org')
+
 	def refpasswdgen_half(self):
-		return self.pwgen('pass','alice@crypto.org',pwlen='h')
+		return self.pwgen('pass', 'alice@crypto.org', pwlen='h')
+
 	def ref_b32passwdgen(self):
-		return self.pwgen('pass32','фубар@crypto.org','b32',17)
+		return self.pwgen('pass32', 'фубар@crypto.org', 'b32', 17)
 
 	def ref_hexpasswdgen(self):
-		pwlen = {'1':32,'2':48,'3':64}[self.test_name[-1]]
-		return self.pwgen('passhex','фубар@crypto.org','hex',pwlen)
+		pwlen = {'1':32, '2':48, '3':64}[self.test_name[-1]]
+		return self.pwgen('passhex', 'фубар@crypto.org', 'hex', pwlen)
 
 	def ref_hexpasswdgen_half(self):
 		return self.pwgen('passhex', 'фубар@crypto.org', 'hex', 'h', ['--accept-defaults'], stdout=True)
@@ -448,12 +471,16 @@ class CmdTestRef3Addr(CmdTestRef3Seed):
 		return self.pwgen(ftype, 'фубар@crypto.org', pwfmt, pwlen, ['--accept-defaults'])
 
 	def ref_bip39_12_passwdgen(self):
-		return self.mn_pwgen(12,'bip39')
+		return self.mn_pwgen(12, 'bip39')
+
 	def ref_bip39_18_passwdgen(self):
-		return self.mn_pwgen(18,'bip39')
+		return self.mn_pwgen(18, 'bip39')
+
 	def ref_bip39_24_passwdgen(self):
-		return self.mn_pwgen(24,'bip39')
+		return self.mn_pwgen(24, 'bip39')
+
 	def ref_hex2bip39_24_passwdgen(self):
-		return self.mn_pwgen(24,'hex2bip39')
+		return self.mn_pwgen(24, 'hex2bip39')
+
 	def ref_xmrseed_25_passwdgen(self):
-		return self.mn_pwgen(24,'xmrseed',ftype='passxmrseed')
+		return self.mn_pwgen(24, 'xmrseed', ftype='passxmrseed')

+ 19 - 14
test/cmdtest_py_d/ct_regtest.py

@@ -172,7 +172,6 @@ class CmdTestRegtest(CmdTestBase,CmdTestShared):
 	'transacting and tracking wallet operations via regtest mode'
 	networks = ('btc','ltc','bch')
 	passthru_opts = ('coin','rpc_backend')
-	extra_spawn_args = ['--regtest=1']
 	tmpdir_nums = [17]
 	color = True
 	deterministic = False
@@ -531,8 +530,10 @@ class CmdTestRegtest(CmdTestBase,CmdTestShared):
 			'mmgen-regtest',
 			(['--bdb-wallet'] if self.use_bdb_wallet else [])
 			+ ['--setup-no-stop-daemon', 'setup'])
-		for s in ('Starting','Creating','Creating','Creating','Mined','Setup complete'):
-			t.expect(s)
+		t.expect('Starting')
+		for _ in range(3): t.expect('Creating')
+		for _ in range(5): t.expect('Mined')
+		t.expect('Setup complete')
 		return t
 
 	def daemon_version(self):
@@ -546,10 +547,10 @@ class CmdTestRegtest(CmdTestBase,CmdTestShared):
 		return t
 
 	def walletgen(self,user):
-		t = self.spawn('mmgen-walletgen',['-q','-r0','-p1','--'+user])
-		t.passphrase_new('new '+dfl_wcls.desc,rt_pw)
+		t = self.spawn('mmgen-walletgen', ['-q', '-r0', '-p1', f'--{user}'], no_passthru_opts=True)
+		t.passphrase_new(f'new {dfl_wcls.desc}', rt_pw)
 		t.label()
-		t.expect('move it to the data directory? (Y/n): ','y')
+		t.expect('move it to the data directory? (Y/n): ', 'y')
 		t.written_to_file(capfirst(dfl_wcls.desc))
 		return t
 
@@ -559,7 +560,7 @@ class CmdTestRegtest(CmdTestBase,CmdTestShared):
 		return self.walletgen('alice')
 
 	def _user_dir(self, user, coin=None):
-		return joinpath(self.tr.data_dir, 'regtest', coin or self.coin, user)
+		return joinpath(self.tr.data_dir, 'regtest', user)
 
 	def _user_sid(self,user):
 		if user in self.user_sids:
@@ -586,11 +587,11 @@ class CmdTestRegtest(CmdTestBase,CmdTestShared):
 		for mmtype in mmtypes or self.proto.mmtypes:
 			t = self.spawn(
 				'mmgen-addrgen',
-				['--quiet','--'+user,'--type='+mmtype,f'--outdir={self._user_dir(user)}'] +
-				([wf] if wf else []) +
-				(['--subwallet='+subseed_idx] if subseed_idx else []) +
-				[addr_range],
-				extra_desc = '({})'.format( MMGenAddrType.mmtypes[mmtype].name ))
+				['--quiet', f'--{user}', f'--type={mmtype}', f'--outdir={self._user_dir(user)}']
+				+ ([wf] if wf else [])
+				+ ([f'--subwallet={subseed_idx}'] if subseed_idx else [])
+				+ [addr_range],
+				extra_desc = '({})'.format(MMGenAddrType.mmtypes[mmtype].name))
 			t.passphrase(dfl_wcls.desc,rt_pw)
 			t.written_to_file('Addresses')
 			ok_msg()
@@ -1858,14 +1859,18 @@ class CmdTestRegtest(CmdTestBase,CmdTestShared):
 
 	def bob_walletconv_words(self):
 		t = self.spawn(
-			'mmgen-walletconv', [ '--bob', f'--outdir={self.tmpdir}', '--out-fmt=words' ] )
+			'mmgen-walletconv',
+			['--bob', f'--outdir={self.tmpdir}', '--out-fmt=words'],
+			no_passthru_opts = True)
 		t.passphrase(dfl_wcls.desc,rt_pw)
 		t.written_to_file('data')
 		return t
 
 	def bob_subwalletgen_bip39(self):
 		t = self.spawn(
-			'mmgen-subwalletgen', [ '--bob', f'--outdir={self.tmpdir}', '--out-fmt=bip39', '29L' ] )
+			'mmgen-subwalletgen',
+			['--bob', f'--outdir={self.tmpdir}', '--out-fmt=bip39', '29L'],
+			no_passthru_opts = True)
 		t.passphrase(dfl_wcls.desc,rt_pw)
 		t.written_to_file('data')
 		return t

+ 9 - 4
test/cmdtest_py_d/ct_shared.py

@@ -230,11 +230,14 @@ class CmdTestShared:
 			dfl_wallet = False):
 		hp = self.hash_preset if hasattr(self,'hash_preset') else '1'
 		wcls = wcls or get_wallet_cls(ext=get_extension(wf))
-		t = self.spawn('mmgen-walletchk',
+		t = self.spawn(
+				'mmgen-walletchk',
 				([] if dfl_wallet else ['-i',wcls.fmt_codes[0]])
+				+ self.testnet_opt
 				+ add_args + ['-p',hp]
 				+ ([wf] if wf else []),
-				extra_desc=extra_desc)
+				extra_desc       = extra_desc,
+				no_passthru_opts = True)
 		if wcls.type != 'incog_hidden':
 			t.expect(f"Getting {wcls.desc} from file ‘")
 		if wcls.enc and wcls.type != 'brain':
@@ -254,7 +257,8 @@ class CmdTestShared:
 			extra_opts = [],
 			mmtype     = None,
 			stdout     = False,
-			dfl_wallet = False):
+			dfl_wallet = False,
+			no_passthru_opts = False):
 		list_type = ftype[:4]
 		passgen = list_type == 'pass'
 		if not mmtype and not passgen:
@@ -267,7 +271,8 @@ class CmdTestShared:
 				([],[wf])[bool(wf)] +
 				([],[id_str])[bool(id_str)] +
 				[getattr(self,f'{list_type}_idx_list')],
-				extra_desc=f'({mmtype})' if mmtype in ('segwit','bech32') else '')
+				extra_desc       = f'({mmtype})' if mmtype in ('segwit','bech32') else '',
+				no_passthru_opts = no_passthru_opts)
 		t.license()
 		wcls = get_wallet_cls( ext = 'mmdat' if dfl_wallet else get_extension(wf) )
 		t.passphrase(wcls.desc,self.wpasswd)

+ 2 - 2
test/gentest.py

@@ -41,7 +41,7 @@ opts_data = {
 		'usage':'[options] <spec> <rounds | dump file>',
 		'options': """
 -h, --help         Print this help message
---, --longhelp     Print help message for long options (common options)
+--, --longhelp     Print help message for long (global) options
 -a, --all-coins    Test all coins supported by specified external tool
 -k, --use-internal-keccak-module Force use of the internal keccak module
 -q, --quiet        Produce quieter output
@@ -479,7 +479,7 @@ def get_protos(proto,addr_type,toolname):
 def parse_args():
 
 	if len(cfg._args) != 2:
-		cfg._opts.usage()
+		cfg._usage()
 
 	arg1,arg2 = cfg._args
 	gen1,gen2,rounds = (0,0,0)

+ 2 - 2
test/include/coin_daemon_control.py

@@ -36,7 +36,7 @@ opts_data = {
 		'usage':'[opts] <network IDs>',
 		'options': """
 -h, --help           Print this help message
---, --longhelp       Print help message for long options (common options)
+--, --longhelp       Print help message for long (global) options
 -D, --debug          Produce debugging output (implies --verbose)
 -d, --datadir=       Override the default datadir
 -i, --daemon-ids     Print all known daemon IDs
@@ -140,7 +140,7 @@ def main():
 		ids = cfg._args
 		network_ids = CoinDaemon.get_network_ids(cfg)
 		if not ids:
-			cfg._opts.usage()
+			cfg._usage()
 		for i in ids:
 			if i not in network_ids + list(xmr_wallet_network_ids):
 				die(1,f'{i!r}: invalid network ID')

+ 14 - 0
test/include/common.py

@@ -97,6 +97,20 @@ sample_mn = {
 ref_kafile_pass = 'kafile password'
 ref_kafile_hash_preset = '1'
 
+proto_cmds = (
+	'addrimport',
+	'autosign',
+	'msg',
+	'regtest',
+	'tool',
+	'txbump',
+	'txcreate',
+	'txdo',
+	'txsend',
+	'txsign',
+	'xmrwallet',
+)
+
 def getrand(n):
 	if cfg.test_suite_deterministic:
 		from mmgen.test import fake_urandom

+ 15 - 1
test/misc/opts_main.py

@@ -10,7 +10,7 @@ opts_data = {
 		'usage':'[args] [opts]',
 		'options': """
 -h, --help            Print this help message
---, --longhelp        Print help message for long options (common options)
+--, --longhelp        Print help message for long (global) options
 -i, --in-fmt=      f  Input is from wallet format 'f'
 -d, --outdir=      d  Use outdir 'd'
 -C, --print-checksum  Print a checksum
@@ -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}')

+ 1 - 1
test/objattrtest.py

@@ -43,7 +43,7 @@ opts_data = {
 		'usage':'[options] [object]',
 		'options': """
 -h, --help                  Print this help message
---, --longhelp              Print help message for long options (common options)
+--, --longhelp              Print help message for long (global) options
 -d, --show-descriptor-type  Display the attribute's descriptor type
 -v, --verbose               Produce more verbose output
 """

+ 1 - 1
test/objtest.py

@@ -46,7 +46,7 @@ opts_data = {
 		'usage':'[options] [object]',
 		'options': """
 -h, --help         Print this help message
---, --longhelp     Print help message for long options (common options)
+--, --longhelp     Print help message for long (global) options
 -g, --getobj       Instantiate objects with get_obj() wrapper
 -q, --quiet        Produce quieter output
 -s, --silent       Silence output of tested objects

+ 1 - 1
test/scrambletest.py

@@ -42,7 +42,7 @@ opts_data = {
 		'usage':'[options] [command]',
 		'options': """
 -h, --help          Print this help message
---, --longhelp      Print help message for long options (common options)
+--, --longhelp      Print help message for long (global) options
 -a, --no-altcoin    Skip altcoin tests
 -C, --coverage      Produce code coverage info using trace module
 -l, --list-cmds     List and describe the tests and commands in this test suite

+ 28 - 18
test/test-release.d/cfg.sh

@@ -8,7 +8,7 @@
 #   https://github.com/mmgen/mmgen-wallet
 #   https://gitlab.com/mmgen/mmgen-wallet
 
-all_tests="dep dev lint obj color unit hash ref altref altgen xmr eth autosign btc btc_tn btc_rt bch bch_tn bch_rt ltc ltc_tn ltc_rt tool tool2 gen alt"
+all_tests="dep dev lint obj color unit hash ref altref altgen xmr eth autosign btc btc_tn btc_rt bch bch_tn bch_rt ltc ltc_tn ltc_rt tool tool2 gen alt help"
 
 groups_desc="
 	default  - All tests minus the extra tests
@@ -19,10 +19,10 @@ groups_desc="
 "
 
 init_groups() {
-	dfl_tests='dep alt obj color unit hash ref tool tool2 gen autosign btc btc_tn btc_rt altref altgen bch bch_rt ltc ltc_rt eth etc xmr'
+	dfl_tests='dep alt obj color unit hash ref tool tool2 gen help autosign btc btc_tn btc_rt altref altgen bch bch_rt ltc ltc_rt eth etc xmr'
 	extra_tests='dep dev lint autosign_live ltc_tn bch_tn'
-	noalt_tests='dep alt obj color unit hash ref tool tool2 gen autosign btc btc_tn btc_rt'
-	quick_tests='dep alt obj color unit hash ref tool tool2 gen autosign btc btc_rt altref altgen eth etc xmr'
+	noalt_tests='dep alt obj color unit hash ref tool tool2 gen help autosign btc btc_tn btc_rt'
+	quick_tests='dep alt obj color unit hash ref tool tool2 gen help autosign btc btc_rt altref altgen eth etc xmr'
 	qskip_tests='lint btc_tn bch bch_rt ltc ltc_rt'
 	noalt_ok_tests='lint'
 
@@ -141,23 +141,21 @@ init_tests() {
 		z #   zcash-mini
 		z $gentest_py --coin=zec --type=zcash_z all:zcash-mini $rounds50x
 	"
-
 	[ "$MSYS2" ] && t_altgen_skip='z'    # no zcash-mini (golang)
 	[ "$ARM32" ] && t_altgen_skip='z e'
 	[ "$FAST" ]  && t_altgen_skip+=' M'
 	# ARM ethkey available only on Arch Linux:
 	[ \( "$ARM32" -o "$ARM64" \) -a "$DISTRO" != 'archarm' ] && t_altgen_skip+=' e'
 
-	d_xmr="Monero xmrwallet operations"
-	t_xmr="
-		- $HTTP_LONG_TIMEOUT$cmdtest_py$PEXPECT_LONG_TIMEOUT --coin=xmr
+	d_help="helpscreens for selected coins"
+	t_help="
+		- $cmdtest_py --coin=btc help
+		a $cmdtest_py --coin=bch help
+		a $cmdtest_py --coin=eth help
+		a $cmdtest_py --coin=xmr help
+		a $cmdtest_py --coin=doge help:helpscreens help:longhelpscreens
 	"
-
-	d_eth="operations for Ethereum using devnet"
-	t_eth="geth $cmdtest_py --coin=eth ethdev"
-
-	d_etc="operations for Ethereum Classic using devnet"
-	t_etc="parity $cmdtest_py --coin=etc ethdev"
+	[ "$SKIP_ALT_DEP" ] && t_help_skip='a'
 
 	d_autosign="transaction autosigning with automount"
 	t_autosign="
@@ -176,7 +174,7 @@ init_tests() {
 
 	d_btc="overall operations with emulated RPC data (Bitcoin)"
 	t_btc="
-		- $cmdtest_py --exclude regtest,autosign,autosign_clean,autosign_automount,ref_altcoin
+		- $cmdtest_py --exclude regtest,autosign,autosign_clean,autosign_automount,ref_altcoin,help
 		- $cmdtest_py --segwit
 		- $cmdtest_py --segwit-random
 		- $cmdtest_py --bech32
@@ -193,12 +191,13 @@ init_tests() {
 	d_btc_rt="overall operations using the regtest network (Bitcoin)"
 	t_btc_rt="
 		- $cmdtest_py regtest
-		- $cmdtest_py regtest_legacy
+		x $cmdtest_py regtest_legacy
 	"
+	[ "$FAST" ]  && t_btc_skip='x'
 
 	d_bch="overall operations with emulated RPC data (Bitcoin Cash Node)"
 	t_bch="
-		- $cmdtest_py --coin=bch --exclude regtest
+		- $cmdtest_py --coin=bch --exclude regtest,autosign_automount,help
 		- $cmdtest_py --coin=bch --cashaddr=0 ref3_addr
 	"
 
@@ -213,7 +212,7 @@ init_tests() {
 
 	d_ltc="overall operations with emulated RPC data (Litecoin)"
 	t_ltc="
-		- $cmdtest_py --coin=ltc --exclude regtest
+		- $cmdtest_py --coin=ltc --exclude regtest,autosign_automount,help
 		- $cmdtest_py --coin=ltc --segwit
 		- $cmdtest_py --coin=ltc --segwit-random
 		- $cmdtest_py --coin=ltc --bech32
@@ -230,6 +229,17 @@ init_tests() {
 	d_ltc_rt="overall operations using the regtest network (Litecoin)"
 	t_ltc_rt="- $cmdtest_py --coin=ltc regtest"
 
+	d_eth="operations for Ethereum using devnet"
+	t_eth="geth $cmdtest_py --coin=eth ethdev"
+
+	d_etc="operations for Ethereum Classic using devnet"
+	t_etc="parity $cmdtest_py --coin=etc ethdev"
+
+	d_xmr="Monero xmrwallet operations"
+	t_xmr="
+		- $HTTP_LONG_TIMEOUT$cmdtest_py$PEXPECT_LONG_TIMEOUT --coin=xmr --exclude help
+	"
+
 	d_tool2="'mmgen-tool' utility with data check"
 	t_tool2="
 		- $tooltest2_py --tool-api # test the tool_api subsystem

+ 1 - 1
test/tooltest.py

@@ -40,7 +40,7 @@ opts_data = {
 -h, --help          Print this help message
 -C, --coverage      Produce code coverage info using trace module
 -d, --debug         Produce debugging output (stderr from spawned script)
---, --longhelp      Print help message for long options (common options)
+--, --longhelp      Print help message for long (global) options
 -l, --list-cmds     List and describe the tests and commands in this test suite
 -s, --testing-status  List the testing status of all 'mmgen-tool' commands
 -t, --type=t        Specify address type (valid choices: 'zcash_z')

+ 1 - 1
test/tooltest2.py

@@ -51,7 +51,7 @@ opts_data = {
 -A, --tool-api       Test the tool_api subsystem
 -C, --coverage       Produce code coverage info using trace module
 -d, --die-on-missing Abort if no test data found for given command
---, --longhelp       Print help message for long options (common options)
+--, --longhelp       Print help message for long (global) options
 -l, --list-tests     List the test groups in this test suite
 -L, --list-tested-cmds Output the 'mmgen-tool' commands that are tested by this test suite
 -n, --names          Print command names instead of descriptions