4 Commits f8a312e407 ... 037c6bfb6f

Author SHA1 Message Date
  The MMGen Project 037c6bfb6f opts, help: contextual command options 10 months ago
  The MMGen Project 4eb7c64560 opts, help: contextual usage screens 10 months ago
  The MMGen Project 0ec7bca054 opts, help: minor fixes & cleanups 10 months ago
  The MMGen Project 42e7671898 opts, help: variable renames 10 months ago
10 changed files with 323 additions and 239 deletions
  1. 4 2
      mmgen/cfg.py
  2. 1 1
      mmgen/data/version
  3. 67 42
      mmgen/help/__init__.py
  4. 22 1
      mmgen/help/help_notes.py
  5. 37 36
      mmgen/main_addrgen.py
  6. 40 36
      mmgen/main_txcreate.py
  7. 62 59
      mmgen/main_txdo.py
  8. 43 41
      mmgen/main_wallet.py
  9. 30 18
      mmgen/opts.py
  10. 17 3
      test/cmdtest_d/ct_help.py

+ 4 - 2
mmgen/cfg.py

@@ -433,7 +433,6 @@ class Config(Lockable):
 			cfg          = None,
 			opts_data    = None,
 			init_opts    = None,
-			opt_filter   = None,
 			parse_only   = False,
 			parsed_opts  = None,
 			need_proto   = True,
@@ -455,7 +454,6 @@ class Config(Lockable):
 				cfg          = self,
 				opts_data    = opts_data,
 				init_opts    = init_opts,
-				opt_filter   = opt_filter,
 				parsed_opts  = parsed_opts,
 				need_proto   = need_proto)
 			self._uopt_src = 'cmdline'
@@ -533,6 +531,10 @@ class Config(Lockable):
 		self.coin = self.coin.upper()
 		self.token = self.token.upper() if self.token else None
 
+		if 'usage' in self._uopts: # requires self.coin
+			import importlib
+			getattr(importlib.import_module(UserOpts.help_pkg), 'usage')(self) # exits
+
 		# self.color is finalized, so initialize color:
 		if self.color: # MMGEN_DISABLE_COLOR sets this to False
 			from .color import init_color

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-15.1.dev12
+15.1.dev13

+ 67 - 42
mmgen/help/__init__.py

@@ -44,6 +44,31 @@ def show_hash_presets(cfg):
 	msg('  N = memory usage (power of two)\n  p = iterations (rounds)')
 	sys.exit(0)
 
+def gen_arg_tuple(cfg, func, text):
+
+	def help_notes(k):
+		import importlib
+		return getattr(importlib.import_module(
+			f'{cfg._opts.help_pkg}.help_notes').help_notes(proto, cfg), k)()
+
+	def help_mod(modname):
+		import importlib
+		return importlib.import_module(
+			f'{cfg._opts.help_pkg}.{modname}').help(proto, cfg)
+
+	from ..protocol import init_proto_from_cfg
+	proto = init_proto_from_cfg(cfg, need_amt=True)
+
+	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 make_usage_str(cfg, caller):
 	indent, col1_w = {
 		'help': (2, len(gc.prog_name) + 1),
@@ -52,7 +77,11 @@ def make_usage_str(cfg, 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}'
+			yield '{a:{w}} {b} {c}'.format(
+				a = ulbl,
+				b = gc.prog_name,
+				c = cfg._usage_code(*gen_arg_tuple(cfg, cfg._usage_code, line)) if cfg._usage_code else line,
+				w = col1_w)
 			ulbl = ''
 	return ('\n' + (' ' * indent)).join(gen())
 
@@ -64,27 +93,6 @@ class Help:
 
 	def make(self, cfg, opts):
 
-		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() + ':', opts.opts_data['text']['desc'].strip())
 			yield make_usage_str(cfg, caller='help')
@@ -93,42 +101,33 @@ class Help:
 			# process code for options
 			opts_text = nl.join(self.gen_text(opts))
 			if 'options' in code:
-				yield code['options'](*tuple(gen_arg_tuple(code['options'], opts_text)))
+				yield code['options'](*gen_arg_tuple(cfg, code['options'], opts_text))
 			else:
 				yield opts_text
 
 			# process code for notes
 			if 'notes' in text:
 				if 'notes' in code:
-					yield from code['notes'](*tuple(gen_arg_tuple(code['notes'], text['notes']))).splitlines()
+					yield from code['notes'](*gen_arg_tuple(cfg, code['notes'], text['notes'])).splitlines()
 				else:
 					yield from text['notes'].splitlines()
 
-		from ..protocol import init_proto_from_cfg
-		proto = init_proto_from_cfg(cfg, need_amt=True)
 		text = getattr(opts, self.data_desc)['text']
-		code = getattr(opts, self.data_desc)['code']
+		code = getattr(opts, self.data_desc).get('code', {})
 		nl = '\n  '
 
 		return nl.join(gen_output()) + '\n'
 
-class CmdHelp(Help):
+class CmdHelp_v1(Help):
 
 	help_type = 'options'
 	data_desc = 'opts_data'
 
 	def gen_text(self, opts):
