Browse Source

opts.py: init sequence, opt checking cleanups/improvements

Testing:

  $ test/test.py opts
The MMGen Project 5 years ago
parent
commit
987dafd353
7 changed files with 473 additions and 239 deletions
  1. 8 0
      mmgen/globalvars.py
  2. 1 1
      mmgen/main_split.py
  3. 273 235
      mmgen/opts.py
  4. 3 1
      mmgen/share/Opts.py
  5. 71 0
      test/misc/opts.py
  6. 3 2
      test/test.py
  7. 114 0
      test/test_py_d/ts_opts.py

+ 8 - 0
mmgen/globalvars.py

@@ -210,6 +210,14 @@ class g(object):
 		'MMGEN_DISABLE_COLOR',
 		'MMGEN_DISABLE_MSWIN_PW_WARNING',
 	)
+	infile_opts = (
+		'keys_from_file',
+		'mmgen_keys_from_file',
+		'passwd_file',
+		'keysforaddrs',
+		'comment_file',
+		'contract_data',
+	)
 	# Auto-typechecked and auto-set opts - incompatible with global_sets_opt and opt_sets_global
 	# First value in list is the default
 	ov = namedtuple('autoset_opt_info',['type','choices'])

+ 1 - 1
mmgen/main_split.py

@@ -113,7 +113,7 @@ if opt.tx_fees:
 	for idx,g_coin in ((1,opt.other_coin),(0,g.coin)):
 		init_coin(g_coin)
 		opt.tx_fee = opt.tx_fees.split(',')[idx]
-		opts.opt_is_tx_fee(opt.tx_fee,'transaction fee') or sys.exit(1)
+		opts.opt_is_tx_fee('foo',opt.tx_fee,'transaction fee') # raises exception on error
 
 rpc_init(reinit=True)
 

+ 273 - 235
mmgen/opts.py

@@ -21,9 +21,11 @@ opts.py:  MMGen-specific options processing after generic processing by share.Op
 """
 import sys,os,stat
 
-class opt(object):
+class opt_cls(object):
 	pass
+opt = opt_cls()
 
+from mmgen.exception import UserOptError
 from mmgen.globalvars import g
 import mmgen.share.Opts
 from mmgen.util import *
@@ -36,7 +38,7 @@ def fmt_opt(o):
 
 def die_on_incompatible_opts(incompat_list):
 	for group in incompat_list:
-		bad = [k for k in opt.__dict__ if opt.__dict__[k] and k in group]
+		bad = [k for k in opt.__dict__ if k in group and getattr(opt,k) != None]
 		if len(bad) > 1:
 			die(1,'Conflicting options: {}'.format(', '.join(map(fmt_opt,bad))))
 
@@ -48,14 +50,14 @@ def _show_hash_presets():
 		msg(fs.format(i,*g.hash_presets[i]))
 	msg('N = memory usage (power of two), p = iterations (rounds)')
 
-def opt_preproc_debug(short_opts,long_opts,skipped_opts,uopts,args):
+def opt_preproc_debug(po):
 	d = (
 		('Cmdline',            ' '.join(sys.argv)),
-		('Short opts',         short_opts),
-		('Long opts',          long_opts),
-		('Skipped opts',       skipped_opts),
-		('User-selected opts', uopts),
-		('Cmd args',           args),
+		('Short opts',         po.short_opts),
+		('Long opts',          po.long_opts),
+		('Skipped opts',       po.skipped_opts),
+		('User-selected opts', po.user_opts),
+		('Cmd args',           po.cmd_args),
 	)
 	Msg('\n=== opts.py debug ===')
 	for e in d:
@@ -75,17 +77,6 @@ def opt_postproc_debug():
 		Msg('        {:<20}: {}'.format(e, getattr(g,e)))
 	Msg('\n=== end opts.py debug ===\n')
 
-def opt_postproc_initializations():
-	g.coin = g.coin.upper() # allow user to use lowercase
-	g.dcoin = g.coin # the display coin; for ERC20 tokens, g.dcoin is set to the token symbol
-
-def set_data_dir_root():
-	g.data_dir_root = os.path.normpath(os.path.expanduser(opt.data_dir)) if opt.data_dir else \
-			os.path.join(g.home_dir,'.'+g.proj_name.lower())
-
-	# mainnet and testnet share cfg file, as with Core
-	g.cfg_file = os.path.join(g.data_dir_root,'{}.cfg'.format(g.proj_name.lower()))
-
 def init_term_and_color():
 	from mmgen.term import set_terminal_vars
 	set_terminal_vars()
@@ -136,8 +127,33 @@ def common_opts_code(s):
 		cu_dfl=g.coin,
 		cu_all=' '.join(CoinProtocol.coins) )
 
+def show_common_opts_diff():
+
+	def common_opts_data_to_list():
+		for l in common_opts_data['text'].splitlines():
+			if l.startswith('--,'):
+				yield l.split()[1].split('=')[0][2:].replace('-','_')
+
+	def do_fmt(set_data):
+		return fmt_list(['--'+s.replace('_','-') for s in set_data],fmt='col',indent='   ')
+
+	a = set(g.common_opts)
+	b = set(common_opts_data_to_list())
+
+	m1 = 'g.common_opts - common_opts_data:\n   {}\n'
+	msg(m1.format(do_fmt(a-b) if a-b else 'None'))
+
+	m2 = 'common_opts_data - g.common_opts (these do not set global var):\n{}\n'
+	msg(m2.format(do_fmt(b-a)))
+
+	m3 = 'common_opts_data ^ g.common_opts (these set global var):\n{}\n'
+	msg(m3.format(do_fmt(b.intersection(a))))
+
+	sys.exit(0)
+
 common_opts_data = {
 	# Most but not all of these set the corresponding global var
+	# View differences with show_common_opts_diff()
 	'text': """
 --, --accept-defaults     Accept defaults at all prompts
 --, --coin=c              Choose coin unit. Default: {cu_dfl}. Options: {cu_all}
