opts.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2020 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.py: MMGen-specific options processing after generic processing by share.Opts
  20. """
  21. import sys,os,stat
  22. class opt_cls(object):
  23. pass
  24. opt = opt_cls()
  25. from .exception import UserOptError
  26. from .globalvars import g
  27. import mmgen.share.Opts
  28. from .util import *
  29. def usage():
  30. Die(1,'USAGE: {} {}'.format(g.prog_name,usage_txt))
  31. def fmt_opt(o):
  32. return '--' + o.replace('_','-')
  33. def die_on_incompatible_opts(incompat_list):
  34. for group in incompat_list:
  35. bad = [k for k in opt.__dict__ if k in group and getattr(opt,k) != None]
  36. if len(bad) > 1:
  37. die(1,'Conflicting options: {}'.format(', '.join(map(fmt_opt,bad))))
  38. def _show_hash_presets():
  39. fs = ' {:<7} {:<6} {:<3} {}'
  40. msg('Available parameters for scrypt.hash():')
  41. msg(fs.format('Preset','N','r','p'))
  42. for i in sorted(g.hash_presets.keys()):
  43. msg(fs.format(i,*g.hash_presets[i]))
  44. msg('N = memory usage (power of two), p = iterations (rounds)')
  45. def opt_preproc_debug(po):
  46. d = (
  47. ('Cmdline', ' '.join(sys.argv)),
  48. ('Opts', po.opts),
  49. ('Skipped opts', po.skipped_opts),
  50. ('User-selected opts', po.user_opts),
  51. ('Cmd args', po.cmd_args),
  52. )
  53. Msg('\n=== opts.py debug ===')
  54. for e in d:
  55. Msg(' {:<20}: {}'.format(*e))
  56. def opt_postproc_debug():
  57. a = [k for k in dir(opt) if k[:2] != '__' and getattr(opt,k) != None]
  58. b = [k for k in dir(opt) if k[:2] != '__' and getattr(opt,k) == None]
  59. Msg(' Opts after processing:')
  60. for k in a:
  61. v = getattr(opt,k)
  62. Msg(' {:18}: {!r:<6} [{}]'.format(k,v,type(v).__name__))
  63. Msg(" Opts set to 'None':")
  64. Msg(' {}\n'.format('\n '.join(b)))
  65. Msg(' Global vars:')
  66. for e in [d for d in dir(g) if d[:2] != '__']:
  67. Msg(' {:<20}: {}'.format(e, getattr(g,e)))
  68. Msg('\n=== end opts.py debug ===\n')
  69. def init_term_and_color():
  70. from .term import init_term
  71. init_term()
  72. if g.color: # MMGEN_DISABLE_COLOR sets this to False
  73. from .color import start_mscolor,init_color
  74. if g.platform == 'win':
  75. start_mscolor()
  76. init_color(num_colors=('auto',256)[bool(g.force_256_color)])
  77. def override_globals_from_cfg_file(ucfg):
  78. from .protocol import CoinProtocol
  79. for d in ucfg.parse():
  80. val = d.value
  81. if d.name in g.cfg_file_opts:
  82. ns = d.name.split('_')
  83. if ns[0] in CoinProtocol.coins:
  84. nse,tn = (ns[2:],True) if len(ns) > 2 and ns[1] == 'testnet' else (ns[1:],False)
  85. cls = CoinProtocol(ns[0],tn)
  86. attr = '_'.join(nse)
  87. else:
  88. cls = g
  89. attr = d.name
  90. refval = getattr(cls,attr)
  91. if type(refval) is dict and type(val) is str: # hack - catch single colon-separated value
  92. try:
  93. val = dict([val.split(':')])
  94. except:
  95. raise CfgFileParseError('Parse error in file {!r}, line {}'.format(ucfg.fn,d.lineno))
  96. val_conv = set_for_type(val,refval,attr,src=ucfg.fn)
  97. setattr(cls,attr,val_conv)
  98. else:
  99. die(2,'{!r}: unrecognized option in {!r}, line {}'.format(d.name,ucfg.fn,d.lineno))
  100. def override_globals_from_env():
  101. for name in g.env_opts:
  102. if name == 'MMGEN_DEBUG_ALL': continue
  103. disable = name[:14] == 'MMGEN_DISABLE_'
  104. val = os.getenv(name) # os.getenv() returns None if env var is unset
  105. if val: # exclude empty string values; string value of '0' or 'false' sets variable to False
  106. gname = name[(6,14)[disable]:].lower()
  107. setattr(g,gname,set_for_type(val,getattr(g,gname),name,disable))
  108. def common_opts_code(s):
  109. from .protocol import CoinProtocol
  110. return s.format(
  111. pnm=g.proj_name,pn=g.proto.name,dn=g.proto.daemon_name,
  112. cu_dfl=g.coin,
  113. cu_all=' '.join(CoinProtocol.coins) )
  114. def show_common_opts_diff():
  115. def common_opts_data_to_list():
  116. for l in common_opts_data['text'].splitlines():
  117. if l.startswith('--,'):
  118. yield l.split()[1].split('=')[0][2:].replace('-','_')
  119. def do_fmt(set_data):
  120. return fmt_list(['--'+s.replace('_','-') for s in set_data],fmt='col',indent=' ')
  121. a = set(g.common_opts)
  122. b = set(common_opts_data_to_list())
  123. m1 = 'g.common_opts - common_opts_data:\n {}\n'
  124. msg(m1.format(do_fmt(a-b) if a-b else 'None'))
  125. m2 = 'common_opts_data - g.common_opts (these do not set global var):\n{}\n'
  126. msg(m2.format(do_fmt(b-a)))
  127. m3 = 'common_opts_data ^ g.common_opts (these set global var):\n{}\n'
  128. msg(m3.format(do_fmt(b.intersection(a))))
  129. sys.exit(0)
  130. common_opts_data = {
  131. # Most but not all of these set the corresponding global var
  132. # View differences with show_common_opts_diff()
  133. 'text': """
  134. --, --accept-defaults Accept defaults at all prompts
  135. --, --coin=c Choose coin unit. Default: {cu_dfl}. Options: {cu_all}
  136. --, --token=t Specify an ERC20 token by address or symbol
  137. --, --color=0|1 Disable or enable color output
  138. --, --force-256-color Force 256-color output when color is enabled
  139. --, --daemon-data-dir=d Specify coin daemon data directory location 'd'
  140. --, --data-dir=d Specify {pnm} data directory location 'd'
  141. --, --no-license Suppress the GPL license prompt
  142. --, --rpc-host=h Communicate with {dn} running on host 'h'
  143. --, --rpc-port=p Communicate with {dn} listening on port 'p'
  144. --, --rpc-user=user Override 'rpcuser' in {pn}.conf
  145. --, --rpc-password=pass Override 'rpcpassword' in {pn}.conf
  146. --, --monero-wallet-rpc-host=host Override 'monero_wallet_rpc_host' in mmgen.cfg
  147. --, --monero-wallet-rpc-user=user Override 'monero_wallet_rpc_user' in mmgen.cfg
  148. --, --monero-wallet-rpc-password=pass Override 'monero_wallet_rpc_password' in mmgen.cfg
  149. --, --regtest=0|1 Disable or enable regtest mode
  150. --, --testnet=0|1 Disable or enable testnet
  151. --, --skip-cfg-file Skip reading the configuration file
  152. --, --version Print version information and exit
  153. --, --bob Switch to user "Bob" in MMGen regtest setup
  154. --, --alice Switch to user "Alice" in MMGen regtest setup
  155. """,
  156. 'code': common_opts_code
  157. }
  158. def init(opts_data,add_opts=[],opt_filter=None,parse_only=False):
  159. opts_data['text']['long_options'] = common_opts_data['text']
  160. # po: user_opts cmd_args opts skipped_opts
  161. po = mmgen.share.Opts.parse_opts(opts_data,opt_filter=opt_filter,parse_only=parse_only)
  162. if parse_only:
  163. return po
  164. if g.debug_opts:
  165. opt_preproc_debug(po)
  166. # Copy parsed opts to opt, setting values to None if not set by user
  167. for o in set(
  168. po.opts
  169. + po.skipped_opts
  170. + tuple(add_opts)
  171. + g.required_opts
  172. + g.common_opts ):
  173. setattr(opt,o,po.user_opts[o] if o in po.user_opts else None)
  174. # Make this available to usage()
  175. global usage_txt
  176. usage_txt = opts_data['text']['usage']
  177. if opt.version:
  178. Die(0,fmt("""
  179. {pn} version {g.version}
  180. Part of the {g.proj_name} suite, an online/offline cryptocurrency wallet for the
  181. command line. Copyright (C){g.Cdates} {g.author} {g.email}
  182. """.format(g=g,pn=g.prog_name.upper()),indent=' ').rstrip())
  183. # === begin global var initialization === #
  184. # NB: user opt --data-dir is actually g.data_dir_root
  185. # cfg file is in g.data_dir_root, wallet and other data are in g.data_dir
  186. # We must set g.data_dir_root from --data-dir before processing cfg file
  187. g.data_dir_root = (
  188. os.path.normpath(os.path.expanduser(opt.data_dir))
  189. if opt.data_dir else
  190. os.path.join(g.home_dir,'.'+g.proj_name.lower()) )
  191. check_or_create_dir(g.data_dir_root)
  192. init_term_and_color()
  193. if not opt.skip_cfg_file:
  194. from .cfg import cfg_file
  195. cfg_file('sample') # check for changes in system template file
  196. override_globals_from_cfg_file(cfg_file('usr'))
  197. override_globals_from_env()
  198. # Set globals from opts, setting type from original global value
  199. # Do here, before opts are set from globals below
  200. # g.coin is finalized here
  201. for k in (g.common_opts + g.opt_sets_global):
  202. if hasattr(opt,k):
  203. val = getattr(opt,k)
  204. if val != None:
  205. setattr(g,k,set_for_type(val,getattr(g,k),'--'+k))
  206. g.coin = g.coin.upper() # allow user to use lowercase
  207. g.dcoin = g.coin # the display coin; for ERC20 tokens, g.dcoin is set to the token symbol
  208. if g.regtest: # These are equivalent for now
  209. g.testnet = True
  210. g.network = 'testnet' if g.testnet else 'mainnet'
  211. from .protocol import init_genonly_altcoins,CoinProtocol
  212. altcoin_trust_level = init_genonly_altcoins(g.coin)
  213. # g.testnet is finalized, so we can set g.proto
  214. g.proto = CoinProtocol(g.coin,g.testnet)
  215. # this could have been set from long opts
  216. if g.daemon_data_dir:
  217. g.proto.daemon_data_dir = g.daemon_data_dir
  218. # g.proto is set, so we can set g.data_dir
  219. g.data_dir = os.path.normpath(os.path.join(g.data_dir_root,g.proto.data_subdir))
  220. # Set user opts from globals:
  221. # - if opt is unset, set it to global value
  222. # - if opt is set, convert its type to that of global value
  223. opt.set_by_user = []
  224. for k in g.global_sets_opt:
  225. if hasattr(opt,k) and getattr(opt,k) != None:
  226. setattr(opt,k,set_for_type(getattr(opt,k),getattr(g,k),'--'+k))
  227. opt.set_by_user.append(k)
  228. else:
  229. setattr(opt,k,getattr(g,k))
  230. if opt.show_hash_presets:
  231. _show_hash_presets()
  232. sys.exit(0)
  233. if opt.verbose:
  234. opt.quiet = None
  235. if g.bob or g.alice:
  236. g.testnet = True
  237. g.regtest = True
  238. g.proto = CoinProtocol(g.coin,g.testnet)
  239. g.rpc_host = 'localhost'
  240. g.data_dir = os.path.join(g.data_dir_root,'regtest',g.coin.lower(),('alice','bob')[g.bob])
  241. from .regtest import MMGenRegtest
  242. g.rpc_user = MMGenRegtest.rpc_user
  243. g.rpc_password = MMGenRegtest.rpc_password
  244. g.rpc_port = MMGenRegtest(g.coin).d.rpc_port
  245. # === end global var initialization === #
  246. die_on_incompatible_opts(g.incompatible_opts)
  247. # print help screen only after global vars are initialized:
  248. if getattr(opt,'help',None) or getattr(opt,'longhelp',None):
  249. if not 'code' in opts_data:
  250. opts_data['code'] = {}
  251. opts_data['code']['long_options'] = common_opts_data['code']
  252. mmgen.share.Opts.print_help(po,opts_data,opt_filter) # exits
  253. check_or_create_dir(g.data_dir) # g.data_dir is finalized, so we can create it
  254. # Check user-set opts without modifying them
  255. check_usr_opts(po.user_opts)
  256. # Check all opts against g.autoset_opts, setting if unset
  257. check_and_set_autoset_opts()
  258. if g.debug and g.prog_name != 'test.py':
  259. opt.verbose,opt.quiet = (True,None)
  260. if g.debug_opts:
  261. opt_postproc_debug()
  262. warn_altcoins(g.coin,altcoin_trust_level)
  263. # We don't need this data anymore
  264. del mmgen.share.Opts
  265. for k in ('text','notes','code'):
  266. if k in opts_data:
  267. del opts_data[k]
  268. return po.cmd_args
  269. def opt_is_tx_fee(key,val,desc): # 'key' must remain a placeholder
  270. # contract data or non-standard startgas: disable fee checking
  271. if hasattr(opt,'contract_data') and opt.contract_data:
  272. return
  273. if hasattr(opt,'tx_gas') and opt.tx_gas:
  274. return
  275. from .tx import MMGenTX
  276. tx = MMGenTX(offline=True)
  277. # Size of 224 is just a ball-park figure to eliminate the most extreme cases at startup
  278. # This check will be performed again once we know the true size
  279. ret = tx.process_fee_spec(val,224,on_fail='return')
  280. if ret == False:
  281. raise UserOptError('{!r}: invalid {}\n(not a {} amount or {} specification)'.format(
  282. val,desc,g.coin.upper(),tx.rel_fee_desc))
  283. if ret > g.proto.max_tx_fee:
  284. raise UserOptError('{!r}: invalid {}\n({} > max_tx_fee ({} {}))'.format(
  285. val,desc,ret.fmt(fs='1.1'),g.proto.max_tx_fee,g.coin.upper()))
  286. def check_usr_opts(usr_opts): # Raises an exception if any check fails
  287. def opt_splits(val,sep,n,desc):
  288. sepword = 'comma' if sep == ',' else 'colon' if sep == ':' else repr(sep)
  289. try:
  290. l = val.split(sep)
  291. except:
  292. raise UserOptError('{!r}: invalid {} (not {}-separated list)'.format(val,desc,sepword))
  293. if len(l) != n:
  294. raise UserOptError('{!r}: invalid {} ({} {}-separated items required)'.format(val,desc,n,sepword))
  295. def opt_compares(val,op_str,target,desc,desc2=''):
  296. import operator as o
  297. op_f = { '<':o.lt, '<=':o.le, '>':o.gt, '>=':o.ge, '=':o.eq }[op_str]
  298. if not op_f(val,target):
  299. d2 = desc2 + ' ' if desc2 else ''
  300. raise UserOptError('{}: invalid {} ({}not {} {})'.format(val,desc,d2,op_str,target))
  301. def opt_is_int(val,desc):
  302. if not is_int(val):
  303. raise UserOptError('{!r}: invalid {} (not an integer)'.format(val,desc))
  304. def opt_is_float(val,desc):
  305. try:
  306. float(val)
  307. except:
  308. raise UserOptError('{!r}: invalid {} (not a floating-point number)'.format(val,desc))
  309. def opt_is_in_list(val,tlist,desc):
  310. if val not in tlist:
  311. q,sep = (('',','),("'","','"))[type(tlist[0]) == str]
  312. fs = '{q}{v}{q}: invalid {w}\nValid choices: {q}{o}{q}'
  313. raise UserOptError(fs.format(v=val,w=desc,q=q,o=sep.join(map(str,sorted(tlist)))))
  314. def opt_unrecognized(key,val,desc='value'):
  315. raise UserOptError('{!r}: unrecognized {} for option {!r}'.format(val,desc,fmt_opt(key)))
  316. def opt_display(key,val='',beg='For selected',end=':\n'):
  317. s = '{}={}'.format(fmt_opt(key),val) if val else fmt_opt(key)
  318. msg_r('{} option {!r}{}'.format(beg,s,end))
  319. def chk_in_fmt(key,val,desc):
  320. from .wallet import Wallet,IncogWallet,Brainwallet,IncogWalletHidden
  321. sstype = Wallet.fmt_code_to_type(val)
  322. if not sstype:
  323. opt_unrecognized(key,val)
  324. if key == 'out_fmt':
  325. p = 'hidden_incog_output_params'
  326. if sstype == IncogWalletHidden and not getattr(opt,p):
  327. m1 = 'Hidden incog format output requested. '
  328. m2 = 'You must supply a file and offset with the {!r} option'
  329. raise UserOptError(m1+m2.format(fmt_opt(p)))
  330. if issubclass(sstype,IncogWallet) and opt.old_incog_fmt:
  331. opt_display(key,val,beg='Selected',end=' ')
  332. opt_display('old_incog_fmt',beg='conflicts with',end=':\n')
  333. raise UserOptError('Export to old incog wallet format unsupported')
  334. elif issubclass(sstype,Brainwallet):
  335. raise UserOptError('Output to brainwallet format unsupported')
  336. chk_out_fmt = chk_in_fmt
  337. def chk_hidden_incog_input_params(key,val,desc):
  338. a = val.rsplit(',',1) # permit comma in filename
  339. if len(a) != 2:
  340. opt_display(key,val)
  341. raise UserOptError('Option requires two comma-separated arguments')
  342. fn,offset = a
  343. opt_is_int(offset,desc)
  344. if key == 'hidden_incog_input_params':
  345. check_infile(fn,blkdev_ok=True)
  346. key2 = 'in_fmt'
  347. else:
  348. try: os.stat(fn)
  349. except:
  350. b = os.path.dirname(fn)
  351. if b: check_outdir(b)
  352. else:
  353. check_outfile(fn,blkdev_ok=True)
  354. key2 = 'out_fmt'
  355. if hasattr(opt,key2):
  356. val2 = getattr(opt,key2)
  357. from .wallet import IncogWalletHidden
  358. if val2 and val2 not in IncogWalletHidden.fmt_codes:
  359. fs = 'Option conflict:\n {}, with\n {}={}'
  360. raise UserOptError(fs.format(fmt_opt(key),fmt_opt(key2),val2))
  361. chk_hidden_incog_output_params = chk_hidden_incog_input_params
  362. def chk_seed_len(key,val,desc):
  363. opt_is_int(val,desc)
  364. opt_is_in_list(int(val),g.seed_lens,desc)
  365. def chk_hash_preset(key,val,desc):
  366. opt_is_in_list(val,list(g.hash_presets.keys()),desc)
  367. def chk_brain_params(key,val,desc):
  368. a = val.split(',')
  369. if len(a) != 2:
  370. opt_display(key,val)
  371. raise UserOptError('Option requires two comma-separated arguments')
  372. opt_is_int(a[0],'seed length '+desc)
  373. opt_is_in_list(int(a[0]),g.seed_lens,'seed length '+desc)
  374. opt_is_in_list(a[1],list(g.hash_presets.keys()),'hash preset '+desc)
  375. def chk_usr_randchars(key,val,desc):
  376. if val == 0:
  377. return
  378. opt_is_int(val,desc)
  379. opt_compares(val,'>=',g.min_urandchars,desc)
  380. opt_compares(val,'<=',g.max_urandchars,desc)
  381. def chk_tx_fee(key,val,desc):
  382. opt_is_tx_fee(key,val,desc)
  383. def chk_tx_confs(key,val,desc):
  384. opt_is_int(val,desc)
  385. opt_compares(val,'>=',1,desc)
  386. def chk_vsize_adj(key,val,desc):
  387. opt_is_float(val,desc)
  388. ymsg('Adjusting transaction vsize by a factor of {:1.2f}'.format(float(val)))
  389. def chk_key_generator(key,val,desc):
  390. opt_compares(val,'<=',len(g.key_generators),desc)
  391. opt_compares(val,'>',0,desc)
  392. def chk_coin(key,val,desc):
  393. from .protocol import CoinProtocol
  394. opt_is_in_list(val.lower(),list(CoinProtocol.coins.keys()),'coin')
  395. def chk_rbf(key,val,desc):
  396. if not g.proto.cap('rbf'):
  397. m = '--rbf requested, but {} does not support replace-by-fee transactions'
  398. raise UserOptError(m.format(g.coin))
  399. def chk_bob(key,val,desc):
  400. m = "Regtest (Bob and Alice) mode not set up yet. Run '{}-regtest setup' to initialize."
  401. from .regtest import MMGenRegtest
  402. try:
  403. os.stat(os.path.join(MMGenRegtest(g.coin).d.datadir,'regtest','debug.log'))
  404. except:
  405. raise UserOptError(m.format(g.proj_name.lower()))
  406. chk_alice = chk_bob
  407. def chk_locktime(key,val,desc):
  408. opt_is_int(val,desc)
  409. opt_compares(int(val),'>',0,desc)
  410. def chk_token(key,val,desc):
  411. if not 'token' in g.proto.caps:
  412. raise UserOptError('Coin {!r} does not support the --token option'.format(g.coin))
  413. if len(val) == 40 and is_hex_str(val):
  414. return
  415. if len(val) > 20 or not all(s.isalnum() for s in val):
  416. raise UserOptError('{!r}: invalid parameter for --token option'.format(val))
  417. cfuncs = { k:v for k,v in locals().items() if k.startswith('chk_') }
  418. for key in usr_opts:
  419. val = getattr(opt,key)
  420. desc = 'parameter for {!r} option'.format(fmt_opt(key))
  421. if key in g.infile_opts:
  422. check_infile(val) # file exists and is readable - dies on error
  423. elif key == 'outdir':
  424. check_outdir(val) # dies on error
  425. elif 'chk_'+key in cfuncs:
  426. cfuncs['chk_'+key](key,val,desc)
  427. elif g.debug:
  428. Msg('check_usr_opts(): No test for opt {!r}'.format(key))
  429. def check_and_set_autoset_opts(): # Raises exception if any check fails
  430. def nocase_str(key,val,asd):
  431. if val.lower() in asd.choices:
  432. return True
  433. else:
  434. return 'one of'
  435. def nocase_pfx(key,val,asd):
  436. cs = [s.startswith(val.lower()) for s in asd.choices]
  437. if cs.count(True) == 1:
  438. return cs.index(True)
  439. else:
  440. return 'unique substring of'
  441. for key,asd in g.autoset_opts.items():
  442. if hasattr(opt,key):
  443. val = getattr(opt,key)
  444. if val is None:
  445. setattr(opt,key,asd.choices[0])
  446. else:
  447. ret = locals()[asd.type](key,val,asd)
  448. if type(ret) is str:
  449. m = '{!r}: invalid parameter for option --{} (not {}: {})'
  450. raise UserOptError(m.format(val,key.replace('_','-'),ret,fmt_list(asd.choices)))
  451. elif ret is True:
  452. setattr(opt,key,val)
  453. else:
  454. setattr(opt,key,asd.choices[ret])