mmgen-tool: improve usage screens for individual commands

Example:

    $ mmgen-tool help listaddresses

Testing/demo:

    $ test/test.py -e tool_cmd_usage
This commit is contained in:
The MMGen Project 2022-08-15 12:38:46 +00:00
commit d07d665f35
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
9 changed files with 220 additions and 75 deletions

View file

@ -1 +1 @@
13.2.dev12
13.2.dev13

View file

@ -36,6 +36,7 @@ opts_data = {
-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, --list List available commands
-p, --hash-preset= p Use the scrypt hash parameters defined by preset 'p'
for password hashing (default: '{g.dfl_hash_preset}')
-P, --passwd-file= f Get passphrase from file 'f'.
@ -194,11 +195,10 @@ def create_call_sig(cmd,cls,as_string=False):
get_type_from_ann = lambda x: 'str or STDIN' if ann[x] == 'sstr' else ann[x].__name__
return ' '.join(
[f'{a} [{get_type_from_ann(a)}]' for a in args[:nargs]] +
['{a} [{b}={c!r}{d}]'.format(
['{a} [{b}={c!r}]'.format(
a = a,
b = dfl_types[n].__name__,
c = dfls[n],
d = (' ' + ann[a] if a in ann and isinstance(ann[a],str) else ''))
c = dfls[n] )
for n,a in enumerate(args[nargs:])] )
else:
get_type_from_ann = lambda x: 'str' if ann[x] == 'sstr' else ann[x].__name__
@ -206,10 +206,11 @@ def create_call_sig(cmd,cls,as_string=False):
[(a,get_type_from_ann(a)) for a in args[:nargs]], # c_args
dict([(a,dfls[n]) for n,a in enumerate(args[nargs:])]), # c_kwargs
dict([(a,dfl_types[n]) for n,a in enumerate(args[nargs:])]), # c_kwargs_types
('STDIN_OK' if nargs and ann[args[0]] == 'sstr' else flag) ) # flag
('STDIN_OK' if nargs and ann[args[0]] == 'sstr' else flag), # flag
ann ) # ann
def process_args(cmd,cmd_args,cls):
c_args,c_kwargs,c_kwargs_types,flag = create_call_sig(cmd,cls)
c_args,c_kwargs,c_kwargs_types,flag,ann = create_call_sig(cmd,cls)
have_stdin_input = False
def usage_die(s):
@ -339,6 +340,18 @@ if g.prog_name == 'mmgen-tool' and not opt._lock:
po = opts.init( opts_data, parse_only=True )
if po.user_opts.get('list'):
def gen():
for mod,cmdlist in mods.items():
if mod == 'help':
continue
yield capfirst( get_mod_cls(mod).__doc__.lstrip().split('\n')[0] ) + ':'
for cmd in cmdlist:
yield ' ' + cmd
yield ''
Msg('\n'.join(gen()).rstrip())
sys.exit(0)
if len(po.cmd_args) < 1:
opts.usage()

View file

@ -78,8 +78,10 @@ class tool_cmd(tool_cmd_base):
'dfls': ( False, False, 'addr', 'mtime' ),
'annots': {
'mmgen_tx_file(s)': str,
'sort': options_annot_str(['addr','raw']),
'filesort': options_annot_str(['mtime','ctime','atime']),
'pager': 'send output to pager',
'terse': 'produce compact tabular output',
'sort': 'sort order for transaction inputs and outputs ' + options_annot_str(['addr','raw']),
'filesort': 'file sort order ' + options_annot_str(['mtime','ctime','atime']),
}
},
*infiles,

View file

@ -29,7 +29,10 @@ from ..crypto import get_random,aesctr_iv_len
class tool_cmd(tool_cmd_base):
"file utilities"
def find_incog_data(self,filename:str,incog_id:str,keep_searching=False):
def find_incog_data(self,
filename: str,
incog_id: str,
keep_searching: 'continue search after finding data (ID collisions can yield false positives)' = False):
"Use an Incog ID to find hidden incognito wallet data"
from hashlib import sha256
@ -67,6 +70,24 @@ class tool_cmd(tool_cmd_base):
def rand2file(self,outfile:str,nbytes:str,threads=4,silent=False):
"""
write nbytes bytes of random data to specified file (dd-style byte specifiers supported)
Valid specifiers:
c = 1
w = 2
b = 512
kB = 1000
K = 1024
MB = 1000000
M = 1048576
GB = 1000000000
G = 1073741824
TB = 1000000000000
T = 1099511627776
PB = 1000000000000000
P = 1125899906842624
EB = 1000000000000000000
E = 1152921504606846976
"""
from threading import Thread
from queue import Queue

