opts.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2024 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. if 'sets' in opts_data:
  129. for a_opt, a_val, b_opt, b_val in opts_data['sets']:
  130. if a_opt in uopts:
  131. u_val = uopts[a_opt]
  132. if (u_val and a_val == bool) or u_val == a_val:
  133. if b_opt in uopts and uopts[b_opt] != b_val:
  134. die(1,
  135. 'Option conflict:'
  136. + '\n --{}={}, with'.format(b_opt.replace('_', '-'), uopts[b_opt])
  137. + '\n --{}={}\n'.format(a_opt.replace('_', '-'), uopts[a_opt]))
  138. else:
  139. uopts[b_opt] = b_val
  140. return uopts, uargs
  141. cmd_opts_v1_pat = re.compile(r'^-([a-zA-Z0-9-]), --([a-zA-Z0-9-]{2,64})(=| )(.+)')
  142. cmd_opts_v2_pat = re.compile(r'^\t\t\t(.)(.) -([a-zA-Z0-9-]), --([a-z0-9-]{2,64})(=| )(.+)')
  143. cmd_opts_v2_help_pat = re.compile(r'^\t\t\t(.)(.) (?:-([a-zA-Z0-9-]), --([a-z0-9-]{2,64})(=| ))?(.+)')
  144. global_opts_pat = re.compile(r'^\t\t\t(.)(.) --([a-z0-9-]{2,64})(=| )(.+)')
  145. global_opts_help_pat = re.compile(r'^\t\t\t(.)(.) (?:--([{}a-zA-Z0-9-]{2,64})(=| ))?(.+)')
  146. opt_tuple = namedtuple('cmdline_option', ['name', 'has_parm'])
  147. def parse_opts(cfg, opts_data, global_opts_data, global_filter_codes, need_proto):
  148. def parse_v1():
  149. for line in opts_data['text']['options'].strip().splitlines():
  150. if m := cmd_opts_v1_pat.match(line):
  151. ret = opt_tuple(m[2].replace('-', '_'), m[3] == '=')
  152. yield (m[1], ret)
  153. yield (m[2], ret)
  154. def parse_v2():
  155. cmd_filter_codes = opts_data['filter_codes']
  156. for line in opts_data['text']['options'].splitlines():
  157. m = cmd_opts_v2_pat.match(line)
  158. if m and m[1] in global_filter_codes.coin and m[2] in cmd_filter_codes:
  159. ret = opt_tuple(m[4].replace('-', '_'), m[5] == '=')
  160. yield (m[3], ret)
  161. yield (m[4], ret)
  162. def parse_global():
  163. for line in global_opts_data['text']['options'].splitlines():
  164. m = global_opts_pat.match(line)
  165. if m and m[1] in global_filter_codes.coin and m[2] in global_filter_codes.cmd:
  166. yield (m[3], opt_tuple(m[3].replace('-', '_'), m[4] == '='))
  167. opts = tuple((parse_v2 if 'filter_codes' in opts_data else parse_v1)()) + tuple(parse_global())
  168. uopts, uargs = process_uopts(cfg, opts_data, dict(opts), need_proto)
  169. return namedtuple('parsed_cmd_opts', ['user_opts', 'cmd_args', 'opts'])(
  170. uopts, # dict
  171. uargs, # list, callers can pop
  172. tuple(v.name for k, v in opts if len(k) > 1)
  173. )
  174. def opt_preproc_debug(po):
  175. d = (
  176. ('Cmdline', ' '.join(sys.argv), False),
  177. ('Filtered opts', po.filtered_opts, False),
  178. ('User-selected opts', po.user_opts, False),
  179. ('Cmd args', po.cmd_args, False),
  180. ('Opts', po.opts, True),
  181. )
  182. from .util import Msg, fmt_list
  183. Msg('\n=== opts.py debug ===')
  184. for label, data, pretty in d:
  185. Msg(' {:<20}: {}'.format(label, '\n' + fmt_list(data, fmt='col', indent=' '*8) if pretty else data))
  186. opts_data_dfl = {
  187. 'text': {
  188. 'desc': '',
  189. 'usage':'[options]',
  190. 'options': """
  191. -h, --help Print this help message
  192. --, --longhelp Print help message for long (global) options
  193. """
  194. }
  195. }
  196. def get_coin():
  197. for n, arg in enumerate(sys.argv[1:]):
  198. if len(arg) > 4096:
  199. raise RuntimeError(f'{len(arg)} bytes: command-line argument too long')
  200. if arg.startswith('--coin='):
  201. return arg.removeprefix('--coin=').lower()
  202. if arg == '--coin':
  203. if len(sys.argv) < n + 3:
  204. from .util import die
  205. die('CmdlineOptError', f'{arg}: missing parameter')
  206. return sys.argv[n + 2].lower()
  207. if arg == '-' or not arg.startswith('-'): # stop at first non-option
  208. return 'btc'
  209. return 'btc'
  210. class Opts:
  211. def __init__(
  212. self,
  213. cfg,
  214. opts_data,
  215. init_opts, # dict containing opts to pre-initialize
  216. parsed_opts,
  217. need_proto):
  218. if len(sys.argv) > 257:
  219. raise RuntimeError(f'{len(sys.argv) - 1}: too many command-line arguments')
  220. opts_data = opts_data or opts_data_dfl
  221. self.global_filter_codes = self.get_global_filter_codes(need_proto)
  222. self.opts_data = opts_data
  223. po = parsed_opts or parse_opts(
  224. cfg,
  225. opts_data,
  226. self.global_opts_data,
  227. self.global_filter_codes,
  228. need_proto)
  229. cfg._args = po.cmd_args
  230. cfg._uopts = uopts = po.user_opts
  231. if init_opts: # initialize user opts to given value
  232. for uopt, val in init_opts.items():
  233. if uopt not in uopts:
  234. uopts[uopt] = val
  235. cfg._opts = self
  236. cfg._parsed_opts = po
  237. cfg._use_env = True
  238. cfg._use_cfg_file = not 'skip_cfg_file' in uopts
  239. # Make these available to usage():
  240. cfg._usage_data = opts_data['text'].get('usage2') or opts_data['text']['usage']
  241. cfg._usage_code = opts_data.get('code', {}).get('usage')
  242. if os.getenv('MMGEN_DEBUG_OPTS'):
  243. opt_preproc_debug(po)
  244. for funcname in self.info_funcs:
  245. if funcname in uopts:
  246. import importlib
  247. getattr(importlib.import_module(self.help_pkg), funcname)(cfg) # exits
  248. class UserOpts(Opts):
  249. help_pkg = 'mmgen.help'
  250. info_funcs = ('version', 'show_hash_presets')
  251. global_opts_data = {
  252. # coin code : cmd code : opt : opt param : text
  253. 'text': {
  254. 'options': """
  255. -- --accept-defaults Accept defaults at all prompts
  256. hp --cashaddr=0|1 Display addresses in cashaddr format (default: 1)
  257. -p --coin=c Choose coin unit. Default: BTC. Current choice: {cu_dfl}
  258. er --token=t Specify an ERC20 token by address or symbol
  259. -- --color=0|1 Disable or enable color output (default: 1)
  260. -- --columns=N Force N columns of output with certain commands
  261. Rr --scroll Use the curses-like scrolling interface for
  262. + tracking wallet views
  263. -- --force-256-color Force 256-color output when color is enabled
  264. -- --pager Pipe output of certain commands to pager (WIP)
  265. -- --data-dir=path Specify {pnm} data directory location
  266. rr --daemon-data-dir=path Specify coin daemon data directory location
  267. Rr --daemon-id=ID Specify the coin daemon ID
  268. rr --ignore-daemon-version Ignore coin daemon version check
  269. rr --http-timeout=t Set HTTP timeout in seconds for JSON-RPC connections
  270. -- --no-license Suppress the GPL license prompt
  271. Rr --rpc-host=HOST Communicate with coin daemon running on host HOST
  272. rr --rpc-port=PORT Communicate with coin daemon listening on port PORT
  273. br --rpc-user=USER Authenticate to coin daemon using username USER
  274. br --rpc-password=PASS Authenticate to coin daemon using password PASS
  275. Rr --rpc-backend=backend Use backend 'backend' for JSON-RPC communications
  276. Rr --aiohttp-rpc-queue-len=N Use N simultaneous RPC connections with aiohttp
  277. -p --regtest=0|1 Disable or enable regtest mode
  278. -- --testnet=0|1 Disable or enable testnet
  279. br --tw-name=NAME Specify alternate name for the BTC/LTC/BCH tracking
  280. + wallet (default: ‘{tw_name}’)
  281. -- --skip-cfg-file Skip reading the configuration file
  282. -- --version Print version information and exit
  283. -- --usage Print usage information and exit
  284. b- --bob Specify user ‘Bob’ in MMGen regtest mode
  285. b- --alice Specify user ‘Alice’ in MMGen regtest mode
  286. b- --carol Specify user ‘Carol’ in MMGen regtest mode
  287. rr COIN-SPECIFIC OPTIONS:
  288. rr For descriptions, refer to the non-prefixed versions of these options above
  289. rr Prefixed options override their non-prefixed counterparts
  290. rr OPTION SUPPORTED PREFIXES
  291. rr --PREFIX-ignore-daemon-version btc ltc bch eth etc xmr
  292. br --PREFIX-tw-name btc ltc bch
  293. Rr --PREFIX-rpc-host btc ltc bch eth etc
  294. rr --PREFIX-rpc-port btc ltc bch eth etc xmr
  295. br --PREFIX-rpc-user btc ltc bch
  296. br --PREFIX-rpc-password btc ltc bch
  297. Rr --PREFIX-max-tx-fee btc ltc bch eth etc
  298. Rr PROTO-SPECIFIC OPTIONS:
  299. Rr Option Supported Prefixes
  300. Rr --PREFIX-chain-names eth-mainnet eth-testnet etc-mainnet etc-testnet
  301. """,
  302. },
  303. 'code': {
  304. 'options': lambda proto, help_notes, s: s.format(
  305. pnm = gc.proj_name,
  306. cu_dfl = proto.coin,
  307. tw_name = help_notes('dfl_twname')),
  308. }
  309. }
  310. @staticmethod
  311. def get_global_filter_codes(need_proto):
  312. """
  313. Coin codes:
  314. 'b' - Bitcoin or Bitcoin code fork supporting RPC
  315. 'R' - Bitcoin or Ethereum code fork supporting RPC
  316. 'e' - Ethereum or Ethereum code fork
  317. 'r' - coin supporting RPC
  318. 'h' - Bitcoin Cash
  319. '-' - other coin
  320. Cmd codes:
  321. 'p' - proto required
  322. 'r' - RPC required
  323. '-' - no capabilities required
  324. """
  325. ret = namedtuple('global_filter_codes', ['coin', 'cmd'])
  326. if caps := gc.cmd_caps:
  327. coin = caps.coin if caps.coin and len(caps.coin) > 1 else get_coin()
  328. return ret(
  329. coin = (
  330. ('-', 'r', 'R', 'b', 'h') if coin == 'bch' else
  331. ('-', 'r', 'R', 'b') if coin in gc.btc_fork_rpc_coins else
  332. ('-', 'r', 'R', 'e') if coin in gc.eth_fork_coins else
  333. ('-', 'r') if coin in gc.rpc_coins else
  334. ('-')),
  335. cmd = (
  336. ['-']
  337. + (['r'] if caps.rpc else [])
  338. + (['p'] if caps.proto else [])
  339. ))
  340. else:
  341. return ret(
  342. coin = ('-', 'r', 'R', 'b', 'h', 'e'),
  343. cmd = ('-', 'r', 'p')
  344. )