-		opt_filter = opts.opt_filter
-		from ..opts import cmd_opts_pat
+		from ..opts import cmd_opts_v1_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
+			if m := cmd_opts_v1_pat.match(line):
 				yield '{} --{} {}'.format(
 					(f'-{m[1]},', '   ')[m[1] == '-'],
 					m[2],
@@ -136,6 +135,28 @@ class CmdHelp(Help):
 			elif not skipping:
 				yield line
 
+class CmdHelp_v2(CmdHelp_v1):
+
+	def gen_text(self, opts):
+		from ..opts import cmd_opts_v2_help_pat
+		skipping = False
+		coin_filter_codes = opts.global_filter_codes.coin
+		cmd_filter_codes = opts.opts_data['filter_codes']
+		for line in opts.opts_data['text']['options'][1:].rstrip().splitlines():
+			m = cmd_opts_v2_help_pat.match(line)
+			if m[1] == '+':
+				if not skipping:
+					yield line[6:]
+			elif m[1] in coin_filter_codes and m[2] in cmd_filter_codes:
+				yield '{} --{} {}'.format(
+					(f'-{m[3]},', '   ')[m[3] == '-'],
+					m[4],
+					m[6]
+				) if m[4] else m[6]
+				skipping = False
+			else:
+				skipping = True
+
 class GlobalHelp(Help):
 
 	help_type = 'global_options'
@@ -144,12 +165,14 @@ class GlobalHelp(Help):
 	def gen_text(self, opts):
 		from ..opts import global_opts_help_pat
 		skipping = False
-		for line in opts.global_opts_data['text']['options'][1:-3].splitlines():
+		coin_filter_codes = opts.global_filter_codes.coin
+		cmd_filter_codes = opts.global_filter_codes.cmd
+		for line in opts.global_opts_data['text']['options'][1:].rstrip().splitlines():
 			m = global_opts_help_pat.match(line)
 			if m[1] == '+':
 				if not skipping:
 					yield line[4:]
-			elif m[1] in opts.global_opts_filter.coin and m[2] in opts.global_opts_filter.cmd:
+			elif m[1] in coin_filter_codes and m[2] in cmd_filter_codes:
 				yield '  --{} {}'.format(m[3], m[5]) if m[3] else m[5]
 				skipping = False
 			else:
@@ -157,9 +180,11 @@ class GlobalHelp(Help):
 
 def print_help(cfg, opts):
 
-	if not 'code' in opts.opts_data:
-		opts.opts_data['code'] = {}
+	if cfg.help:
+		help_cls = CmdHelp_v2 if 'filter_codes' in opts.opts_data else CmdHelp_v1
+	else:
+		help_cls = GlobalHelp
 
 	from ..ui import do_pager
-	do_pager((CmdHelp if cfg.help else GlobalHelp)().make(cfg, opts))
+	do_pager(help_cls().make(cfg, opts))
 	sys.exit(0)

+ 22 - 1
mmgen/help/help_notes.py

@@ -20,6 +20,14 @@ class help_notes:
 		self.proto = proto
 		self.cfg = cfg
 
+	def txcreate_args(self):
+		return (
+			'<addr,amt>' if self.proto.base_coin == 'ETH' else
+			'[<addr,amt> ...] <change addr, addrlist ID or addr type>')
+
+	def account_info_desc(self):
+		return 'account info' if self.proto.base_coin == 'ETH' else 'unspent outputs'
+
 	def fee_spec_letters(self, use_quotes=False):
 		cu = self.proto.coin_amt.units
 		sep, conj = ((',', ' or '), ("','", "' or '"))[use_quotes]
@@ -139,7 +147,20 @@ seed, the same seed length and hash preset parameters must always be used.
 		addr = t.privhex2addr('bead' * 16)
 		sample_addr = addr.views[addr.view_pref]
 
-		return f"""
+		if self.proto.base_coin == 'ETH':
+			return f"""
+EXAMPLES:
+
+  Send 0.123 {self.proto.coin} to an external {self.proto.name} address:
+
+    $ {gc.prog_name} {sample_addr},0.123
+
+  Send 0.123 {self.proto.coin} to another account in wallet 01ABCDEF:
+
+    $ {gc.prog_name} 01ABCDEF:{mmtype}:7,0.123
+"""
+		else:
+			return f"""
 EXAMPLES:
 
   Send 0.123 {self.proto.coin} to an external {self.proto.name} address, returning the change to a

+ 37 - 36
mmgen/main_addrgen.py

@@ -30,16 +30,17 @@ if gc.prog_name == 'mmgen-keygen':
 	gen_what = 'keys'
 	gen_clsname = 'KeyAddrList'
 	gen_desc = 'secret keys'
-	opt_filter = None
+	filter_codes = ['-', 'k']
 	note_addrkey = 'By default, both addresses and secret keys are generated.\n\n'
 else:
 	gen_what = 'addresses'
 	gen_clsname = 'AddrList'
 	gen_desc = 'addresses'
-	opt_filter = 'hbcdeEiHOkKlpzPqrStUv-'
+	filter_codes = ['-']
 	note_addrkey = ''
 
 opts_data = {
+	'filter_codes': filter_codes,
 	'sets': [('print_checksum', True, 'quiet', True)],
 	'text': {
 		'desc': f"""
@@ -48,39 +49,39 @@ opts_data = {
 			  """,
 		'usage':'[opts] [seed source] <index list or range(s)>',
 		'options': """
--h, --help            Print this help message
---, --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
--e, --echo-passphrase Echo passphrase or mnemonic to screen upon entry
--i, --in-fmt=      f  Input is from wallet format 'f' (see FMT CODES below)
--H, --hidden-incog-input-params=f,o  Read hidden incognito data from file
-                      'f' at offset 'o' (comma-separated)
--O, --old-incog-fmt   Specify old-format incognito input
--k, --use-internal-keccak-module Force use of the internal keccak module
--K, --keygen-backend=n Use backend 'n' for public key generation.  Options
-                      for {coin_id}: {kgs}
--l, --seed-len=    l  Specify wallet seed length of 'l' bits.  This option
-                      is required only for brainwallet and incognito inputs
-                      with non-standard (< {dsl}-bit) seed lengths.
--p, --hash-preset= p  Use the scrypt hash parameters defined by preset 'p'
-                      for password hashing (default: '{gc.dfl_hash_preset}')
--z, --show-hash-presets Show information on available hash presets
--P, --passwd-file= f  Get wallet passphrase from file 'f'
--q, --quiet           Produce quieter output; suppress some warnings
--r, --usr-randchars=n Get 'n' characters of additional randomness from user
-                      (min={cfg.min_urandchars}, max={cfg.max_urandchars}, default={cfg.usr_randchars})
--S, --stdout          Print {what} to stdout
--t, --type=t          Choose address type. Options: see ADDRESS TYPES below
-                      (default: {dmat})
--U, --subwallet=   U  Generate {what} for subwallet 'U' (see SUBWALLETS
-                      below)
--V, --viewkeys        Print viewkeys, omitting secret keys
--v, --verbose         Produce more verbose output
--x, --b16             Print secret keys in hexadecimal too
-""",
-	'notes': """
+			-- -h, --help            Print this help message
+			-- --, --longhelp        Print help message for long (global) options
+			-k -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
+			-- -e, --echo-passphrase Echo passphrase or mnemonic to screen upon entry
+			-- -i, --in-fmt=      f  Input is from wallet format 'f' (see FMT CODES below)
+			-- -H, --hidden-incog-input-params=f,o  Read hidden incognito data from file
+			+                        'f' at offset 'o' (comma-separated)
+			-- -O, --old-incog-fmt   Specify old-format incognito input
+			-- -k, --use-internal-keccak-module Force use of the internal keccak module
+			-- -K, --keygen-backend=n Use backend 'n' for public key generation.  Options
+			+                        for {coin_id}: {kgs}
+			-- -l, --seed-len=    l  Specify wallet seed length of 'l' bits.  This option
+			+                        is required only for brainwallet and incognito inputs
+			+                        with non-standard (< {dsl}-bit) seed lengths.
+			-- -p, --hash-preset= p  Use the scrypt hash parameters defined by preset 'p'
+			+                        for password hashing (default: '{gc.dfl_hash_preset}')
+			-- -z, --show-hash-presets Show information on available hash presets
+			-- -P, --passwd-file= f  Get wallet passphrase from file 'f'
+			-- -q, --quiet           Produce quieter output; suppress some warnings
+			-- -r, --usr-randchars=n Get 'n' characters of additional randomness from user
+			+                        (min={cfg.min_urandchars}, max={cfg.max_urandchars}, default={cfg.usr_randchars})
+			-- -S, --stdout          Print {what} to stdout
+			-- -t, --type=t          Choose address type. Options: see ADDRESS TYPES below
+			+                        (default: {dmat})
+			-- -U, --subwallet=   U  Generate {what} for subwallet 'U' (see SUBWALLETS
+			+                        below)
+			-k -V, --viewkeys        Print viewkeys, omitting secret keys
+			-- -v, --verbose         Produce more verbose output
+			-k -x, --b16             Print secret keys in hexadecimal too
+		""",
+		'notes': """
 
                            NOTES FOR THIS COMMAND
 
@@ -124,7 +125,7 @@ FMT CODES:
 	}
 }
 
-cfg = Config(opts_data=opts_data, opt_filter=opt_filter, need_amt=False)
+cfg = Config(opts_data=opts_data, need_amt=False)
 
 proto = cfg._proto
 

+ 40 - 36
mmgen/main_txcreate.py

@@ -25,50 +25,54 @@ from .cfg import gc, Config
 from .util import fmt_list, async_run
 
 opts_data = {
+	'filter_codes': ['-'],
 	'sets': [('yes', True, 'quiet', True)],
 	'text': {
 		'desc': f'Create a transaction with outputs to specified coin or {gc.proj_name} addresses',
-		'usage':   '[opts]  [<addr,amt> ...] <change addr, addrlist ID or addr type> [addr file ...]',
+		'usage':   '[opts] {u_args} [addr file ...]',
 		'options': """
--h, --help            Print this help message
---, --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
--A, --fee-adjust=  f  Adjust transaction fee by factor 'f' (see below)
--B, --no-blank        Don't blank screen before displaying unspent outputs
--c, --comment-file=f  Source the transaction's comment from file 'f'
--C, --fee-estimate-confs=c Desired number of confirmations for fee estimation
-                      (default: {cfg.fee_estimate_confs})
--d, --outdir=      d  Specify an alternate directory 'd' for output
--D, --contract-data=D Path to hex-encoded contract data (ETH only)
--E, --fee-estimate-mode=M Specify the network fee estimate mode.  Choices:
-                      {fe_all}.  Default: {fe_dfl!r}
--f, --fee=         f  Transaction fee, as a decimal {cu} amount or as
-                      {fu} (an integer followed by {fl!r}).
-                      See FEE SPECIFICATION below.  If omitted, fee will be
-                      calculated using network fee estimation.
--g, --gas=         g  Specify start gas amount in Wei (ETH only)
--i, --info            Display unspent outputs and exit
--I, --inputs=      i  Specify transaction inputs (comma-separated list of
-                      MMGen IDs or coin addresses).  Note that ALL unspent
-                      outputs associated with each address will be included.
--l, --locktime=    t  Lock time (block height or unix seconds) (default: 0)
--L, --autochg-ignore-labels Ignore labels when autoselecting change addresses
--m, --minconf=     n  Minimum number of confirmations required to spend
-                      outputs (default: 1)
--q, --quiet           Suppress warnings; overwrite files without prompting
--R, --no-rbf          Make transaction non-replaceable (non-replace-by-fee
-                      according to BIP 125)
--v, --verbose         Produce more verbose output
--V, --vsize-adj=   f  Adjust transaction's estimated vsize by factor 'f'
--y, --yes             Answer 'yes' to prompts, suppress non-essential output
--X, --cached-balances Use cached balances (Ethereum only)
-""",
+			-- -h, --help            Print this help message
+			-- --, --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
+			-- -A, --fee-adjust=  f  Adjust transaction fee by factor 'f' (see below)
+			-- -B, --no-blank        Don't blank screen before displaying {a_info}
+			-- -c, --comment-file=f  Source the transaction's comment from file 'f'
+			b- -C, --fee-estimate-confs=c Desired number of confirmations for fee estimation
+			+                        (default: {cfg.fee_estimate_confs})
+			-- -d, --outdir=      d  Specify an alternate directory 'd' for output
+			e- -D, --contract-data=D Path to file containing hex-encoded contract data
+			b- -E, --fee-estimate-mode=M Specify the network fee estimate mode.  Choices:
+			+                        {fe_all}.  Default: {fe_dfl!r}
+			-- -f, --fee=         f  Transaction fee, as a decimal {cu} amount or as
+			+                        {fu} (an integer followed by {fl!r}).
+			+                        See FEE SPECIFICATION below.  If omitted, fee will be
+			+                        calculated using network fee estimation.
+			e- -g, --gas=         g  Specify start gas amount in Wei
+			-- -i, --info            Display {a_info} and exit
+			-- -I, --inputs=      i  Specify transaction inputs (comma-separated list of
+			+                        MMGen IDs or coin addresses).  Note that ALL unspent
+			+                        outputs associated with each address will be included.
+			b- -l, --locktime=    t  Lock time (block height or unix seconds) (default: 0)
+			b- -L, --autochg-ignore-labels Ignore labels when autoselecting change addresses
+			-- -m, --minconf=     n  Minimum number of confirmations required to spend
+			+                        outputs (default: 1)
+			-- -q, --quiet           Suppress warnings; overwrite files without prompting
+			b- -R, --no-rbf          Make transaction non-replaceable (non-replace-by-fee
+			+                        according to BIP 125)
+			-- -v, --verbose         Produce more verbose output
+			b- -V, --vsize-adj=   f  Adjust transaction's estimated vsize by factor 'f'
+			-- -y, --yes             Answer 'yes' to prompts, suppress non-essential output
+			e- -X, --cached-balances Use cached balances
+		""",
 		'notes': '\n{c}\n{F}\n{x}',
 	},
 	'code': {
+		'usage': lambda cfg, proto, help_notes, s: s.format(
+			u_args = help_notes('txcreate_args')),
 		'options': lambda cfg, proto, help_notes, s: s.format(
+			a_info = help_notes('account_info_desc'),
 			fu     = help_notes('rel_fee_desc'),
 			fl     = help_notes('fee_spec_letters'),
 			fe_all = fmt_list(cfg._autoset_opts['fee_estimate_mode'].choices, fmt='no_spc'),

+ 62 - 59
mmgen/main_txdo.py

@@ -25,69 +25,69 @@ from .util import die, fmt_list, async_run
 from .subseed import SubSeedIdxRange
 
 opts_data = {
+	'filter_codes': ['-'],
 	'sets': [('yes', True, 'quiet', True)],
 	'text': {
 		'desc': f'Create, sign and send an {gc.proj_name} transaction',
-		'usage':   '[opts]  [<addr,amt> ...] <change addr, addrlist ID or addr type> [addr file ...] ' +
-					'[seed source ...]',
+		'usage':   '[opts] {u_args} [addr file ...] [seed source ...]',
 		'options': """
--h, --help             Print this help message
---, --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
--B, --no-blank         Don't blank screen before displaying unspent outputs
--c, --comment-file=  f Source the transaction's comment from file 'f'
--C, --fee-estimate-confs=c Desired number of confirmations for fee estimation
-                       (default: {cfg.fee_estimate_confs})
--d, --outdir=        d Specify an alternate directory 'd' for output
--D, --contract-data= D Path to hex-encoded contract data (ETH only)
--e, --echo-passphrase  Print passphrase to screen when typing it
--E, --fee-estimate-mode=M Specify the network fee estimate mode.  Choices:
-                       {fe_all}.  Default: {fe_dfl!r}
--f, --fee=           f Transaction fee, as a decimal {cu} amount or as
-                       {fu} (an integer followed by {fl!r}).
-                       See FEE SPECIFICATION below.  If omitted, fee will be
-                       calculated using network fee estimation.
--g, --gas=           g Specify start gas amount in Wei (ETH only)
--H, --hidden-incog-input-params=f,o  Read hidden incognito data from file
-                      'f' at offset 'o' (comma-separated)
--i, --in-fmt=        f Input is from wallet format 'f' (see FMT CODES below)
--I, --inputs=        i Specify transaction inputs (comma-separated list of
-                       MMGen IDs or coin addresses).  Note that ALL unspent
-                       outputs associated with each address will be included.
--l, --seed-len=      l Specify wallet seed length of 'l' bits. This option
-                       is required only for brainwallet and incognito inputs
-                       with non-standard (< {dsl}-bit) seed lengths.
--k, --keys-from-file=f Provide additional keys for non-{pnm} addresses
--K, --keygen-backend=n Use backend 'n' for public key generation.  Options
-                       for {coin_id}: {kgs}
--l, --locktime=      t Lock time (block height or unix seconds) (default: 0)
--L, --autochg-ignore-labels Ignore labels when autoselecting change addresses
--m, --minconf=n        Minimum number of confirmations required to spend
-                       outputs (default: 1)
--M, --mmgen-keys-from-file=f Provide keys for {pnm} addresses in a key-
-                       address file (output of '{pnl}-keygen'). Permits
-                       online signing without an {pnm} seed source. The
-                       key-address file is also used to verify {pnm}-to-{cu}
-                       mappings, so the user should record its checksum.
--O, --old-incog-fmt    Specify old-format incognito input
--p, --hash-preset=   p Use the scrypt hash parameters defined by preset 'p'
-                       for password hashing (default: '{gc.dfl_hash_preset}')
--P, --passwd-file=   f Get {pnm} wallet passphrase from file 'f'
--R, --no-rbf           Make transaction non-replaceable (non-replace-by-fee
-                       according to BIP 125)
--q, --quiet            Suppress warnings; overwrite files without prompting
--u, --subseeds=      n The number of subseed pairs to scan for (default: {ss},
-                       maximum: {ss_max}). Only the default or first supplied
-                       wallet is scanned for subseeds.
--v, --verbose          Produce more verbose output
--V, --vsize-adj=     f Adjust transaction's estimated vsize by factor 'f'
--X, --cached-balances  Use cached balances (Ethereum only)
--y, --yes              Answer 'yes' to prompts, suppress non-essential output
--z, --show-hash-presets Show information on available hash presets
-""",
-	'notes': """
+			-- -h, --help             Print this help message
+			-- --, --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
+			-- -B, --no-blank         Don't blank screen before displaying {a_info}
+			-- -c, --comment-file=  f Source the transaction's comment from file 'f'
+			b- -C, --fee-estimate-confs=c Desired number of confirmations for fee estimation
+			+                         (default: {cfg.fee_estimate_confs})
+			-- -d, --outdir=        d Specify an alternate directory 'd' for output
+			e- -D, --contract-data= D Path to file containing hex-encoded contract data
+			-- -e, --echo-passphrase  Print passphrase to screen when typing it
+			b- -E, --fee-estimate-mode=M Specify the network fee estimate mode.  Choices:
+			+                         {fe_all}.  Default: {fe_dfl!r}
+			-- -f, --fee=           f Transaction fee, as a decimal {cu} amount or as
+			+                         {fu} (an integer followed by {fl!r}).
+			+                         See FEE SPECIFICATION below.  If omitted, fee will be
+			+                         calculated using network fee estimation.
+			e- -g, --gas=           g Specify start gas amount in Wei
+			-- -H, --hidden-incog-input-params=f,o  Read hidden incognito data from file
+			+                        'f' at offset 'o' (comma-separated)
+			-- -i, --in-fmt=        f Input is from wallet format 'f' (see FMT CODES below)
+			-- -I, --inputs=        i Specify transaction inputs (comma-separated list of
+			+                         MMGen IDs or coin addresses).  Note that ALL unspent
+			+                         outputs associated with each address will be included.
+			-- -l, --seed-len=      l Specify wallet seed length of 'l' bits. This option
+			+                         is required only for brainwallet and incognito inputs
+			+                         with non-standard (< {dsl}-bit) seed lengths.
+			-- -k, --keys-from-file=f Provide additional keys for non-{pnm} addresses
+			-- -K, --keygen-backend=n Use backend 'n' for public key generation.  Options
+			+                         for {coin_id}: {kgs}
+			b- -l, --locktime=      t Lock time (block height or unix seconds) (default: 0)
+			b- -L, --autochg-ignore-labels Ignore labels when autoselecting change addresses
+			-- -m, --minconf=n        Minimum number of confirmations required to spend
+			+                         outputs (default: 1)
+			-- -M, --mmgen-keys-from-file=f Provide keys for {pnm} addresses in a key-
+			+                         address file (output of '{pnl}-keygen'). Permits
+			+                         online signing without an {pnm} seed source. The
+			+                         key-address file is also used to verify {pnm}-to-{cu}
+			+                         mappings, so the user should record its checksum.
+			-- -O, --old-incog-fmt    Specify old-format incognito input
+			-- -p, --hash-preset=   p Use the scrypt hash parameters defined by preset 'p'
+			+                         for password hashing (default: '{gc.dfl_hash_preset}')
+			-- -P, --passwd-file=   f Get {pnm} wallet passphrase from file 'f'
+			b- -R, --no-rbf           Make transaction non-replaceable (non-replace-by-fee
+			+                         according to BIP 125)
+			-- -q, --quiet            Suppress warnings; overwrite files without prompting
+			-- -u, --subseeds=      n The number of subseed pairs to scan for (default: {ss},
+			+                         maximum: {ss_max}). Only the default or first supplied
+			+                         wallet is scanned for subseeds.
+			-- -v, --verbose          Produce more verbose output
+			b- -V, --vsize-adj=     f Adjust transaction's estimated vsize by factor 'f'
+			e- -X, --cached-balances  Use cached balances
+			-- -y, --yes              Answer 'yes' to prompts, suppress non-essential output
+			-- -z, --show-hash-presets Show information on available hash presets
+		""",
+		'notes': """
 {c}\n{F}
 
                                  SIGNING NOTES
@@ -102,11 +102,14 @@ FMT CODES:
 {x}"""
 	},
 	'code': {
+		'usage': lambda cfg, proto, help_notes, s: s.format(
+			u_args  = help_notes('txcreate_args')),
 		'options': lambda cfg, proto, help_notes, s: s.format(
 			gc      = gc,
 			cfg     = cfg,
 			pnm     = gc.proj_name,
 			pnl     = gc.proj_name.lower(),
+			a_info  = help_notes('account_info_desc'),
 			kgs     = help_notes('keygen_backends'),
 			coin_id = help_notes('coin_id'),
 			fu      = help_notes('rel_fee_desc'),

+ 43 - 41
mmgen/main_wallet.py

@@ -45,78 +45,80 @@ invoked_as = {
 
 dsw = f'the default or specified {gc.proj_name} wallet'
 
-# full: defhHiJkKlLmoOpPqrSvz-
 if invoked_as == 'gen':
 	desc = f'Generate an {gc.proj_name} wallet from a random seed'
-	opt_filter = 'ehdoJlLpPqrSvz-'
 	usage = '[opts]'
 	oaction = 'output'
 	nargs = 0
 elif invoked_as == 'conv':
 	desc = 'Convert ' + dsw + ' from one format to another'
-	opt_filter = 'dehHiJkKlLmNoOpPqrSvz-'
 elif invoked_as == 'chk':
 	desc = 'Check validity of ' + dsw
-	opt_filter = 'ehiHOlpPqrvz-'
 	iaction = 'input'
 elif invoked_as == 'passchg':
 	desc = 'Change the passphrase, hash preset or label of ' + dsw
-	opt_filter = 'efhdiHkKOlLmNpPqrSvz-'
 	iaction = 'input'
 	do_bw_note = False
 elif invoked_as == 'subgen':
 	desc = 'Generate a subwallet from ' + dsw
-	opt_filter = 'dehHiJkKlLmNoOpPqrSvz-' # omitted: f
 	usage = '[opts] [infile] <Subseed Index>'
 	iaction = 'input'
 	oaction = 'output'
 	do_sw_note = True
 elif invoked_as == 'seedsplit':
 	desc = 'Generate a seed share from ' + dsw
-	opt_filter = 'dehHiJlLMNIoOpPqrSvz-'
 	usage = '[opts] [infile] [<Split ID String>:]<index>:<share count>'
 	iaction = 'input'
 	oaction = 'output'
 	do_ss_note = True
 
 opts_data = {
+	'filter_codes': {
+		# Write  In-fmt  Out-fmt  Keep-pass  Force-update  Master-share  passwd-file-New-only
+		'chk':       ['-',      'i'],
+		'conv':      ['-', 'w', 'i', 'o', 'k',      'n'],
+		'gen':       ['-', 'w',      'o'],
+		'passchg':   ['-', 'w', 'i',      'k', 'f', 'n'],
+		'seedsplit': ['-', 'w', 'i', 'o',      'm', 'n'],
+		'subgen':    ['-', 'w', 'i', 'o', 'k',      'n'],
+	}[invoked_as],
 	'text': {
 		'desc': desc,
 		'usage': usage,
 		'options': """
--h, --help            Print this help message
---, --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
--i, --in-fmt=      f  {iaction} from wallet format 'f' (see FMT CODES below)
--o, --out-fmt=     f  {oaction} to wallet format 'f' (see FMT CODES below)
--H, --hidden-incog-input-params=f,o  Read hidden incognito data from file
-                      'f' at offset 'o' (comma-separated)
--J, --hidden-incog-output-params=f,o  Write hidden incognito data to file
-                      'f' at offset 'o' (comma-separated). File 'f' will be
-                      created if necessary and filled with random data.
--O, --old-incog-fmt   Specify old-format incognito input
--k, --keep-passphrase Reuse passphrase of input wallet for output wallet
--K, --keep-hash-preset Reuse hash preset of input wallet for output wallet
--l, --seed-len=    l  Specify wallet seed length of 'l' bits.  This option
-                      is required only for brainwallet and incognito inputs
-                      with non-standard (< {dsl}-bit) seed lengths.
--L, --label=       l  Specify a label 'l' for output wallet
--m, --keep-label      Reuse label of input wallet for output wallet
--M, --master-share=i  Use a master share with index 'i' (min:{ms_min}, max:{ms_max})
--p, --hash-preset= p  Use the scrypt hash parameters defined by preset 'p'
-                      for password hashing (default: '{gc.dfl_hash_preset}')
--z, --show-hash-presets Show information on available hash presets
--P, --passwd-file= f  Get wallet passphrase from file 'f'
--N, --passwd-file-new-only Use passwd file only for new, not existing, wallet
--q, --quiet           Produce quieter output; suppress some warnings
--r, --usr-randchars=n Get 'n' characters of additional randomness from user
-                      (min={cfg.min_urandchars}, max={cfg.max_urandchars}, default={cfg.usr_randchars})
--S, --stdout          Write wallet data to stdout instead of file
--v, --verbose         Produce more verbose output
-""",
-	'notes': """
+			-- -h, --help            Print this help message
+			-- --, --longhelp        Print help message for long (global) options
+			-w -d, --outdir=      d  Output files to directory 'd' instead of working dir
+			-- -e, --echo-passphrase Echo passphrases and other user input to screen
+			-f -f, --force-update    Force update of wallet even if nothing has changed
+			-i -i, --in-fmt=      f  {iaction} from wallet format 'f' (see FMT CODES below)
+			-o -o, --out-fmt=     f  {oaction} to wallet format 'f' (see FMT CODES below)
+			-i -H, --hidden-incog-input-params=f,o  Read hidden incognito data from file
+			+                        'f' at offset 'o' (comma-separated)
+			-o -J, --hidden-incog-output-params=f,o  Write hidden incognito data to file
+			+                        'f' at offset 'o' (comma-separated). File 'f' will be
+			+                        created if necessary and filled with random data.
+			-i -O, --old-incog-fmt   Specify old-format incognito input
+			-k -k, --keep-passphrase Reuse passphrase of input wallet for output wallet
+			-k -K, --keep-hash-preset Reuse hash preset of input wallet for output wallet
+			-- -l, --seed-len=    l  Specify wallet seed length of 'l' bits.  This option
+			+                        is required only for brainwallet and incognito inputs
+			+                        with non-standard (< {dsl}-bit) seed lengths.
+			-w -L, --label=       l  Specify a label 'l' for output wallet
+			-k -m, --keep-label      Reuse label of input wallet for output wallet
+			-m -M, --master-share=i  Use a master share with index 'i' (min:{ms_min}, max:{ms_max})
+			-- -p, --hash-preset= p  Use the scrypt hash parameters defined by preset 'p'
+			+                        for password hashing (default: '{gc.dfl_hash_preset}')
+			-- -z, --show-hash-presets Show information on available hash presets
+			-- -P, --passwd-file= f  Get wallet passphrase from file 'f'
+			-n -N, --passwd-file-new-only Use passwd file only for new, not existing, wallet
+			-- -q, --quiet           Produce quieter output; suppress some warnings
+			-- -r, --usr-randchars=n Get 'n' characters of additional randomness from user
+			+                        (min={cfg.min_urandchars}, max={cfg.max_urandchars}, default={cfg.usr_randchars})
+			-w -S, --stdout          Write wallet data to stdout instead of file
+			-- -v, --verbose         Produce more verbose output
+		""",
+		'notes': """
 
 {n_ss}{n_sw}{n_pw}{n_bw}
 
@@ -145,7 +147,7 @@ FMT CODES:
 	}
 }
 
-cfg = Config(opts_data=opts_data, opt_filter=opt_filter, need_proto=False)
+cfg = Config(opts_data=opts_data, need_proto=False)
 
 cmd_args = cfg._args
 

+ 30 - 18
mmgen/opts.py

@@ -150,28 +150,41 @@ def process_uopts(cfg, opts_data, opts, need_proto):
 
 	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-z0-9-]{2,64})(=| )(.+)')
+cmd_opts_v1_pat      = re.compile(r'^-([a-zA-Z0-9-]), --([a-zA-Z0-9-]{2,64})(=| )(.+)')
+
+cmd_opts_v2_pat      = re.compile(r'^\t\t\t(.)(.) -([a-zA-Z0-9-]), --([a-z0-9-]{2,64})(=| )(.+)')
+cmd_opts_v2_help_pat = re.compile(r'^\t\t\t(.)(.) (?:-([a-zA-Z0-9-]), --([a-z0-9-]{2,64})(=| ))?(.+)')
+
+global_opts_pat      = re.compile(r'^\t\t\t(.)(.) --([a-z0-9-]{2,64})(=| )(.+)')
 global_opts_help_pat = re.compile(r'^\t\t\t(.)(.) (?:--([{}a-zA-Z0-9-]{2,64})(=| ))?(.+)')
+
 opt_tuple = namedtuple('cmdline_option', ['name', 'has_parm'])
 
-def parse_opts(cfg, opts_data, opt_filter, global_opts_data, global_opts_filter, need_proto):
+def parse_opts(cfg, opts_data, global_opts_data, global_filter_codes, need_proto):
 
-	def parse_cmd_opts_text():
+	def parse_v1():
 		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):
+			if m := cmd_opts_v1_pat.match(line):
 				ret = opt_tuple(m[2].replace('-', '_'), m[3] == '=')
 				yield (m[1], ret)
 				yield (m[2], ret)
 
-	def parse_global_opts_text():
+	def parse_v2():
+		cmd_filter_codes = opts_data['filter_codes']
+		for line in opts_data['text']['options'].splitlines():
+			m = cmd_opts_v2_pat.match(line)
+			if m and m[1] in global_filter_codes.coin and m[2] in cmd_filter_codes:
+				ret = opt_tuple(m[4].replace('-', '_'), m[5] == '=')
+				yield (m[3], ret)
+				yield (m[4], ret)
+
+	def parse_global():
 		for line in global_opts_data['text']['options'].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:
+			if m and m[1] in global_filter_codes.coin and m[2] in global_filter_codes.cmd:
 				yield (m[3], opt_tuple(m[3].replace('-', '_'), m[4] == '='))
 
-	opts = tuple(parse_cmd_opts_text()) + tuple(parse_global_opts_text())
+	opts = tuple((parse_v2 if 'filter_codes' in opts_data else parse_v1)()) + tuple(parse_global())
 
 	uopts, uargs = process_uopts(cfg, opts_data, dict(opts), need_proto)
 
@@ -227,7 +240,6 @@ class Opts:
 			cfg,
 			opts_data,
 			init_opts,    # dict containing opts to pre-initialize
-			opt_filter,   # whitelist of opt letters; all others are skipped
 			parsed_opts,
 			need_proto):
 
@@ -235,17 +247,15 @@ class Opts:
 			raise RuntimeError(f'{len(sys.argv) - 1}: too many command-line arguments')
 
 		opts_data = opts_data or opts_data_dfl
-		self.opt_filter = opt_filter
 
-		self.global_opts_filter = self.get_global_opts_filter(need_proto)
+		self.global_filter_codes = self.get_global_filter_codes(need_proto)
 		self.opts_data = opts_data
 
 		po = parsed_opts or parse_opts(
 			cfg,
 			opts_data,
-			opt_filter,
 			self.global_opts_data,
-			self.global_opts_filter,
+			self.global_filter_codes,
 			need_proto)
 
 		cfg._args = po.cmd_args
@@ -260,8 +270,10 @@ class Opts:
 		cfg._parsed_opts = po
 		cfg._use_env = True
 		cfg._use_cfg_file = not 'skip_cfg_file' in uopts
-		# Make this available to usage()
+
+		# Make these available to usage():
 		cfg._usage_data = opts_data['text'].get('usage2') or opts_data['text']['usage']
+		cfg._usage_code = opts_data.get('code', {}).get('usage')
 
 		if os.getenv('MMGEN_DEBUG_OPTS'):
 			opt_preproc_debug(po)
@@ -274,7 +286,7 @@ class Opts:
 class UserOpts(Opts):
 
 	help_pkg = 'mmgen.help'
-	info_funcs = ('usage', 'version', 'show_hash_presets')
+	info_funcs = ('version', 'show_hash_presets')
 
 	global_opts_data = {
 		#  coin code : cmd code : opt : opt param : text
@@ -337,7 +349,7 @@ class UserOpts(Opts):
 	}
 
 	@staticmethod
-	def get_global_opts_filter(need_proto):
+	def get_global_filter_codes(need_proto):
 		"""
 		Coin codes:
 		  'b' - Bitcoin or Bitcoin code fork supporting RPC
@@ -351,7 +363,7 @@ class UserOpts(Opts):
 		  'r' - RPC required
 		  '-' - no capabilities required
 		"""
-		ret = namedtuple('global_opts_filter', ['coin', 'cmd'])
+		ret = namedtuple('global_filter_codes', ['coin', 'cmd'])
 		if caps := gc.cmd_caps:
 			coin = caps.coin if caps.coin and len(caps.coin) > 1 else get_coin()
 			return ret(

+ 17 - 3
test/cmdtest_d/ct_help.py

@@ -26,7 +26,9 @@ class CmdTestHelp(CmdTestBase):
 	passthru_opts = ('daemon_data_dir', 'rpc_port', 'coin', 'testnet')
 	cmd_group = (
 		('usage1',            (1, 'usage message (via --usage)', [])),
-		('usage2',            (1, 'usage message (via bad invocation)', [])),
+		('usage2',            (1, 'usage message (via --usage)', [])),
+		('usage3',            (1, 'usage message (via bad invocation)', [])),
+		('usage4',            (1, 'usage message (via bad invocation, with --coin)', [])),
 		('version',           (1, 'version message', [])),
 		('license',           (1, 'license message', [])),
 		('helpscreens',       (1, 'help screens',             [])),
@@ -39,15 +41,27 @@ class CmdTestHelp(CmdTestBase):
 	)
 
 	def usage1(self):
-		t = self.spawn('mmgen-txsend', ['--usage'], no_passthru_opts=True)
-		t.expect('USAGE: mmgen-txsend')
+		t = self.spawn('mmgen-walletgen', ['--usage'], no_passthru_opts=True)
+		t.expect('USAGE: mmgen-walletgen')
 		return t
 
 	def usage2(self):
+		cmd = 'xmrwallet' if self.coin == 'xmr' else 'txcreate'
+		t = self.spawn(f'mmgen-{cmd}', ['--usage', f'--coin={self.coin}'], no_passthru_opts=True)
+		t.expect(f'USAGE: mmgen-{cmd}')
+		return t
+
+	def usage3(self):
 		t = self.spawn('mmgen-walletgen', ['foo'], exit_val=1, no_passthru_opts=True)
 		t.expect('USAGE: mmgen-walletgen')
 		return t
 
+	def usage4(self):
+		cmd = 'xmrwallet' if self.coin == 'xmr' else 'addrgen'
+		t = self.spawn(f'mmgen-{cmd}', [f'--coin={self.coin}'], exit_val=1, no_passthru_opts=True)
+		t.expect(f'USAGE: mmgen-{cmd}')
+		return t
+
 	def version(self):
 		t = self.spawn('mmgen-tool', ['--version'], exit_val=0)
 		t.expect('MMGEN-TOOL version')