View file

@ -25,23 +25,20 @@ import mmgen.main_tool as main_tool
def main_help():
from ..util import pretty_format
from ..util import pretty_format,capfirst
def do():
for clsname,cmdlist in main_tool.mods.items():
cls = main_tool.get_mod_cls(clsname)
cls_doc = cls.__doc__.strip().split('\n')
for l in cls_doc:
if l is cls_doc[0]:
l += ':'
l = l.replace('\t','',1)
if l:
l = l.replace('\t',' ')
yield l[0].upper() + l[1:]
else:
yield ''
cls_docstr = cls.__doc__.strip()
yield capfirst(cls_docstr.split('\n')[0].strip()) + ':'
yield ''
if '\n' in cls_docstr:
for line in cls_docstr.split('\n')[2:]:
yield ' ' + line.lstrip('\t')
yield ''
max_w = max(map(len,cmdlist))
for cmdname in cmdlist:
@ -64,7 +61,7 @@ def gen_tool_usage():
from ..util import capfirst
m1 = """
USAGE INFORMATION FOR MMGEN-TOOL COMMANDS:
GENERAL USAGE INFORMATION FOR MMGEN-TOOL COMMANDS
Arguments with only type specified in square brackets are required
@ -140,15 +137,49 @@ def gen_tool_cmd_usage(mod,cmdname):
cls = main_tool.get_mod_cls(mod)
docstr = getattr(cls,cmdname).__doc__.strip()
args,kwargs,kwargs_types,flag = main_tool.create_call_sig(cmdname,cls)
args,kwargs,kwargs_types,flag,ann = main_tool.create_call_sig(cmdname,cls)
ARGS = 'ARG' if len(args) == 1 else 'ARGS' if args else ''
KWARGS = 'KEYWORD ARG' if len(kwargs) == 1 else 'KEYWORD ARGS' if kwargs else ''
yield '{a}\n\nUSAGE: {b} {c} {d}{e}'.format(
a = capfirst( docstr.split('\n')[0].strip() ),
yield capfirst( docstr.split('\n')[0].strip() )
yield ''
yield 'USAGE: {b} [OPTS] {c}{d}{e}'.format(
b = g.prog_name,
c = cmdname,
d = main_tool.create_call_sig(cmdname,cls,as_string=True),
e = '\n\n' + fmt('\n'.join(docstr.split('\n')[1:]),strip_char='\t').rstrip()
if '\n' in docstr else '' )
d = f' {ARGS}' if ARGS else '',
e = f' [{KWARGS}]' if KWARGS else '' )
if args:
max_w = max(len(k[0]) for k in args)
yield ''
yield f'Required {ARGS} (type shown in square brackets):'
yield ''
for argname,argtype in args:
have_sstr = ann.get(argname) == 'sstr'
yield ' {a:{w}} [{b}]{c}{d}'.format(
a = argname,
b = argtype,
c = " (use '-' to read from STDIN)" if have_sstr else '',
d = ' ' + ann[argname] if isinstance(ann.get(argname),str) and not have_sstr else '',
w = max_w )
if kwargs:
max_w = max(len(k) for k in kwargs)
max_w2 = max(len(kwargs_types[k].__name__) + len(repr(kwargs[k])) for k in kwargs) + 3
yield ''
yield f'Optional {KWARGS} (type and default value shown in square brackets):'
yield ''
for argname in kwargs:
yield ' {a:{w}} {b:{w2}} {c}'.format(
a = argname,
b = '[{}={}]'.format( kwargs_types[argname].__name__, repr(kwargs[argname]) ),
c = capfirst(ann[argname]) if isinstance(ann.get(argname),str) else '',
w = max_w,
w2 = max_w2 ).rstrip()
if '\n' in docstr:
for line in docstr.split('\n')[1:]:
yield line.lstrip('\t')
def usage(cmdname=None,exit_val=1):

View file

@ -35,7 +35,7 @@ mnemonic_fmts = {
'bip39': mft( 'bip39', None, bip39 ),
'xmrseed': mft( 'xmrseed', None, xmrseed ),
}
mn_opts_disp = options_annot_str(mnemonic_fmts)
mn_opts_disp = 'seed phrase format ' + options_annot_str(mnemonic_fmts)
class tool_cmd(tool_cmd_base):
"""
@ -106,7 +106,10 @@ class tool_cmd(tool_cmd_base):
f = mnemonic_fmts[fmt]
return f.conv_cls(fmt).tohex( seed_mnemonic.split(), f.pad )
def mn2hex_interactive( self, fmt:mn_opts_disp = dfl_mnemonic_fmt, mn_len=24, print_mn=False ):
def mn2hex_interactive( self,
fmt: mn_opts_disp = dfl_mnemonic_fmt,
mn_len: 'length of seed phrase in words' = 24,
print_mn: 'print the seed phrase after entry' = False ):
"convert an interactively supplied mnemonic seed phrase to a hexadecimal string"
from ..mn_entry import mn_entry
mn = mn_entry(fmt).get_mnemonic_from_user(25 if fmt == 'xmrseed' else mn_len,validate=False)
@ -119,7 +122,10 @@ class tool_cmd(tool_cmd_base):
"show stats for a mnemonic wordlist"
return mnemonic_fmts[fmt].conv_cls(fmt).check_wordlist()
def mn_printlist( self, fmt:mn_opts_disp = dfl_mnemonic_fmt, enum=False, pager=False ):
def mn_printlist(self,
fmt: mn_opts_disp = dfl_mnemonic_fmt,
enum: 'enumerate the list' = False,
pager: 'send output to pager' = False ):
"print a mnemonic wordlist"
ret = mnemonic_fmts[fmt].conv_cls(fmt).get_wordlist()
if enum:

View file

@ -22,6 +22,7 @@ tool/rpc.py: JSON/RPC routines for the 'mmgen-tool' utility
from .common import tool_cmd_base,options_annot_str
from ..tw.common import TwCommon
from ..tw.txhistory import TwTxHistory
class tool_cmd(tool_cmd_base):
"tracking-wallet commands using the JSON-RPC interface"
@ -35,16 +36,19 @@ class tool_cmd(tool_cmd_base):
r = await rpc_init( self.proto, ignore_daemon_version=True )
return f'{r.daemon.coind_name} version {r.daemon_version} ({r.daemon_version_str})'
async def getbalance(self,minconf=1,quiet=False,pager=False):
async def getbalance(self,
minconf: 'minimum number of confirmations' = 1,
quiet: 'produce quieter output' = False,
pager: 'send output to pager' = False ):
"list confirmed/unconfirmed, spendable/unspendable balances in tracking wallet"
from ..tw.bal import TwGetBalance
return (await TwGetBalance(self.proto,minconf,quiet)).format()
async def listaddress(self,
mmgen_addr:str,
minconf = 1,
showbtcaddr = True,
age_fmt: options_annot_str(TwCommon.age_fmts) = 'confs' ):
minconf: 'minimum number of confirmations' = 1,
showbtcaddr: 'display coin address in addition to MMGen ID' = True,
age_fmt: 'format for the Age/Date column ' + options_annot_str(TwCommon.age_fmts) = 'confs' ):
"list the specified MMGen address in the tracking wallet and its balance"
return await self.listaddresses(
@ -54,15 +58,15 @@ class tool_cmd(tool_cmd_base):
age_fmt = age_fmt )
async def listaddresses(self,
mmgen_addrs:'(range or list)' = '',
minconf = 1,
showempty = False,
pager = False,
showbtcaddrs = True,
all_labels = False,
sort: options_annot_str(['reverse','age']) = '',
age_fmt: options_annot_str(TwCommon.age_fmts) = 'confs' ):
"list MMGen addresses and their balances"
mmgen_addrs: 'hyphenated range or comma-separated list of addresses' = '',
minconf: 'minimum number of confirmations' = 1,
pager: 'send output to pager' = False,
showbtcaddr: 'display coin addresses in addition to MMGen IDs' = True,
showempty: 'show addresses with no balances' = True,
all_labels: 'show all addresses with labels' = False,
age_fmt: 'format for the Age/Date column ' + options_annot_str(TwCommon.age_fmts) = 'confs',
sort: 'address sort order ' + options_annot_str(['reverse','age']) = '' ):
"list MMGen addresses in the tracking wallet and their balances"
show_age = bool(age_fmt)
@ -111,14 +115,14 @@ class tool_cmd(tool_cmd_base):
return await obj.format_squeezed()
async def twview(self,
pager = False,
reverse = False,
wide = False,
minconf = 1,
sort = 'age',
age_fmt: options_annot_str(TwCommon.age_fmts) = 'confs',
interactive = False,
show_mmid = True ):
pager: 'send output to pager' = False,
reverse: 'reverse order of unspent outputs' = False,
wide: 'display data in wide tabular format' = False,
minconf: 'minimum number of confirmations' = 1,
sort: 'unspent output sort order ' + options_annot_str(TwCommon.sort_funcs) = 'age',
age_fmt: 'format for the Age/Date column ' + options_annot_str(TwCommon.age_fmts) = 'confs',
interactive: 'enable interactive operation' = False,
show_mmid: 'show MMGen IDs along with coin addresses' = True ):
"view tracking wallet unspent outputs"
from ..tw.unspent import TwUnspentOutputs
@ -129,16 +133,15 @@ class tool_cmd(tool_cmd_base):
return ret
async def txhist(self,
pager = False,
reverse = False,
detail = False,
sinceblock = 0,
sort = 'age',
age_fmt: options_annot_str(TwCommon.age_fmts) = 'confs',
interactive = False ):
pager: 'send output to pager' = False,
reverse: 'reverse order of transactions' = False,
detail: 'produce detailed, non-tabular output' = False,
sinceblock: 'display transactions starting from this block' = 0,
sort: 'transaction sort order ' + options_annot_str(TwTxHistory.sort_funcs) = 'age',
age_fmt: 'format for the Age/Date column ' + options_annot_str(TwCommon.age_fmts) = 'confs',
interactive: 'enable interactive operation' = False ):
"view transaction history of tracking wallet"
from ..tw.txhistory import TwTxHistory
obj = await TwTxHistory(self.proto,sinceblock=sinceblock)
return await self.twops(
obj,pager,reverse,detail,sort,age_fmt,interactive,show_mmid=None)

