Browse Source

cfg.py: improve cfg file parsing

- Parsing is now more strict, requiring a value to exist for each option, so
  existing config files may generate errors.  If a sample file generates a
  parse error upon script launch, delete the file and restart the script.
The MMGen Project 3 years ago
parent
commit
9862f4c5a6
3 changed files with 38 additions and 35 deletions
  1. 32 26
      mmgen/cfg.py
  2. 4 7
      mmgen/opts.py
  3. 2 2
      test/misc/cfg.py

+ 32 - 26
mmgen/cfg.py

@@ -41,6 +41,7 @@ class CfgFile(object):
 	write_metadata = False
 	write_metadata = False
 	fn_base = g.proj_name.lower() + '.cfg'
 	fn_base = g.proj_name.lower() + '.cfg'
 	file_not_found_fs = 'WARNING: {} not found at {!r}'
 	file_not_found_fs = 'WARNING: {} not found at {!r}'
+	line_data = namedtuple('cfgfile_line',['name','value','lineno','chunk'])
 
 
 	def __init__(self):
 	def __init__(self):
 		self.fn = os.path.join(self.fn_dir,self.fn_base)
 		self.fn = os.path.join(self.fn_dir,self.fn_base)
@@ -65,22 +66,31 @@ class CfgFile(object):
 			except:
 			except:
 				die(2,'ERROR: unable to write to {!r}'.format(self.fn))
 				die(2,'ERROR: unable to write to {!r}'.format(self.fn))
 
 
-	def parse_var(self,line,lineno):
-		try:
-			m = re.match(r'(\w+)(\s+(\S+)|(\s+\w+:\S+)+)$',line) # allow multiple colon-separated values
-			return (m[1], dict([i.split(':') for i in m[2].split()]) if m[4] else m[3])
-		except:
-			raise CfgFileParseError('Parse error in file {!r}, line {}'.format(self.fn,lineno))
+	def parse_value(self,value,refval):
+		if isinstance(refval,dict):
+			m = re.fullmatch(r'((\s+\w+:\S+)+)',' '+value) # expect one or more colon-separated values
+			if m:
+				return dict([i.split(':') for i in m[1].split()])
+		elif isinstance(refval,list) or isinstance(refval,tuple):
+			m = re.fullmatch(r'((\s+\S+)+)',' '+value)     # expect single value or list
+			if m:
+				ret = m[1].split()
+				return ret if isinstance(refval,list) else tuple(ret)
+		else:
+			return value
 
 
-	def parse(self):
-		cdata = namedtuple('cfg_var',['name','value','lineno'])
-		def do_parse():
-			for n,line in enumerate(self.data,1):
+	def get_lines(self):
+		def gen_lines():
+			for lineno,line in enumerate(self.data,1):
 				line = strip_comments(line)
 				line = strip_comments(line)
 				if line == '':
 				if line == '':
 					continue
 					continue
-				yield cdata(*self.parse_var(line,n),n)
-		return do_parse()
+				m = re.fullmatch(r'(\w+)(\s+)(.*)',line)
+				if m:
+					yield self.line_data(m[1],m[3],lineno,None)
+				else:
+					raise CfgFileParseError('Parse error in file {!r}, line {}'.format(self.fn,lineno))
+		return gen_lines()
 
 
 	@classmethod
 	@classmethod
 	def get_cls_by_id(self,id_str):
 	def get_cls_by_id(self,id_str):
@@ -106,7 +116,7 @@ class CfgFileSample(CfgFile):
 	def computed_chksum(self):
 	def computed_chksum(self):
 		return type(self).compute_chksum(self.data)
 		return type(self).compute_chksum(self.data)
 
 
-	def parse(self,parse_vars=False):
+	def get_lines(self):
 		"""
 		"""
 		The config file template contains some 'magic':
 		The config file template contains some 'magic':
 		- lines must either be empty or begin with '# '
 		- lines must either be empty or begin with '# '
@@ -117,16 +127,12 @@ class CfgFileSample(CfgFile):
 		- last line is metadata line of the form '# Version VER_NUM HASH'
 		- last line is metadata line of the form '# Version VER_NUM HASH'
 		"""
 		"""
 
 
-		cdata = namedtuple('chunk_data',['name','lines','lineno','parsed'])
-
-		def process_chunk(chunk,n):
-			last_line = chunk[-1].split()
-			return cdata(
-				last_line[1],
-				chunk,
-				n,
-				self.parse_var(' '.join(last_line[1:]),n) if parse_vars else None,
-			)
+		def process_chunk(chunk,lineno):
+			m = re.fullmatch(r'(#\s*)(\w+)(\s+)(.*)',chunk[-1])
+			if m:
+				return self.line_data(m[2],m[4],lineno,chunk)
+			else:
+				raise CfgFileParseError('Parse error in file {!r}, line {}'.format(self.fn,lineno))
 
 
 		def gen_chunks(lines):
 		def gen_chunks(lines):
 			hdr = True
 			hdr = True
