389 lines
14 KiB
Python
Executable file
389 lines
14 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
#
|
|
# MMGen Wallet, a terminal-based cryptocurrency wallet
|
|
# Copyright (C)2013-2025 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/>.
|
|
|
|
"""
|
|
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: # None == no parm
|
|
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())
|
|
|
|
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']
|
|
coin_codes = global_filter_codes.coin
|
|
for line in opts_data['text']['options'].splitlines():
|
|
m = cmd_opts_v2_pat.match(line)
|
|
if m and (coin_codes is None or m[1] in coin_codes) 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():
|
|
coin_codes = global_filter_codes.coin
|
|
cmd_codes = global_filter_codes.cmd
|
|
for line in global_opts_data['text']['options'].splitlines():
|
|
m = global_opts_pat.match(line)
|
|
if m and (
|
|
(coin_codes is None or m[1] in coin_codes) and
|
|
(cmd_codes is None or m[2] in cmd_codes)):
|
|
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, # 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),
|
|
('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('-'): # stop at first non-option
|
|
return 'btc'
|
|
return 'btc'
|
|
|
|
class Opts:
|
|
|
|
def __init__(
|
|
self,
|
|
cfg,
|
|
*,
|
|
opts_data,
|
|
init_opts, # dict containing opts to pre-initialize
|
|
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 = need_proto)
|
|
|
|
cfg._args = po.cmd_args
|
|
cfg._uopts = uopts = po.user_opts
|
|
|
|
if init_opts: # initialize user opts to given value
|
|
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
|
|
|
|
# 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')
|
|
cfg._help_pkg = self.help_pkg
|
|
|
|
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) # exits
|
|
|
|
class UserOpts(Opts):
|
|
|
|
help_pkg = 'mmgen.help'
|
|
info_funcs = ('version', 'show_hash_presets', 'list_daemon_ids')
|
|
|
|
global_opts_data = {
|
|
# coin code : cmd code : opt : opt param : text
|
|
'text': {
|
|
'options': """
|
|
-- --accept-defaults Accept defaults at all prompts
|
|
hp --cashaddr=0|1 Display addresses in cashaddr format (default: 1)
|
|
-c --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 --list-daemon-ids List all available daemon IDs
|
|
xr --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
|
|
-- --test-suite Use test suite configuration
|
|
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
|
|
x- --bob Specify user ‘Bob’ in MMGen regtest or test mode
|
|
x- --alice Specify user ‘Alice’ in MMGen regtest or test mode
|
|
x- --carol Specify user ‘Carol’ in MMGen regtest or test 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-daemon-id btc ltc bch eth etc
|
|
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):
|
|
"""
|
|
Enable options based on the value of --coin and name of executable
|
|
|
|
Both must produce a matching code list, or None, for the option to be enabled
|
|
|
|
Coin codes:
|
|
'b' - Bitcoin or Bitcoin code fork supporting RPC
|
|
'R' - Bitcoin or Ethereum code fork supporting RPC
|
|
'e' - Ethereum or Ethereum code fork
|
|
'h' - Bitcoin Cash
|
|
'r' - local RPC coin
|
|
'X' - remote RPC coin
|
|
'x' - local or remote RPC coin
|
|
'-' - any coin
|
|
Cmd codes:
|
|
'p' - proto required
|
|
'c' - proto required, --coin recognized
|
|
'r' - RPC required
|
|
'-' - no capabilities required
|
|
"""
|
|
ret = namedtuple('global_filter_codes', ['coin', 'cmd'])
|
|
if caps := gc.cmd_caps:
|
|
coin = get_coin() if caps.use_coin_opt else None
|
|
# a return value of None removes the filter, enabling all options for the given criterion
|
|
return ret(
|
|
coin = caps.coin_codes or (
|
|
None if coin is None else
|
|
['-', 'r', 'R', 'b', 'h', 'x'] if coin == 'bch' else
|
|
['-', 'r', 'R', 'b', 'x'] if coin in gc.btc_fork_rpc_coins else
|
|
['-', 'r', 'R', 'e', 'x'] if coin in gc.eth_fork_coins else
|
|
['-', 'r', 'x'] if coin in gc.local_rpc_coins else
|
|
['-', 'X', 'x'] if coin in gc.remote_rpc_coins else
|
|
['-']),
|
|
cmd = (
|
|
['-']
|
|
+ (['r'] if caps.rpc else [])
|
|
+ (['p', 'c'] if caps.proto and caps.use_coin_opt else ['p'] if caps.proto else [])
|
|
))
|
|
else: # unmanaged command: enable everything
|
|
return ret(None, None)
|