View file

@ -25,21 +25,64 @@ from .common import tool_cmd_base
class tool_cmd(tool_cmd_base):
"general string conversion and hashing utilities"
# mmgen.util.bytespec_map
def bytespec(self,dd_style_byte_specifier:str):
"convert a byte specifier such as '1GB' into an integer"
"""
convert a byte specifier such as 4GB into an integer
Valid specifiers:
c = 1
w = 2
b = 512
kB = 1000
K = 1024
MB = 1000000
M = 1048576
GB = 1000000000
G = 1073741824
TB = 1000000000000
T = 1099511627776
PB = 1000000000000000
P = 1125899906842624
EB = 1000000000000000000
E = 1152921504606846976
"""
from ..util import parse_bytespec
return parse_bytespec(dd_style_byte_specifier)
# mmgen.util.bytespec_map
def to_bytespec(self,
n: int,
dd_style_byte_specifier: str,
fmt = '0.2',
print_sym = True ):
"convert an integer to a byte specifier such as '1GB'"
fmt: 'width and precision of output' = '0.2',
print_sym: 'print the specifier after the numerical value' = True ):
"""
convert an integer to a byte specifier such as 4GB
Supported specifiers:
c = 1
w = 2
b = 512
kB = 1000
K = 1024
MB = 1000000
M = 1048576
GB = 1000000000
G = 1073741824
TB = 1000000000000
T = 1099511627776
PB = 1000000000000000
P = 1125899906842624
EB = 1000000000000000000
E = 1152921504606846976
"""
from ..util import int2bytespec
return int2bytespec( n, dd_style_byte_specifier, fmt, print_sym )
def randhex(self,nbytes=32):
def randhex(self,
nbytes: 'number of bytes to output' = 32 ):
"print 'n' bytes (default 32) of random data in hex format"
from ..crypto import get_random
return get_random( nbytes ).hex()
@ -58,7 +101,10 @@ class tool_cmd(tool_cmd_base):
"convert a hexadecimal string to bytes (warning: outputs binary data)"
return bytes.fromhex(hexstr)
def hexdump(self,infile:str,cols=8,line_nums='hex'):
def hexdump(self,
infile: str,
cols: 'number of columns in output' = 8,
line_nums: "format for line numbers (valid choices: 'hex','dec')" = 'hex'):
"create hexdump of data from file (use '-' for stdin)"
from ..fileutil import get_data_from_file
from ..util import pretty_hexdump
@ -81,7 +127,11 @@ class tool_cmd(tool_cmd_base):
from ..proto.common import hash160
return hash160( bytes.fromhex(hexstr) ).hex()
def hash256(self,data:str,file_input=False,hex_input=False): # TODO: handle stdin
# TODO: handle stdin
def hash256(self,
data: str,
file_input: 'first arg is the name of a file containing the data' = False,
hex_input: 'first arg is a hexadecimal string' = False ):
"compute sha256(sha256(data)) (double sha256)"
from hashlib import sha256
if file_input:
@ -113,30 +163,32 @@ class tool_cmd(tool_cmd_base):
return make_chksum_8(
get_data_from_file( infile, dash=True, quiet=True, binary=True ))
def randb58(self,nbytes=32,pad=0):
def randb58(self,
nbytes: 'number of bytes to output' = 32,
pad: 'pad output to this width' = 0 ):
"generate random data (default: 32 bytes) and convert it to base 58"
from ..crypto import get_random
from ..baseconv import baseconv
return baseconv('b58').frombytes( get_random(nbytes), pad=pad, tostr=True )
def bytestob58(self,infile:str,pad=0):
def bytestob58(self,infile:str,pad: 'pad output to this width' = 0):
"convert bytes to base 58 (supply data via STDIN)"
from ..fileutil import get_data_from_file
from ..baseconv import baseconv
data = get_data_from_file( infile, dash=True, quiet=True, binary=True )
return baseconv('b58').frombytes( data, pad=pad, tostr=True )
def b58tobytes(self,b58_str:'sstr',pad=0):
def b58tobytes(self,b58_str:'sstr',pad: 'pad output to this width' = 0):
"convert a base 58 string to bytes (warning: outputs binary data)"
from ..baseconv import baseconv
return baseconv('b58').tobytes( b58_str, pad=pad )
def hextob58(self,hexstr:'sstr',pad=0):
def hextob58(self,hexstr:'sstr',pad: 'pad output to this width' = 0):
"convert a hexadecimal string to base 58"
from ..baseconv import baseconv
return baseconv('b58').fromhex( hexstr, pad=pad, tostr=True )
def b58tohex(self,b58_str:'sstr',pad=0):
def b58tohex(self,b58_str:'sstr',pad: 'pad output to this width' = 0):
"convert a base 58 string to hexadecimal"
from ..baseconv import baseconv
return baseconv('b58').tohex( b58_str, pad=pad )
@ -151,24 +203,27 @@ class tool_cmd(tool_cmd_base):
from ..proto.common import b58chk_decode
return b58chk_decode(b58chk_str).hex()
def hextob32(self,hexstr:'sstr',pad=0):
def hextob32(self,hexstr:'sstr',pad: 'pad output to this width' = 0):
"convert a hexadecimal string to an MMGen-flavor base 32 string"
from ..baseconv import baseconv
return baseconv('b32').fromhex( hexstr, pad, tostr=True )
def b32tohex(self,b32_str:'sstr',pad=0):
def b32tohex(self,b32_str:'sstr',pad: 'pad output to this width' = 0):
"convert an MMGen-flavor base 32 string to hexadecimal"
from ..baseconv import baseconv
return baseconv('b32').tohex( b32_str.upper(), pad )
def hextob6d(self,hexstr:'sstr',pad=0,add_spaces=True):
def hextob6d(self,
hexstr:'sstr',
pad: 'pad output to this width' = 0,
add_spaces: 'add a space after every 5th character' = True):
"convert a hexadecimal string to die roll base6 (base6d)"
from ..baseconv import baseconv
from ..util import block_format
ret = baseconv('b6d').fromhex(hexstr,pad,tostr=True)
return block_format( ret, gw=5, cols=None ).strip() if add_spaces else ret
def b6dtohex(self,b6d_str:'sstr',pad=0):
def b6dtohex(self,b6d_str:'sstr',pad: 'pad output to this width' = 0):
"convert a die roll base6 (base6d) string to hexadecimal"
from ..baseconv import baseconv
from ..util import remove_whitespace

View file

@ -54,6 +54,7 @@ class TestSuiteHelp(TestSuiteBase):
('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,"'test.py' help screens",[])),
)
@ -114,6 +115,19 @@ class TestSuiteHelp(TestSuiteBase):
t = self.spawn_chk('mmgen-tool',args,extra_desc=f"('mmgen-tool {fmt_list(args,fmt='bare')}')")
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 args in (
['--help'],