@@ -168,23 +184,23 @@ def init(opts_data,add_opts=[],opt_filter=None,parse_only=False):
 
 	opts_data['text']['long_options'] = common_opts_data['text']
 
-	uopts,args,short_opts,long_opts,skipped_opts = \
-		mmgen.share.Opts.parse_opts(opts_data,opt_filter=opt_filter,parse_only=parse_only)
+	# po: user_opts cmd_args short_opts long_opts skipped_opts
+	po = mmgen.share.Opts.parse_opts(opts_data,opt_filter=opt_filter,parse_only=parse_only)
 
 	if parse_only:
-		return uopts,args,short_opts,long_opts,skipped_opts
+		return po
 
 	if g.debug_opts:
-		opt_preproc_debug(short_opts,long_opts,skipped_opts,uopts,args)
+		opt_preproc_debug(po)
 
 	# Copy parsed opts to opt, setting values to None if not set by user
 	for o in  (
-			tuple(s.rstrip('=') for s in long_opts)
+			tuple(s.rstrip('=') for s in po.long_opts)
 			+ tuple(add_opts)
-			+ tuple(skipped_opts)
+			+ tuple(po.skipped_opts)
 			+ g.required_opts
 			+ g.common_opts ):
-		setattr(opt,o,uopts[o] if o in uopts else None)
+		setattr(opt,o,po.user_opts[o] if o in po.user_opts else None)
 
 	# Make this available to usage()
 	global usage_txt
