opts.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. opts: command-line options processing for the MMGen Project
  20. """
  21. import sys, os, re
  22. from collections import namedtuple
  23. from .cfg import gc
  24. def negated_opts(opts, data={}):
  25. if data:
  26. return data
  27. else:
  28. data.update(dict(
  29. ((k[3:] if k.startswith('no-') else f'no-{k}'), v)
  30. for k, v in opts.items()
  31. if len(k) > 1 and not v.has_parm))
  32. return data
  33. def get_opt_by_substring(opt, opts):
  34. matches = [o for o in opts if o.startswith(opt)]
  35. if len(matches) == 1:
  36. return matches[0]
  37. if len(matches) > 1:
  38. from .util import die
  39. die('CmdlineOptError', f'--{opt}: ambiguous option (not unique substring)')
  40. def process_uopts(cfg, opts_data, opts, need_proto):
  41. from .util import die
  42. def get_uopts():
  43. nonlocal uargs
  44. idx = 1
  45. argv_len = len(sys.argv)
  46. while idx < argv_len:
  47. arg = sys.argv[idx]
  48. if len(arg) > 4096:
  49. raise RuntimeError(f'{len(arg)} bytes: command-line argument too long')
  50. if arg.startswith('--'):
  51. if len(arg) == 2:
  52. uargs = sys.argv[idx+1:]
  53. return
  54. opt, parm = arg[2:].split('=', 1) if '=' in arg else (arg[2:], None)
  55. if len(opt) < 2:
  56. die('CmdlineOptError', f'--{opt}: option name must be at least two characters long')
  57. if (
  58. (_opt := opt) in opts
  59. or (_opt := get_opt_by_substring(_opt, opts))):
  60. if opts[_opt].has_parm:
  61. if parm:
  62. yield (opts[_opt].name, parm)
  63. else:
  64. idx += 1
  65. if idx == argv_len or (parm := sys.argv[idx]).startswith('-'):
  66. die('CmdlineOptError', f'missing parameter for option --{_opt}')
  67. yield (opts[_opt].name, parm)
  68. else:
  69. if parm:
  70. die('CmdlineOptError', f'option --{_opt} requires no parameter')
  71. yield (opts[_opt].name, True)
  72. elif (
  73. (_opt := opt) in negated_opts(opts)
  74. or (_opt := get_opt_by_substring(_opt, negated_opts(opts)))):
  75. if parm:
  76. die('CmdlineOptError', f'option --{_opt} requires no parameter')
  77. yield (negated_opts(opts)[_opt].name, False)
  78. elif (
  79. need_proto
  80. and (not gc.cmd_caps or gc.cmd_caps.rpc)
  81. and any(opt.startswith(coin + '-') for coin in gc.rpc_coins)):
  82. opt_name = opt.replace('-', '_')
  83. from .protocol import init_proto
  84. try:
  85. refval = init_proto(cfg, opt.split('-', 1)[0], return_cls=True).get_opt_clsval(cfg, opt_name)
  86. except AttributeError:
  87. die('CmdlineOptError', f'--{opt}: unrecognized option')
  88. else:
  89. if refval is None: # None == no parm
  90. if parm:
  91. die('CmdlineOptError', f'option --{opt} requires no parameter')
  92. yield (opt_name, True)
  93. else:
  94. from .cfg import conv_type
  95. if parm:
  96. yield (opt_name,
  97. conv_type(opt_name, parm, refval, src='cmdline'))
  98. else:
  99. idx += 1
  100. if idx == argv_len or (parm := sys.argv[idx]).startswith('-'):
  101. die('CmdlineOptError', f'missing parameter for option --{opt}')
  102. yield (opt_name,
  103. conv_type(opt_name, parm, refval, src='cmdline'))
  104. else:
  105. die('CmdlineOptError', f'--{opt}: unrecognized option')
  106. elif arg[0] == '-' and len(arg) > 1:
  107. for j, sopt in enumerate(arg[1:], 2):
  108. if sopt in opts:
  109. if opts[sopt].has_parm:
  110. if arg[j:]:
  111. yield (opts[sopt].name, arg[j:])
  112. else:
  113. idx += 1
  114. if idx == argv_len or (parm := sys.argv[idx]).startswith('-'):
  115. die('CmdlineOptError', f'missing parameter for option -{sopt}')
  116. yield (opts[sopt].name, parm)
  117. break
  118. else:
  119. yield (opts[sopt].name, True)
  120. else:
  121. die('CmdlineOptError', f'-{sopt}: unrecognized option')
  122. else:
  123. uargs = sys.argv[idx:]
  124. return
  125. idx += 1
  126. uargs = []
  127. uopts = dict(get_uopts())
  128. return uopts, uargs
  129. cmd_opts_v1_pat = re.compile(r'^-([a-zA-Z0-9-]), --([a-zA-Z0-9-]{2,64})(=| )(.+)')
  130. cmd_opts_v2_pat = re.compile(r'^\t\t\t(.)(.) -([a-zA-Z0-9-]), --([a-z0-9-]{2,64})(=| )(.+)')
  131. cmd_opts_v2_help_pat = re.compile(r'^\t\t\t(.)(.) (?:-([a-zA-Z0-9-]), --([a-z0-9-]{2,64})(=| ))?(.+)')
  132. global_opts_pat = re.compile(r'^\t\t\t(.)(.) --([a-z0-9-]{2,64})(=| )(.+)')
  133. global_opts_help_pat = re.compile(r'^\t\t\t(.)(.) (?:--([{}a-zA-Z0-9-]{2,64})(=| ))?(.+)')
  134. opt_tuple = namedtuple('cmdline_option', ['name', 'has_parm'])
  135. def parse_opts(cfg, opts_data, global_opts_data, global_filter_codes, *, need_proto):
  136. def parse_v1():
  137. for line in opts_data['text']['options'].strip().splitlines():
  138. if m := cmd_opts_v1_pat.match(line):
  139. ret = opt_tuple(m[2].replace('-', '_'), m[3] == '=')
  140. yield (m[1], ret)
  141. yield (m[2], ret)
  142. def parse_v2():
  143. cmd_filter_codes = opts_data['filter_codes']
  144. coin_codes = global_filter_codes.coin
  145. for line in opts_data['text']['options'].splitlines():
  146. m = cmd_opts_v2_pat.match(line)
  147. if m and (coin_codes is None or m[1] in coin_codes) and m[2] in cmd_filter_codes:
  148. ret = opt_tuple(m[4].replace('-', '_'), m[5] == '=')
  149. yield (m[3], ret)
  150. yield (m[4], ret)
  151. def parse_global():
  152. coin_codes = global_filter_codes.coin
  153. cmd_codes = global_filter_codes.cmd
  154. for line in global_opts_data['text']['options'].splitlines():
  155. m = global_opts_pat.match(line)
  156. if m and (
  157. (coin_codes is None or m[1] in coin_codes) and
  158. (cmd_codes is None or m[2] in cmd_codes)):
  159. yield (m[3], opt_tuple(m[3].replace('-', '_'), m[4] == '='))
  160. opts = tuple((parse_v2 if 'filter_codes' in opts_data else parse_v1)()) + tuple(parse_global())
  161. uopts, uargs = process_uopts(cfg, opts_data, dict(opts), need_proto)
  162. return namedtuple('parsed_cmd_opts', ['user_opts', 'cmd_args', 'opts'])(
  163. uopts, # dict
  164. uargs, # list, callers can pop
  165. tuple(v.name for k, v in opts if len(k) > 1)
  166. )
  167. def opt_preproc_debug(po):
  168. d = (
  169. ('Cmdline', ' '.join(sys.argv), False),
  170. ('Filtered opts', po.filtered_opts, False),
  171. ('User-selected opts', po.user_opts, False),
  172. ('Cmd args', po.cmd_args, False),
  173. ('Opts', po.opts, True),
  174. )
  175. from .util import Msg, fmt_list
  176. Msg('\n=== opts.py debug ===')
  177. for label, data, pretty in d:
  178. Msg(' {:<20}: {}'.format(label, '\n' + fmt_list(data, fmt='col', indent=' '*8) if pretty else data))
  179. opts_data_dfl = {
  180. 'text': {
  181. 'desc': '',
  182. 'usage':'[options]',
  183. 'options': """
  184. -h, --help Print this help message
  185. --, --longhelp Print help message for long (global) options
  186. """
  187. }
  188. }
  189. def get_coin():
  190. for n, arg in enumerate(sys.argv[1:]):
  191. if len(arg) > 4096:
  192. raise RuntimeError(f'{len(arg)} bytes: command-line argument too long')
  193. if arg.startswith('--coin='):
  194. return arg.removeprefix('--coin=').lower()
  195. if arg == '--coin':
  196. if len(sys.argv) < n + 3:
  197. from .util import die
  198. die('CmdlineOptError', f'{arg}: missing parameter')
  199. return sys.argv[n + 2].lower()
  200. if arg == '-' or not arg.startswith('-'): # stop at first non-option
  201. return 'btc'
  202. return 'btc'
  203. class Opts:
  204. def __init__(
  205. self,
  206. cfg,
  207. *,
  208. opts_data,
  209. init_opts, # dict containing opts to pre-initialize
  210. parsed_opts,
  211. need_proto):
  212. if len(sys.argv) > 257:
  213. raise RuntimeError(f'{len(sys.argv) - 1}: too many command-line arguments')
  214. opts_data = opts_data or opts_data_dfl
  215. self.global_filter_codes = self.get_global_filter_codes(need_proto)
  216. self.opts_data = opts_data
  217. po = parsed_opts or parse_opts(
  218. cfg,
  219. opts_data,
  220. self.global_opts_data,
  221. self.global_filter_codes,
  222. need_proto = need_proto)
  223. cfg._args = po.cmd_args
  224. cfg._uopts = uopts = po.user_opts
  225. if init_opts: # initialize user opts to given value
  226. for uopt, val in init_opts.items():
  227. if uopt not in uopts:
  228. uopts[uopt] = val
  229. cfg._opts = self
  230. cfg._parsed_opts = po
  231. cfg._use_env = True
  232. cfg._use_cfg_file = not 'skip_cfg_file' in uopts
  233. # Make these available to usage():
  234. cfg._usage_data = opts_data['text'].get('usage2') or opts_data['text']['usage']
  235. cfg._usage_code = opts_data.get('code', {}).get('usage')
  236. cfg._help_pkg = self.help_pkg
  237. if os.getenv('MMGEN_DEBUG_OPTS'):
  238. opt_preproc_debug(po)
  239. for funcname in self.info_funcs:
  240. if funcname in uopts:
  241. import importlib
  242. getattr(importlib.import_module(self.help_pkg), funcname)(cfg) # exits
  243. class UserOpts(Opts):
  244. help_pkg = 'mmgen.help'
  245. info_funcs = ('version', 'show_hash_presets', 'list_daemon_ids')
  246. global_opts_data = {
  247. # coin code : cmd code : opt : opt param : text
  248. 'text': {
  249. 'options': """
  250. -- --accept-defaults Accept defaults at all prompts
  251. hp --cashaddr=0|1 Display addresses in cashaddr format (default: 1)
  252. -c --coin=c Choose coin unit. Default: BTC. Current choice: {cu_dfl}
  253. er --token=t Specify an ERC20 token by address or symbol
  254. -- --color=0|1 Disable or enable color output (default: 1)
  255. -- --columns=N Force N columns of output with certain commands
  256. Rr --scroll Use the curses-like scrolling interface for
  257. + tracking wallet views
  258. -- --force-256-color Force 256-color output when color is enabled
  259. -- --pager Pipe output of certain commands to pager (WIP)
  260. -- --data-dir=path Specify {pnm} data directory location
  261. rr --daemon-data-dir=path Specify coin daemon data directory location
  262. Rr --daemon-id=ID Specify the coin daemon ID
  263. rr --ignore-daemon-version Ignore coin daemon version check
  264. Rr --list-daemon-ids List all available daemon IDs
  265. xr --http-timeout=t Set HTTP timeout in seconds for JSON-RPC connections
  266. -- --no-license Suppress the GPL license prompt
  267. Rr --rpc-host=HOST Communicate with coin daemon running on host HOST
  268. rr --rpc-port=PORT Communicate with coin daemon listening on port PORT
  269. br --rpc-user=USER Authenticate to coin daemon using username USER
  270. br --rpc-password=PASS Authenticate to coin daemon using password PASS
  271. Rr --rpc-backend=backend Use backend 'backend' for JSON-RPC communications
  272. Rr --aiohttp-rpc-queue-len=N Use N simultaneous RPC connections with aiohttp
  273. -p --regtest=0|1 Disable or enable regtest mode
  274. -- --testnet=0|1 Disable or enable testnet
  275. -- --test-suite Use test suite configuration
  276. br --tw-name=NAME Specify alternate name for the BTC/LTC/BCH tracking
  277. + wallet (default: ‘{tw_name}’)
  278. -- --skip-cfg-file Skip reading the configuration file
  279. -- --version Print version information and exit
  280. -- --usage Print usage information and exit
  281. x- --bob Specify user ‘Bob’ in MMGen regtest or test mode
  282. x- --alice Specify user ‘Alice’ in MMGen regtest or test mode
  283. x- --carol Specify user ‘Carol’ in MMGen regtest or test mode
  284. x- --miner Specify user ‘Miner’ in MMGen regtest or test mode
  285. rr COIN-SPECIFIC OPTIONS:
  286. rr For descriptions, refer to the non-prefixed versions of these options above
  287. rr Prefixed options override their non-prefixed counterparts
  288. rr OPTION SUPPORTED PREFIXES
  289. Rr --PREFIX-daemon-id btc ltc bch eth etc
  290. rr --PREFIX-ignore-daemon-version btc ltc bch eth etc xmr
  291. br --PREFIX-tw-name btc ltc bch
  292. Rr --PREFIX-rpc-host btc ltc bch eth etc
  293. rr --PREFIX-rpc-port btc ltc bch eth etc xmr
  294. br --PREFIX-rpc-user btc ltc bch
  295. br --PREFIX-rpc-password btc ltc bch
  296. Rr --PREFIX-max-tx-fee btc ltc bch eth etc
  297. Rr PROTO-SPECIFIC OPTIONS:
  298. Rr Option Supported Prefixes
  299. Rr --PREFIX-chain-names eth-mainnet eth-testnet etc-mainnet etc-testnet
  300. """,
  301. },
  302. 'code': {
  303. 'options': lambda proto, help_notes, s: s.format(
  304. pnm = gc.proj_name,
  305. cu_dfl = proto.coin,
  306. tw_name = help_notes('dfl_twname')),
  307. }
  308. }
  309. @staticmethod
  310. def get_global_filter_codes(need_proto):
  311. """
  312. Enable options based on the value of --coin and name of executable
  313. Both must produce a matching code list, or None, for the option to be enabled
  314. Coin codes:
  315. 'b' - Bitcoin or Bitcoin code fork supporting RPC
  316. 'R' - Bitcoin or Ethereum code fork supporting RPC
  317. 'e' - Ethereum or Ethereum code fork
  318. 'h' - Bitcoin Cash
  319. 'r' - local RPC coin
  320. 'X' - remote RPC coin
  321. 'x' - local or remote RPC coin
  322. '-' - any coin
  323. Cmd codes:
  324. 'p' - proto required
  325. 'c' - proto required, --coin recognized
  326. 'r' - RPC required
  327. '-' - no capabilities required
  328. """
  329. ret = namedtuple('global_filter_codes', ['coin', 'cmd'])
  330. if caps := gc.cmd_caps:
  331. coin = get_coin() if caps.use_coin_opt else None
  332. # a return value of None removes the filter, enabling all options for the given criterion
  333. return ret(
  334. coin = caps.coin_codes or (
  335. None if coin is None else
  336. ['-', 'r', 'R', 'b', 'h', 'x'] if coin == 'bch' else
  337. ['-', 'r', 'R', 'b', 'x'] if coin in gc.btc_fork_rpc_coins else
  338. ['-', 'r', 'R', 'e', 'x'] if coin in gc.eth_fork_coins else
  339. ['-', 'r', 'x'] if coin in gc.local_rpc_coins else
  340. ['-', 'X', 'x'] if coin in gc.remote_rpc_coins else
  341. ['-']),
  342. cmd = (
  343. ['-']
  344. + (['r'] if caps.rpc else [])
  345. + (['p', 'c'] if caps.proto and caps.use_coin_opt else ['p'] if caps.proto else [])
  346. ))
  347. else: # unmanaged command: enable everything
  348. return ret(None, None)