@@ -212,7 +218,7 @@ class CfgFileSampleUsr(CfgFileSample):
 		if self.data:
 		if self.data:
 			if self.parse_metadata():
 			if self.parse_metadata():
 				if self.chksum == self.computed_chksum:
 				if self.chksum == self.computed_chksum:
-					diff = self.diff(self.parse(),src.parse())
+					diff = self.diff(self.get_lines(),src.get_lines())
 					if not diff:
 					if not diff:
 						return
 						return
 					self.show_changes(diff)
 					self.show_changes(diff)
@@ -260,7 +266,7 @@ class CfgFileSampleUsr(CfgFileSample):
 				msg(m1.format(suf(data,verb='has'),desc,opts))
 				msg(m1.format(suf(data,verb='has'),desc,opts))
 				if desc == 'removed' and data:
 				if desc == 'removed' and data:
 					uc = cfg_file('usr')
 					uc = cfg_file('usr')
-					usr_names = [i.name for i in uc.parse()]
+					usr_names = [i.name for i in uc.get_lines()]
 					rm_names = [i.name for i in data]
 					rm_names = [i.name for i in data]
 					bad = sorted(set(usr_names).intersection(rm_names))
 					bad = sorted(set(usr_names).intersection(rm_names))
 					if bad:
 					if bad:
@@ -277,7 +283,7 @@ class CfgFileSampleUsr(CfgFileSample):
 						yield (
 						yield (
 							'{} section{}:'.format(capfirst(desc),suf(data))
 							'{} section{}:'.format(capfirst(desc),suf(data))
 							+ sep2
 							+ sep2
-							+ sep2.join(['{}'.format(sep.join(v.lines)) for v in data])
+							+ sep2.join(['{}'.format(sep.join(v.chunk)) for v in data])
 						)
 						)
 
 
 			do_pager(
 			do_pager(

+ 4 - 7
mmgen/opts.py

@@ -115,8 +115,7 @@ def opt_postproc_debug():
 
 
 def override_globals_from_cfg_file(ucfg):
 def override_globals_from_cfg_file(ucfg):
 	from .protocol import CoinProtocol,init_proto
 	from .protocol import CoinProtocol,init_proto
-	for d in ucfg.parse():
-		val = d.value
+	for d in ucfg.get_lines():
 		if d.name in g.cfg_file_opts:
 		if d.name in g.cfg_file_opts:
 			ns = d.name.split('_')
 			ns = d.name.split('_')
 			if ns[0] in CoinProtocol.coins:
 			if ns[0] in CoinProtocol.coins:
@@ -130,11 +129,9 @@ def override_globals_from_cfg_file(ucfg):
 				cls = g                          # g is "singleton" instance, so override _instance_ attr
 				cls = g                          # g is "singleton" instance, so override _instance_ attr
 				attr = d.name
 				attr = d.name
 			refval = getattr(cls,attr)
 			refval = getattr(cls,attr)
-			if type(refval) is dict and type(val) is str: # hack - catch single colon-separated value
-				try:
-					val = dict([val.split(':')])
-				except:
-					raise CfgFileParseError(f'Parse error in file {ucfg.fn!r}, line {d.lineno}')
+			val = ucfg.parse_value(d.value,refval)
+			if not val:
+				raise CfgFileParseError(f'Parse error in file {ucfg.fn!r}, line {d.lineno}')
 			val_conv = set_for_type(val,refval,attr,src=ucfg.fn)
 			val_conv = set_for_type(val,refval,attr,src=ucfg.fn)
 			setattr(cls,attr,val_conv)
 			setattr(cls,attr,val_conv)
 		else:
 		else:

+ 2 - 2
test/misc/cfg.py

@@ -17,9 +17,9 @@ msg('Sample cfg file: {}'.format(cf_sample.fn))
 
 
 if cmd_args:
 if cmd_args:
 	if cmd_args[0] == 'parse_test':
 	if cmd_args[0] == 'parse_test':
-		ps = cf_sample.parse(parse_vars=True)
+		ps = cf_sample.get_lines()
 		msg('parsed chunks: {}'.format(len(ps)))
 		msg('parsed chunks: {}'.format(len(ps)))
-		pu = cf_usr.parse()
+		pu = cf_usr.get_lines()
 		msg('usr cfg: {}'.format(' '.join(['{}={}'.format(i.name,i.value) for i in pu])))
 		msg('usr cfg: {}'.format(' '.join(['{}={}'.format(i.name,i.value) for i in pu])))
 	elif cmd_args[0] == 'coin_specific_vars':
 	elif cmd_args[0] == 'coin_specific_vars':
 		from mmgen.protocol import init_proto_from_opts
 		from mmgen.protocol import init_proto_from_opts