123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- """
- opts: command-line options processing for the MMGen Project
- """
- import sys, os, re
- from collections import namedtuple
- from .cfg import gc
- def negated_opts(opts, data={}):
- if data:
- return data
- else:
- data.update(dict(
- ((k[3:] if k.startswith('no-') else f'no-{k}'), v)
- for k, v in opts.items()
- if len(k) > 1 and not v.has_parm))
- return data
- 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(cfg, opts_data, opts, need_proto):
- 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('=', 1) 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 := 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)
- elif (
- (_opt := opt) in negated_opts(opts)
- or (_opt := get_opt_by_substring(_opt, negated_opts(opts)))):
- if parm:
- die('CmdlineOptError', f'option --{_opt} requires no parameter')
- yield (negated_opts(opts)[_opt].name, False)
- elif (
- need_proto
- and (not gc.cmd_caps or gc.cmd_caps.rpc)
- and any(opt.startswith(coin + '-') for coin in gc.rpc_coins)):
- opt_name = opt.replace('-', '_')
- from .protocol import init_proto
- try:
- refval = init_proto(cfg, opt.split('-', 1)[0], return_cls=True).get_opt_clsval(cfg, opt_name)
- except AttributeError:
- die('CmdlineOptError', f'--{opt}: unrecognized option')
- else:
- if refval is None:
- if parm:
- die('CmdlineOptError', f'option --{opt} requires no parameter')
- yield (opt_name, True)
- else:
- from .cfg import conv_type
- if parm:
- yield (opt_name,
- conv_type(opt_name, parm, refval, src='cmdline'))
- else:
- idx += 1
- if idx == argv_len or (parm := sys.argv[idx]).startswith('-'):
- die('CmdlineOptError', f'missing parameter for option --{opt}')
- yield (opt_name,
- conv_type(opt_name, parm, refval, src='cmdline'))
- else:
- die('CmdlineOptError', f'--{opt}: unrecognized option')
- elif arg[0] == '-' and len(arg) > 1:
- for j, sopt in enumerate(arg[1:], 2):
- if sopt in opts:
- if opts[sopt].has_parm:
- if arg[j:]:
- yield (opts[sopt].name, arg[j:])
- 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_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, global_opts_data, global_filter_codes, need_proto):
- def parse_v1():
- for line in opts_data['text']['options'].strip().splitlines():
- 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_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_filter_codes.coin and m[2] in global_filter_codes.cmd:
- yield (m[3], opt_tuple(m[3].replace('-', '_'), m[4] == '='))
- opts = tuple((parse_v2 if 'filter_codes' in opts_data else parse_v1)()) + tuple(parse_global())
- uopts, uargs = process_uopts(cfg, opts_data, dict(opts), need_proto)
- return namedtuple('parsed_cmd_opts', ['user_opts', 'cmd_args', 'opts'])(
- uopts,
- uargs,
- tuple(v.name for k, v in opts if len(k) > 1)
- )
- def opt_preproc_debug(po):
- d = (
- ('Cmdline', ' '.join(sys.argv), False),
- ('Filtered opts', po.filtered_opts, False),
- ('User-selected opts', po.user_opts, False),
- ('Cmd args', po.cmd_args, False),
- ('Opts', po.opts, True),
- )
- from .util import Msg, fmt_list
- Msg('\n=== opts.py debug ===')
- for label, data, pretty in d:
- Msg(' {:<20}: {}'.format(label, '\n' + fmt_list(data, fmt='col', indent=' '*8) if pretty else data))
- opts_data_dfl = {
- 'text': {
- 'desc': '',
- 'usage':'[options]',
- 'options': """
- -h, --help Print this help message
- --, --longhelp Print help message for long (global) options
- """
- }
- }
- 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('-'):
- return 'btc'
- return 'btc'
- class Opts:
- def __init__(
- self,
- cfg,
- opts_data,
- init_opts,
- parsed_opts,
- need_proto):
- if len(sys.argv) > 257:
- raise RuntimeError(f'{len(sys.argv) - 1}: too many command-line arguments')
- opts_data = opts_data or opts_data_dfl
- self.global_filter_codes = self.get_global_filter_codes(need_proto)
- self.opts_data = opts_data
- po = parsed_opts or parse_opts(
- cfg,
- opts_data,
- self.global_opts_data,
- self.global_filter_codes,
- need_proto)
- cfg._args = po.cmd_args
- cfg._uopts = uopts = po.user_opts
- if init_opts:
- for uopt, val in init_opts.items():
- if uopt not in uopts:
- uopts[uopt] = val
- cfg._opts = self
- cfg._parsed_opts = po
- cfg._use_env = True
- cfg._use_cfg_file = not 'skip_cfg_file' in uopts
-
- 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)
- for funcname in self.info_funcs:
- if funcname in uopts:
- import importlib
- getattr(importlib.import_module(self.help_pkg), funcname)(cfg)
- class UserOpts(Opts):
- help_pkg = 'mmgen.help'
- info_funcs = ('version', 'show_hash_presets')
- global_opts_data = {
-
- 'text': {
- 'options': """
- -- --accept-defaults Accept defaults at all prompts
- hp --cashaddr=0|1 Display addresses in cashaddr format (default: 1)
- -p --coin=c Choose coin unit. Default: BTC. Current choice: {cu_dfl}
- 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
- br --rpc-user=USER Authenticate to coin daemon using username USER
- br --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
- rr COIN-SPECIFIC OPTIONS:
- rr For descriptions, refer to the non-prefixed versions of these options above
- rr Prefixed options override their non-prefixed counterparts
- rr OPTION SUPPORTED PREFIXES
- rr --PREFIX-ignore-daemon-version btc ltc bch eth etc xmr
- br --PREFIX-tw-name btc ltc bch
- Rr --PREFIX-rpc-host btc ltc bch eth etc
- rr --PREFIX-rpc-port btc ltc bch eth etc xmr
- br --PREFIX-rpc-user btc ltc bch
- br --PREFIX-rpc-password btc ltc bch
- Rr --PREFIX-max-tx-fee btc ltc bch eth etc
- Rr PROTO-SPECIFIC OPTIONS:
- Rr Option Supported Prefixes
- Rr --PREFIX-chain-names eth-mainnet eth-testnet etc-mainnet etc-testnet
- """,
- },
- 'code': {
- 'options': 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_filter_codes(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_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(
- 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')
- )
|