@@ -197,17 +213,16 @@ def init(opts_data,add_opts=[],opt_filter=None,parse_only=False):
 			command line.  Copyright (C){g.Cdates} {g.author} {g.email}
 		""".format(g=g,pn=g.prog_name.upper()),indent='    ').rstrip())
 
-	if os.getenv('MMGEN_DEBUG_ALL'):
-		for name in g.env_opts:
-			if name[:11] == 'MMGEN_DEBUG':
-				os.environ[name] = '1'
-
 	# === begin global var initialization === #
 
 	# NB: user opt --data-dir is actually g.data_dir_root
 	# cfg file is in g.data_dir_root, wallet and other data are in g.data_dir
-	# We must set g.data_dir_root and g.cfg_file from cmdline before processing cfg file
-	set_data_dir_root()
+	# We must set g.data_dir_root from --data-dir before processing cfg file
+	g.data_dir_root = (
+			os.path.normpath(os.path.expanduser(opt.data_dir))
+		if opt.data_dir else
+			os.path.join(g.home_dir,'.'+g.proj_name.lower()) )
+
 	check_or_create_dir(g.data_dir_root)
 
 	init_term_and_color()
@@ -228,12 +243,16 @@ def init(opts_data,add_opts=[],opt_filter=None,parse_only=False):
 			if val != None:
 				setattr(g,k,set_for_type(val,getattr(g,k),'--'+k))
 
-	if g.regtest: g.testnet = True # These are equivalent for now
+	g.coin = g.coin.upper() # allow user to use lowercase
+	g.dcoin = g.coin # the display coin; for ERC20 tokens, g.dcoin is set to the token symbol
+
+	if g.regtest: # These are equivalent for now
+		g.testnet = True
 
 	g.network = 'testnet' if g.testnet else 'mainnet'
 
 	from mmgen.protocol import init_genonly_altcoins,CoinProtocol
-	altcoin_trust_level = init_genonly_altcoins(opt.coin or 'btc')
+	altcoin_trust_level = init_genonly_altcoins(g.coin)
 
 	# g.testnet is finalized, so we can set g.proto
 	g.proto = CoinProtocol(g.coin,g.testnet)
@@ -250,11 +269,11 @@ def init(opts_data,add_opts=[],opt_filter=None,parse_only=False):
 	# - if opt is set, convert its type to that of global value
 	opt.set_by_user = []
 	for k in g.global_sets_opt:
-		if k in opt.__dict__ and getattr(opt,k) != None:
+		if hasattr(opt,k) and getattr(opt,k) != None:
 			setattr(opt,k,set_for_type(getattr(opt,k),getattr(g,k),'--'+k))
 			opt.set_by_user.append(k)
 		else:
-			setattr(opt,k,g.__dict__[k])
+			setattr(opt,k,getattr(g,k))
 
 	if opt.show_hash_presets:
 		_show_hash_presets()
@@ -263,16 +282,6 @@ def init(opts_data,add_opts=[],opt_filter=None,parse_only=False):
 	if opt.verbose:
 		opt.quiet = None
 
-	die_on_incompatible_opts(g.incompatible_opts)
-
-	opt_postproc_initializations()
-
-	if opts_data['do_help']: # print help screen only after global vars are initialized
-		if not 'code' in opts_data:
-			opts_data['code'] = {}
-		opts_data['code']['long_options'] = common_opts_data['code']
-		mmgen.share.Opts.print_help(opts_data,opt_filter) # exits
-
 	if g.bob or g.alice:
 		g.testnet = True
 		g.regtest = True
@@ -284,20 +293,23 @@ def init(opts_data,add_opts=[],opt_filter=None,parse_only=False):
 		g.rpc_password = MMGenRegtest.rpc_password
 		g.rpc_port = MMGenRegtest(g.coin).d.rpc_port
 
-	check_or_create_dir(g.data_dir) # g.data_dir is finalized, so now we can do this
+	# === end global var initialization === #
 
-	# Check user-set opts without modifying them
-	if not check_opts(uopts):
-		die(1,'Options checking failed')
+	if opts_data['do_help']: # print help screen only after global vars are initialized
+		if not 'code' in opts_data:
+			opts_data['code'] = {}
+		opts_data['code']['long_options'] = common_opts_data['code']
+		mmgen.share.Opts.print_help(opts_data,opt_filter) # exits
+
+	die_on_incompatible_opts(g.incompatible_opts)
+
+	check_or_create_dir(g.data_dir) # g.data_dir is finalized, so we can create it
 
-	# Check user-set opts against g.autoset_opts, setting opt if unset:
-	if not check_opts2(uopts):
-		die(1,'Options checking failed')
+	# Check user-set opts without modifying them
+	check_usr_opts(po.user_opts)
 
-	if hasattr(g,'cfg_options_changed'):
-		ymsg("Warning: config file options have changed! See '{}' for details".format(g.cfg_file+'.sample'))
-		if not g.test_suite:
-			my_raw_input('Hit ENTER to continue: ')
+	# Check all opts against g.autoset_opts, setting if unset
+	check_and_set_autoset_opts()
 
 	if g.debug and g.prog_name != 'test.py':
 		opt.verbose,opt.quiet = (True,None)
@@ -308,211 +320,237 @@ def init(opts_data,add_opts=[],opt_filter=None,parse_only=False):
 	warn_altcoins(g.coin,altcoin_trust_level)
 
 	# We don't need this data anymore
-	del mmgen.share.Opts, opts_data
+	del mmgen.share.Opts
+	for k in ('text','notes','code'):
+		if k in opts_data:
+			del opts_data[k]
+
+	return po.cmd_args
 
-	return args
+def opt_is_tx_fee(key,val,desc): # 'key' must remain a placeholder
+
+	# contract data or non-standard startgas: disable fee checking
+	if hasattr(opt,'contract_data') and opt.contract_data:
+		return
+	if hasattr(opt,'tx_gas') and opt.tx_gas:
+		return
 
-def opt_is_tx_fee(val,desc):
 	from mmgen.tx import MMGenTX
 	tx = MMGenTX(offline=True)
-	# TODO: size is just a guess; do this check after parsing tx file
+	# Size of 224 is just a ball-park figure to eliminate the most extreme cases at startup
+	# This check will be performed again once we know the true size
 	ret = tx.process_fee_spec(val,224,on_fail='return')
-	# Non-standard startgas: disable fee checking
-	if hasattr(opt,'contract_data') and opt.contract_data: ret = None
-	if hasattr(opt,'tx_gas') and opt.tx_gas:               ret = None
+
 	if ret == False:
-		msg("'{}': invalid {}\n(not a {} amount or {} specification)".format(
+		raise UserOptError('{!r}: invalid {}\n(not a {} amount or {} specification)'.format(
 				val,desc,g.coin.upper(),tx.rel_fee_desc))
-	elif ret != None and ret > g.proto.max_tx_fee:
-		msg("'{}': invalid {}\n({} > max_tx_fee ({} {}))".format(
-				val,desc,ret.fmt(fs='1.1'),g.proto.max_tx_fee,g.coin.upper()))
-	else:
-		return True
-	return False
 
-def check_opts2(usr_opts): # Returns false if any check fails
-
-	for key in [e for e in opt.__dict__ if not e.startswith('__')]:
-		if key in g.autoset_opts:
-			val = getattr(opt,key)
-			d = g.autoset_opts[key]
-			if d[0] == 'nocase_str':
-				if val == None:
-					setattr(opt,key,d[1][0])
-				elif val.lower() not in d[1]:
-					m = "{!r}: invalid parameter for option --{} (valid choices: '{}')"
-					msg(m.format(val,key.replace('_','-'),"', '".join(d[1])))
-					return False
-
-	return True
+	if ret > g.proto.max_tx_fee:
+		raise UserOptError('{!r}: invalid {}\n({} > max_tx_fee ({} {}))'.format(
+				val,desc,ret.fmt(fs='1.1'),g.proto.max_tx_fee,g.coin.upper()))
 
-def check_opts(usr_opts): # Returns false if any check fails
+def check_usr_opts(usr_opts): # Raises an exception if any check fails
 
 	def opt_splits(val,sep,n,desc):
-		sepword = 'comma' if sep == ',' else 'colon' if sep == ':' else "'{}'".format(sep)
-		try: l = val.split(sep)
+		sepword = 'comma' if sep == ',' else 'colon' if sep == ':' else repr(sep)
+		try:
+			l = val.split(sep)
 		except:
-			msg("'{}': invalid {} (not {}-separated list)".format(val,desc,sepword))
-			return False
+			raise UserOptError('{!r}: invalid {} (not {}-separated list)'.format(val,desc,sepword))
 
-		if len(l) == n: return True
-		else:
-			msg("'{}': invalid {} ({} {}-separated items required)".format(val,desc,n,sepword))
-			return False
+		if len(l) != n:
+			raise UserOptError('{!r}: invalid {} ({} {}-separated items required)'.format(val,desc,n,sepword))
 
-	def opt_compares(val,op_str,target,desc,what=''):
+	def opt_compares(val,op_str,target,desc,desc2=''):
 		import operator as o
 		op_f = { '<':o.lt, '<=':o.le, '>':o.gt, '>=':o.ge, '=':o.eq }[op_str]
-		if what: what += ' '
 		if not op_f(val,target):
-			msg('{}: invalid {} ({}not {} {})'.format(val,desc,what,op_str,target))
-			return False
-		return True
+			d2 = desc2 + ' ' if desc2 else ''
+			raise UserOptError('{}: invalid {} ({}not {} {})'.format(val,desc,d2,op_str,target))
 
 	def opt_is_int(val,desc):
-		try: int(val)
-		except:
-			msg("'{}': invalid {} (not an integer)".format(val,desc))
-			return False
-		return True
+		if not is_int(val):
+			raise UserOptError('{!r}: invalid {} (not an integer)'.format(val,desc))
 
 	def opt_is_float(val,desc):
-		try: float(val)
+		try:
+			float(val)
 		except:
-			msg("'{}': invalid {} (not a floating-point number)".format(val,desc))
-			return False
-		return True
+			raise UserOptError('{!r}: invalid {} (not a floating-point number)'.format(val,desc))
 
-	def opt_is_in_list(val,lst,desc):
-		if val not in lst:
-			q,sep = (('',','),("'","','"))[type(lst[0]) == str]
+	def opt_is_in_list(val,tlist,desc):
+		if val not in tlist:
+			q,sep = (('',','),("'","','"))[type(tlist[0]) == str]
 			fs = '{q}{v}{q}: invalid {w}\nValid choices: {q}{o}{q}'
-			msg(fs.format(v=val,w=desc,q=q,o=sep.join(map(str,sorted(lst)))))
-			return False
-		return True
+			raise UserOptError(fs.format(v=val,w=desc,q=q,o=sep.join(map(str,sorted(tlist)))))
 
-	def opt_unrecognized(key,val,desc):
-		msg("'{}': unrecognized {} for option '{}'".format(val,desc,fmt_opt(key)))
-		return False
+	def opt_unrecognized(key,val,desc='value'):
+		raise UserOptError('{!r}: unrecognized {} for option {!r}'.format(val,desc,fmt_opt(key)))
 
 	def opt_display(key,val='',beg='For selected',end=':\n'):
 		s = '{}={}'.format(fmt_opt(key),val) if val else fmt_opt(key)
-		msg_r("{} option '{}'{}".format(beg,s,end))
-
-	global opt
-	for key,val in [(k,getattr(opt,k)) for k in usr_opts]:
-
-		desc = "parameter for '{}' option".format(fmt_opt(key))
-
-		# Check for file existence and readability
-		if key in ('keys_from_file','mmgen_keys_from_file',
-				'passwd_file','keysforaddrs','comment_file'):
-			check_infile(val)  # exits on error
-			continue
-
-		if key == 'outdir':
-			check_outdir(val)  # exits on error
-# 		# NEW
-		elif key in ('in_fmt','out_fmt'):
-			from mmgen.seed import SeedSource,IncogWallet,Brainwallet,IncogWalletHidden
-			sstype = SeedSource.fmt_code_to_type(val)
-			if not sstype:
-				return opt_unrecognized(key,val,'format code')
-			if key == 'out_fmt':
-				p = 'hidden_incog_output_params'
-				if sstype == IncogWalletHidden and not getattr(opt,p):
-					m1 = 'Hidden incog format output requested.  '
-					m2 = "You must supply a file and offset with the '{}' option"
-					die(1,m1+m2.format(fmt_opt(p)))
-				if issubclass(sstype,IncogWallet) and opt.old_incog_fmt:
-					opt_display(key,val,beg='Selected',end=' ')
-					opt_display('old_incog_fmt',beg='conflicts with',end=':\n')
-					die(1,'Export to old incog wallet format unsupported')
-				elif issubclass(sstype,Brainwallet):
-					die(1,'Output to brainwallet format unsupported')
-		elif key in ('hidden_incog_input_params','hidden_incog_output_params'):
-			a = val.split(',')
-			if len(a) < 2:
-				opt_display(key,val)
-				msg('Option requires two comma-separated arguments')
-				return False
-			fn,ofs = ','.join(a[:-1]),a[-1] # permit comma in filename
-			if not opt_is_int(ofs,desc): return False
-			if key == 'hidden_incog_input_params':
-				check_infile(fn,blkdev_ok=True)
-				key2 = 'in_fmt'
+		msg_r('{} option {!r}{}'.format(beg,s,end))
+
+	def chk_in_fmt(key,val,desc):
+		from mmgen.seed import SeedSource,IncogWallet,Brainwallet,IncogWalletHidden
+		sstype = SeedSource.fmt_code_to_type(val)
+		if not sstype:
+			opt_unrecognized(key,val)
+		if key == 'out_fmt':
+			p = 'hidden_incog_output_params'
+			if sstype == IncogWalletHidden and not getattr(opt,p):
+				m1 = 'Hidden incog format output requested.  '
+				m2 = 'You must supply a file and offset with the {!r} option'
+				raise UserOptError(m1+m2.format(fmt_opt(p)))
+			if issubclass(sstype,IncogWallet) and opt.old_incog_fmt:
+				opt_display(key,val,beg='Selected',end=' ')
+				opt_display('old_incog_fmt',beg='conflicts with',end=':\n')
+				raise UserOptError('Export to old incog wallet format unsupported')
+			elif issubclass(sstype,Brainwallet):
+				raise UserOptError('Output to brainwallet format unsupported')
+
+	chk_out_fmt = chk_in_fmt
+
+	def chk_hidden_incog_input_params(key,val,desc):
+		a = val.rsplit(',',1) # permit comma in filename
+		if len(a) != 2:
+			opt_display(key,val)
+			raise UserOptError('Option requires two comma-separated arguments')
+
+		fn,offset = a
+		opt_is_int(offset,desc)
+
+		if key == 'hidden_incog_input_params':
+			check_infile(fn,blkdev_ok=True)
+			key2 = 'in_fmt'
+		else:
+			try: os.stat(fn)
+			except:
+				b = os.path.dirname(fn)
+				if b: check_outdir(b)
 			else:
-				try: os.stat(fn)
-				except:
-					b = os.path.dirname(fn)
-					if b: check_outdir(b)
-				else: check_outfile(fn,blkdev_ok=True)
-				key2 = 'out_fmt'
-			if hasattr(opt,key2):
-				val2 = getattr(opt,key2)
-				from mmgen.seed import IncogWalletHidden
-				if val2 and val2 not in IncogWalletHidden.fmt_codes:
-					fs = 'Option conflict:\n  {}, with\n  {}={}'
-					die(1,fs.format(fmt_opt(key),fmt_opt(key2),val2))
-		elif key == 'seed_len':
-			if not opt_is_int(val,desc): return False
-			if not opt_is_in_list(int(val),g.seed_lens,desc): return False
-		elif key == 'hash_preset':
-			if not opt_is_in_list(val,list(g.hash_presets.keys()),desc): return False
-		elif key == 'brain_params':
-			a = val.split(',')
-			if len(a) != 2:
-				opt_display(key,val)
-				msg('Option requires two comma-separated arguments')
-				return False
-			d = 'seed length ' + desc
-			if not opt_is_int(a[0],d): return False
-			if not opt_is_in_list(int(a[0]),g.seed_lens,d): return False
-			d = 'hash preset ' + desc
-			if not opt_is_in_list(a[1],list(g.hash_presets.keys()),d): return False
-		elif key == 'usr_randchars':
-			if val == 0: continue
-			if not opt_is_int(val,desc): return False
-			if not opt_compares(val,'>=',g.min_urandchars,desc): return False
-			if not opt_compares(val,'<=',g.max_urandchars,desc): return False
-		elif key == 'tx_fee':
-			if not opt_is_tx_fee(val,desc): return False
-		elif key == 'tx_confs':
-			if not opt_is_int(val,desc): return False
-			if not opt_compares(val,'>=',1,desc): return False
-		elif key == 'vsize_adj':
-			if not opt_is_float(val,desc): return False
-			ymsg('Adjusting transaction vsize by a factor of {:1.2f}'.format(float(val)))
-		elif key == 'key_generator':
-			if not opt_compares(val,'<=',len(g.key_generators),desc): return False
-			if not opt_compares(val,'>',0,desc): return False
-		elif key == 'coin':
-			from mmgen.protocol import CoinProtocol
-			if not opt_is_in_list(val.lower(),list(CoinProtocol.coins.keys()),'coin'): return False
-		elif key == 'rbf':
-			if not g.proto.cap('rbf'):
-				msg('--rbf requested, but {} does not support replace-by-fee transactions'.format(g.coin))
-				return False
-		elif key in ('bob','alice'):
-			m = "Regtest (Bob and Alice) mode not set up yet.  Run '{}-regtest setup' to initialize."
-			from mmgen.regtest import MMGenRegtest
-			try: os.stat(os.path.join(MMGenRegtest(g.coin).d.datadir,'regtest','debug.log'))
-			except: die(1,m.format(g.proj_name.lower()))
-		elif key == 'locktime':
-			if not opt_is_int(val,desc): return False
-			if not opt_compares(int(val),'>',0,desc): return False
-		elif key == 'token':
-			if not 'token' in g.proto.caps:
-				msg("Coin '{}' does not support the --token option".format(g.coin))
-				return False
-			elif len(val) == 40 and is_hex_str(val):
-				pass
-			elif len(val) > 20 or not all(s.isalnum() for s in val):
-				msg("u'{}: invalid parameter for --token option".format(val))
-				return False
-		elif key == 'contract_data':
-			check_infile(val)
+				check_outfile(fn,blkdev_ok=True)
+			key2 = 'out_fmt'
+
+		if hasattr(opt,key2):
+			val2 = getattr(opt,key2)
+			from mmgen.seed import IncogWalletHidden
+			if val2 and val2 not in IncogWalletHidden.fmt_codes:
+				fs = 'Option conflict:\n  {}, with\n  {}={}'
+				raise UserOptError(fs.format(fmt_opt(key),fmt_opt(key2),val2))
+
+	chk_hidden_incog_output_params = chk_hidden_incog_input_params
+
+	def chk_seed_len(key,val,desc):
+		opt_is_int(val,desc)
+		opt_is_in_list(int(val),g.seed_lens,desc)
+
+	def chk_hash_preset(key,val,desc):
+		opt_is_in_list(val,list(g.hash_presets.keys()),desc)
+
+	def chk_brain_params(key,val,desc):
+		a = val.split(',')
+		if len(a) != 2:
+			opt_display(key,val)
+			raise UserOptError('Option requires two comma-separated arguments')
+		opt_is_int(a[0],'seed length '+desc)
+		opt_is_in_list(int(a[0]),g.seed_lens,'seed length '+desc)
+		opt_is_in_list(a[1],list(g.hash_presets.keys()),'hash preset '+desc)
+
+	def chk_usr_randchars(key,val,desc):
+		if val == 0:
+			return
+		opt_is_int(val,desc)
+		opt_compares(val,'>=',g.min_urandchars,desc)
+		opt_compares(val,'<=',g.max_urandchars,desc)
+
+	def chk_tx_fee(key,val,desc):
+		opt_is_tx_fee(key,val,desc)
+
+	def chk_tx_confs(key,val,desc):
+		opt_is_int(val,desc)
+		opt_compares(val,'>=',1,desc)
+
+	def chk_vsize_adj(key,val,desc):
+		opt_is_float(val,desc)
+		ymsg('Adjusting transaction vsize by a factor of {:1.2f}'.format(float(val)))
+
+	def chk_key_generator(key,val,desc):
+		opt_compares(val,'<=',len(g.key_generators),desc)
+		opt_compares(val,'>',0,desc)
+
+	def chk_coin(key,val,desc):
+		from mmgen.protocol import CoinProtocol
+		opt_is_in_list(val.lower(),list(CoinProtocol.coins.keys()),'coin')
+
+	def chk_rbf(key,val,desc):
+		if not g.proto.cap('rbf'):
+			m = '--rbf requested, but {} does not support replace-by-fee transactions'
+			raise UserOptError(m.format(g.coin))
+
+	def chk_bob(key,val,desc):
+		m = "Regtest (Bob and Alice) mode not set up yet.  Run '{}-regtest setup' to initialize."
+		from mmgen.regtest import MMGenRegtest
+		try:
+			os.stat(os.path.join(MMGenRegtest(g.coin).d.datadir,'regtest','debug.log'))
+		except:
+			raise UserOptError(m.format(g.proj_name.lower()))
+
+	chk_alice = chk_bob
+
+	def chk_locktime(key,val,desc):
+		opt_is_int(val,desc)
+		opt_compares(int(val),'>',0,desc)
+
+	def chk_token(key,val,desc):
+		if not 'token' in g.proto.caps:
+			raise UserOptError('Coin {!r} does not support the --token option'.format(g.coin))
+		if len(val) == 40 and is_hex_str(val):
+			return
+		if len(val) > 20 or not all(s.isalnum() for s in val):
+			raise UserOptError('{!r}: invalid parameter for --token option'.format(val))
+
+	cfuncs = { k:v for k,v in locals().items() if k.startswith('chk_') }
+
+	for key in usr_opts:
+		val = getattr(opt,key)
+		desc = 'parameter for {!r} option'.format(fmt_opt(key))
+
+		if key in g.infile_opts:
+			check_infile(val) # file exists and is readable - dies on error
+		elif key == 'outdir':
+			check_outdir(val) # dies on error
+		elif 'chk_'+key in cfuncs:
+			cfuncs['chk_'+key](key,val,desc)
+		elif g.debug:
+			Msg('check_usr_opts(): No test for opt {!r}'.format(key))
+
+def check_and_set_autoset_opts(): # Raises exception if any check fails
+
+	def nocase_str(key,val,asd):
+		if val.lower() in asd.choices:
+			return True
 		else:
-			if g.debug: Msg("check_opts(): No test for opt '{}'".format(key))
+			return 'one of'
 
-	return True
+	def nocase_pfx(key,val,asd):
+		cs = [s.startswith(val.lower()) for s in asd.choices]
+		if cs.count(True) == 1:
+			return cs.index(True)
+		else:
+			return 'unique substring of'
+
+	for key,asd in g.autoset_opts.items():
+		if hasattr(opt,key):
+			val = getattr(opt,key)
+			if val is None:
+				setattr(opt,key,asd.choices[0])
+			else:
+				ret = locals()[asd.type](key,val,asd)
+				if type(ret) is str:
+					m = '{!r}: invalid parameter for option --{} (not {}: {})'
+					raise UserOptError(m.format(val,key.replace('_','-'),ret,fmt_list(asd.choices)))
+				elif ret is True:
+					setattr(opt,key,val)
+				else:
+					setattr(opt,key,asd.choices[ret])

+ 3 - 1
mmgen/share/Opts.py

@@ -136,4 +136,6 @@ def parse_opts(opts_data,opt_filter=None,parse_only=False):
 
 	opts,args = process_opts(opts_data,short_opts,long_opts)
 
-	return opts,args,short_opts,long_opts,skipped_opts
+	from collections import namedtuple
+	ret = namedtuple('parsed_cmd_opts',['user_opts','cmd_args','short_opts','long_opts','skipped_opts'])
+	return ret(opts,args,short_opts,long_opts,skipped_opts)

+ 71 - 0
test/misc/opts.py

@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+
+from mmgen.common import *
+
+opts_data = {
+	'sets': [('print_checksum',True,'quiet',True)],
+	'text': {
+		'desc': 'Opts test',
+		'usage':'[args] [opts]',
+		'options': """
+-h, --help            Print this help message
+--, --longhelp        Print help message for long options (common options)
+-i, --in-fmt=      f  Input is from wallet format 'f'
+-d, --outdir=      d  Use outdir 'd'
+-C, --print-checksum  Print a checksum
+-E, --fee-estimate-mode=M Specify the network fee estimate mode.
+-H, --hidden-incog-input-params=f,o  Read hidden incognito data from file
+                      'f' at offset 'o' (comma-separated)
+-K, --key-generator=m Use method 'm' for public key generation
+                      Options: {kgs} (default: {kg})
+-l, --seed-len=    l  Specify wallet seed length of 'l' bits.
+-L, --label=       l  Specify a label 'l' for output wallet
+-m, --keep-label      Reuse label of input wallet for output wallet
+-p, --hash-preset= p  Use the scrypt hash parameters defined by preset 'p'
+-P, --passwd-file= f  Get wallet passphrase from file 'f'
+-u, --subseeds=    n  The number of subseed pairs to scan for
+-q, --quiet           Be quieter
+-v, --verbose         Be more verbose
+""",
+	'notes': """
+
+                           NOTES FOR THIS COMMAND
+	{nn}
+"""
+	},
+	'code': {
+		'options': lambda s: s.format(
+			kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
+			kg=g.key_generator,
+			g=g,
+		),
+		'notes': lambda s: s.format(nn='a note'),
+	}
+}
+
+cmd_args = opts.init(opts_data,add_opts=['foo'])
+
+if cmd_args == ['show_common_opts_diff']:
+	from mmgen.opts import show_common_opts_diff
+	show_common_opts_diff()
+	sys.exit(0)
+
+for k in (
+	'foo',               # added opt
+	'print_checksum',    # sets 'quiet'
+	'quiet','verbose',   # required_opts, incompatible_opts
+	'fee_estimate_mode', # autoset_opts
+	'passwd_file',       # infile_opts - check_infile()
+	'outdir',            # check_outdir()
+	'subseeds',          # opt_sets_global
+	'key_generator',     # global_sets_opt
+	'hidden_incog_input_params',
+	):
+	msg('{:30} {}'.format('opt.'+k+':',getattr(opt,k)))
+
+msg('')
+for k in (
+	'subseeds',          # opt_sets_global
+	'key_generator',     # global_sets_opt
+	):
+	msg('{:30} {}'.format('g.'+k+':',getattr(opt,k)))

+ 3 - 2
test/test.py

@@ -142,7 +142,7 @@ If no command is given, the whole test suite is run.
 data_dir = os.path.join('test','data_dir' + ('','-α')[bool(os.getenv('MMGEN_DEBUG_UTF8'))])
 
 # we need the values of two opts before running opts.init, so parse without initializing:
-_uopts = opts.init(opts_data,parse_only=True)[0]
+_uopts = opts.init(opts_data,parse_only=True).user_opts
 
 # step 1: delete data_dir symlink in ./test;
 if not ('resume' in _uopts or 'skip_deps' in _uopts):
@@ -161,7 +161,6 @@ sys.argv.insert(1,'--rpc-port={}'.format(CoinDaemon(network_id,test_suite=True).
 # step 2: opts.init will create new data_dir in ./test (if not 'resume' or 'skip_deps'):
 usr_args = opts.init(opts_data)
 
-
 # step 3: move data_dir to /dev/shm and symlink it back to ./test:
 trash_dir = os.path.join('test','trash')
 if not ('resume' in _uopts or 'skip_deps' in _uopts):
@@ -339,6 +338,7 @@ cfgs = { # addr_idx_lists (except 31,32,33,34) must contain exactly 8 addresses
 	'33': {},
 	'34': {},
 	'40': {},
+	'41': {},
 }
 
 def fixup_cfgs():
@@ -463,6 +463,7 @@ def set_restore_term_at_exit():
 class CmdGroupMgr(object):
 
 	cmd_groups_dfl = {
+		'opts':             ('TestSuiteOpts',{'full_data':True}),
 		'cfg':              ('TestSuiteCfg',{'full_data':True}),
 		'helpscreens':      ('TestSuiteHelp',{'modname':'misc','full_data':True}),
 		'main':             ('TestSuiteMain',{'full_data':True}),

+ 114 - 0
test/test_py_d/ts_opts.py

@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2020 The MMGen Project <mmgen@tuta.io>
+#
+# Project source code repository: https://github.com/mmgen/mmgen
+# Licensed according to the terms of GPL Version 3.  See LICENSE for details.
+
+"""
+ts_opts.py: options processing tests for the MMGen test.py test suite
+"""
+
+from test.common import *
+from test.test_py_d.ts_base import *
+
+class TestSuiteOpts(TestSuiteBase):
+	'options processing'
+	networks = ('btc',)
+	tmpdir_nums = [41]
+	cmd_group = (
+		('opt_helpscreen',       (41,"helpscreen output", [])),
+		('opt_noargs',           (41,"invocation with no user options or arguments", [])),
+		('opt_good',             (41,"good opts", [])),
+		('opt_bad_infile',       (41,"bad infile parameter", [])),
+		('opt_bad_outdir',       (41,"bad outdir parameter", [])),
+		('opt_bad_incompatible', (41,"incompatible opts", [])),
+		('opt_bad_autoset',      (41,"invalid autoset value", [])),
+		('opt_show_diff',        (41,"show_common_opts_diff()", [])),
+	)
+
+	def spawn_prog(self,args):
+		return self.spawn('test/misc/opts.py',args,cmd_dir='.')
+
+	def check_vals(self,args,vals):
+		t = self.spawn_prog(args)
+		for k,v in vals:
+			t.expect(r'{}:\s+{}'.format(k,v),regex=True)
+		t.read()
+		return t
+
+	def do_run(self,args,expect,exit_val,regex=False):
+		t = self.spawn_prog(args)
+		t.expect(expect,regex=regex)
+		t.read()
+		t.req_exit_val = exit_val
+		return t
+
+	def opt_helpscreen(self):
+		return self.do_run(
+			['--help'],
+			r'OPTS.PY: Opts test.*USAGE:\s+opts.py.*1:python-ecdsa 2:libsecp256k1 \(default: 2\).*'
+			+ r'NOTES FOR THIS.*a note',
+			0,
+			regex=True )
+
+	def opt_noargs(self):
+		return self.check_vals(
+				[],
+				(
+					('opt.foo',               'None'),         # added opt
+					('opt.print_checksum',    'None'),         # sets 'quiet'
+					('opt.quiet',             'False'),        # required_opts, incompatible_opts
+					('opt.verbose',           'None'),         # required_opts, incompatible_opts
+					('opt.fee_estimate_mode', 'conservative'), # autoset_opts
+					('opt.passwd_file',       'None'),         # infile_opts - check_infile()
+					('opt.outdir',            'None'),         # check_outdir()
+					('opt.subseeds',          'None'),         # opt_sets_global
+					('opt.key_generator',     '2'),            # global_sets_opt
+					('g.subseeds',            'None'),
+					('g.key_generator',       '2'),
+				)
+			)
+
+	def opt_good(self):
+		pf_base = 'testfile'
+		pf = os.path.join(self.tmpdir,pf_base)
+		self.write_to_tmpfile(pf_base,'')
+		return self.check_vals(
+				[
+					'--print-checksum',
+					'--fee-estimate-mode=E',
+					'--passwd-file='+pf,
+					'--outdir='+self.tmpdir,
+					'--subseeds=200',
+					'--hidden-incog-input-params={},123'.format(pf),
+				],
+				(
+					('opt.print_checksum',    'True'),
+					('opt.quiet',             'True'), # set by print_checksum
+					('opt.fee_estimate_mode', 'economical'),
+					('opt.passwd_file',       pf),
+					('opt.outdir',            self.tmpdir),
+					('opt.subseeds',          '200'),
+					('opt.hidden_incog_input_params', pf+',123'),
+					('g.subseeds',            '200'),
+				)
+			)
+
+	def opt_bad_infile(self):
+		pf = os.path.join(self.tmpdir,'fubar')
+		return self.do_run(['--passwd-file='+pf],'not found',1)
+
+	def opt_bad_outdir(self):
+		bo = self.tmpdir+'_fubar'
+		return self.do_run(['--outdir='+bo],'not found',1)
+
+	def opt_bad_incompatible(self):
+		return self.do_run(['--label=Label','--keep-label'],'Conflicting options',1)
+
+	def opt_bad_autoset(self):
+		return self.do_run(['--fee-estimate-mode=Fubar'],'not unique substring',1)
+
+	def opt_show_diff(self):
+		return self.do_run(['show_common_opts_diff'],'common_opts_data',0)