Index: Source/Phases/MetricsPhase.cobra
===================================================================
--- Source/Phases/MetricsPhase.cobra	(revision 0)
+++ Source/Phases/MetricsPhase.cobra	(revision 0)
@@ -0,0 +1,24 @@
+class MetricsPhase inherits Phase
+	"""
+	Run any Code Metrics accumulation or calculation that can be done on the AST.
+	Uses a Visitor subclass MetricsGenerator
+	"""
+
+	cue init(c as Compiler)
+		base.init(c)
+
+	get description as String is override
+		return 'Calculating Code Metrics'
+
+	def innerRun is override
+		c = .compiler
+	
+		if not c.options.containsKey('metrics') # metrics calc not enabled
+			return
+		
+		count = 0
+		for mod in c.modules, if mod inherits CobraModule, count += 1
+		if count == 0, return  # probably all sharp modules
+
+		MetricsGeneratorVisitor().gen(c)
+
Index: Source/BackEndClr/SharpGenerator.cobra
===================================================================
--- Source/BackEndClr/SharpGenerator.cobra	(revision 2417)
+++ Source/BackEndClr/SharpGenerator.cobra	(working copy)
@@ -72,7 +72,7 @@
 		_modules.add(SharpModule(fileName, _verbosity))
 
 	def writeSharpRunAllTests(cw as CurlyWriter)
-		runner = .options['test-runner'] to String  # ex: Cobra.Lang.CobraCore.runAllTests, ex: MyProgram.runTests
+		runner = .options.getSubOpt('testifyArgs', 'runner') to String  # ex: Cobra.Lang.CobraCore.runAllTests, ex: MyProgram.runTests
 		if runner <> 'nil'
 			if runner.endsWith('()'), runner = runner[:-2]
 			if runner.startsWith('Cobra.Lang.'), runner = 'CobraLangInternal.' + runner['Cobra.Lang.'.length:]
Index: Source/files-to-compile.text
===================================================================
--- Source/files-to-compile.text	(revision 2417)
+++ Source/files-to-compile.text	(working copy)
@@ -35,6 +35,8 @@
 DocGenerator
 SyntaxHighlighter
 
+MetricsGenerator
+
 Phases/Phase.cobra
 Phases/BindRunTimeLibraryPhase.cobra
 Phases/ReadLibrariesPhase.cobra
@@ -47,6 +49,7 @@
 Phases/BindImplementationPhase.cobra
 Phases/CountNodesPhase.cobra
 Phases/SuggestDefaultNumberPhase.cobra
+Phases/MetricsPhase.cobra
 
 BackEndCommon/CurlyWriter
 BackEndCommon/CurlyGenerator
Index: Source/MetricsGenerator.cobra
===================================================================
--- Source/MetricsGenerator.cobra	(revision 0)
+++ Source/MetricsGenerator.cobra	(revision 0)
@@ -0,0 +1,313 @@
+"""
+Runs in metrics phase if enabled, Walks AST and generates various code metrics
+	ToDate: LinesOfCode and a simple Mccabe/Cyclomatic Complexity Calculation.
+"""
+
+
+use System.Reflection
+
+class MethodMetricsAccumulator
+	"""Value Object for accumulating metrics per Method"""
+	var name = ''
+	var mcc = 0 # mccabe Complexity calc accumulator
+
+	
+class MetricsGeneratorVisitor inherits Visitor
+	"""
+	Example invocations:
+	
+	cobra -metrics Utils.cobra
+	cobra -metrics:loc -files:files-to-compile.text
+	cobra -metrics:mcc=10,loc=50 -lib:System.Web
+
+	Potential options:
+		* warnings vs Table
+
+	Also, see TODO comments in the code.
+	"""
+
+	var methodAccStack =  Stack<of MethodMetricsAccumulator>()
+	
+	# Threshold values for emitting message, set from optionValues
+	var locThreshold = 0
+	var mccThreshold = 0
+
+	var warn = false
+	var metOpt as OptionValues? 	# commandline optionValues for accessing metricsOptions
+	var linesOut =0
+	
+	cue init
+		base.init
+		
+	get methodName as String is override
+		return 'gen'
+		
+	## Generate
+
+	def dispatch(obj as Object?)
+		if true
+			base.dispatch(obj)
+		else
+			try
+				base.dispatch(obj)
+			catch exc as Exception
+				while exc inherits TargetInvocationException and exc.innerException, exc = exc.innerException to !
+				print
+				print ' *** exception for obj [obj]:'
+				print ' ### [exc.typeOf.name]: [exc.message]'
+
+	def gen(c as Compiler)
+		""" Entry point for starting Visitor scan for metric calc"""
+			
+		.metOpt = c.options
+		.locThreshold = .metOpt.getSubOpt('metrics','loc', 999)
+		.mccThreshold = .metOpt.getSubOpt('metrics','mcc', 999)
+		assert .metOpt
+		# .dispatch(c.globalNS)
+		.dispatch(c.modules)
+		
+
+	#def gen(obj as Object?)
+	#	if obj is nil, return
+	#	msg = '*** Unbound visitation for type [obj.getType]: [obj]'
+	#	print msg
+
+	def gen(mod as Module)
+		# This is to capture "native" modules like SharpModule
+		pass
+
+	def gen(mod as AssemblyModule)
+		pass
+
+	def gen(mod as CobraModule)
+		try
+			for decl in mod.topNameSpace.declsInOrder
+				.dispatch(decl)
+		finally
+			pass #.finishFile
+	
+
+	def gen(ns as NameSpace)
+		for decl in ns.declsInOrder, .dispatch(decl)
+
+	# Box includes all below; Class, Interface, Struct, Extension
+	def gen(box as Box)
+		.genBoxMembers(box)
+/#
+	def gen(cl as Class)
+		# TODO: inherits, implements, generic constraints
+		.genBoxMembers(cl)
+
+	def gen(ifc as Interface)
+		# TODO: inherits, generic constraints
+		#.writeDocString(ifc.docString)
+		.genBoxMembers(ifc)
+
+	def gen(struc as Struct)
+		# TODO: implements, generic constraints
+		#.writeDocString(struc.docString)
+		.genBoxMembers(struc)
+
+	def gen(ext as Extension)
+		#.writeDocString(ext.docString)
+		.genBoxMembers(ext)
+#/
+
+	def genBoxMembers(box as Box)
+		typeDecls = .sorted(for decl in box.declsInOrder where decl inherits IType)
+		if typeDecls.count
+			.genBoxMembers(typeDecls)
+
+		sharedDecls = .sorted(for decl in box.declsInOrder where decl.isShared and not (decl to Object) inherits IType)
+		if sharedDecls.count
+			.genBoxMembers(sharedDecls)
+
+		nonsharedDecls = .sorted(for decl in box.declsInOrder where not decl.isShared)
+		if nonsharedDecls.count
+			.genBoxMembers(nonsharedDecls)
+
+	def genBoxMembers(decls as IList<of IBoxMember>)
+		for decl in decls
+			.dispatch(decl)
+		#for i in decls.count
+			#decl = decls[i]
+				
+	def gen(method as AbstractMethod)
+		fullName = '[method.parentBox.name].[method.name]'
+		mmacc = MethodMetricsAccumulator()
+		mmacc.name = fullName
+		mmacc.mcc = 0
+		.methodAccStack.push(mmacc)
+		
+		.genStatementList(method.statements)
+		#TODO requirepart, ensurepart
+
+		mmacc = .methodAccStack.pop
+		if .metOpt.hasSubOpt('metrics','loc') and method.statements.count
+			stmts = method.statements
+			startLine = stmts[0].token.lineNum
+			numLines = stmts[stmts.count-1].lastToken.lineNum - startLine + 1
+			
+		if .metOpt.hasSubOpt('metrics','mcc')
+			mcc = mmacc.mcc
+			
+		if mcc > .mccThreshold or numLines > .locThreshold 	# 10 or 25 and 100
+			# Original McCabe had count <10 (7+/-2) Great, 10-15 OK, >50 Unmaintainable
+			# later research suggests   <11 Great, < 25 OK, >25 =>  >=30% correlation to > faults (http://www.sdtimes.com/content/article.aspx?ArticleID=31820)
+			metricsLine = StringBuilder('[fullName.padLeft(50)]\t\t')
+			if .metOpt.hasSubOpt('metrics','loc')
+				metricsLine.append('[numLines]\t')
+			if .metOpt.hasSubOpt('metrics','mcc')
+				metricsLine.append('[mcc]')
+			#if .warn
+			#	if mcc > .mccThreshold
+			#		_warning('Method [fullName] complexity=[mcc]  McCabe Cyclomatic Complexity > [.ccMetricsThreshold].')
+			#	if numLines > .locThreshold 
+			#		_warning('Method [fullName] LOC=[numLines]: LOC > [.locThreshold] ([stmts.count] toplevel statements).') 
+			#else
+			if .linesOut == 0
+				hdrLine = StringBuilder('...METHOD...'.padLeft(50))
+				hdrLine.append('\t\t')
+				if .metOpt.hasSubOpt('metrics','loc'), hdrLine.append('Lines\t')
+				if .metOpt.hasSubOpt('metrics','mcc'), hdrLine.append('McCabe')
+				print hdrLine.toString	
+			print metricsLine.toString
+			.linesOut += 1	
+				
+	def genStatementList(statements as List<of Stmt>)
+		if statements.count
+			for stmt in statements
+				.dispatch(stmt)
+			
+	# convenience methods for bumping stack top mccabe metrics accumulator field
+	def incMcCabe
+		.methodAccStack.peek.mcc += 1
+		
+	# For metrics to date (mccabe) we are only interested in branch type statements. 
+	# For anything else with statement lists or blocks we just need to dispatch on them	
+
+	# Branch Statement and sub parts
+	def gen(brStmt as BranchStmt)
+		.incMcCabe
+		#.genExpr(brStmt.expr)		
+		for onPart in brStmt.onParts
+			.dispatch(onPart)
+		if brStmt.elsePart
+			.dispatch(brStmt.elsePart)
+		
+	def gen(onPart as BranchOnPart)
+		#for expr in onPart.exprs
+		#	.genExpr(expr)
+		.dispatch(onPart.block)
+	
+	def gen(expct as ExpectStmt)	
+		.dispatch(expct.block)
+		
+	def gen(forStmt as ForStmt)
+		.incMcCabe
+		.dispatch(forStmt.block)
+	
+	def gen(ifStmt as IfStmt)
+		.incMcCabe
+		#.genExpr(ifStmt.cond)
+		# TODO possibly increment again for each and/or in condition list
+		.dispatch(ifStmt.trueStmts)
+		if ifStmt.falseStmts
+			.dispatch(ifStmt.falseStmts)
+			
+	def gen(l as LockStmt)
+		#.genExpr(l.expr)
+		.dispatch(l.block)
+		
+	def gen(prtStmt as PrintRedirectStmt)
+		.dispatch(prtStmt.block)
+		
+	def gen(tryStmt as TryStmt)
+		.dispatch(tryStmt.tryBlock)
+		for cb in tryStmt.catchBlocks
+			.dispatch(cb)	
+		if tryStmt.successBlock
+			.dispatch(tryStmt.successBlock)
+		if tryStmt.finallyBlock
+			.dispatch(tryStmt.finallyBlock)
+		
+	def gen(cb as CatchBlock)
+		.dispatch(cb.block)
+
+	def gen(us as UsingStmt)
+		#.genExpr(us.varExpr) # UsingStmt needs property for varExpr
+		#.genExpr(us.initExpr)
+		.dispatch(us.block)
+		
+	def gen(aws as AbstractWhileStmt)
+		.incMcCabe
+		#.genExpr(aws.expr)
+		# TODO possibly increment again for each and/or in condition list
+		.dispatch(aws.block)
+
+	def gen(block as BlockStmt)
+		for stmt in block.stmts
+			.dispatch(stmt)
+	
+	#Expressions
+	def genExpr(expr as Expr)
+		pass
+		#.dispatch(expr)			
+				
+	
+	# catch everything else and ignore
+	def gen(node as INode)
+		pass
+		
+
+	## Sorting members
+
+	def sorted(decls as IList<of IBoxMember>) as List<of IBoxMember>
+		"""
+		Return the box members in a logically sorted order.
+		First order is by type of member (enums, vars, constructors, props, indexers, methods).
+		Second order is alphabetical.
+		Third order is parameter count.
+		"""
+		t = List<of IBoxMember>(decls)
+		t.sort(ref .compareMembers)
+		return t
+
+	def compareMembers(a as IBoxMember, b as IBoxMember) as int is shared
+		if a.getType is b.getType
+			diff = a.name.compareTo(b.name)  # note that Cobra disallows member names that differ only by case
+			if diff == 0
+				if a inherits AbstractMethod
+					diff = a.params.count.compareTo((b to AbstractMethod).params.count)
+				else if a inherits Indexer
+					# b must also be an indexer since their names were the same
+					diff = a.params.count.compareTo((b to Indexer).params.count)
+			else if a.name.startsWith('_') and not b.name.startsWith('_')
+				diff = .compareNamesWithFirstUnderscored(a.name, b.name)
+			else if not a.name.startsWith('_') and b.name.startsWith('_')
+				diff = -1 * .compareNamesWithFirstUnderscored(b.name, a.name)
+		else
+			diff = .rank(a).compareTo(.rank(b))
+		return diff
+
+	def compareNamesWithFirstUnderscored(a as String, b as String) as int is shared
+		require
+			a.startsWith('_')
+			not b.startsWith('_')
+		body
+			a = a[1:]
+			if a == b, return 1  # underscored/protected member comes last
+			else, return a.compareTo(b)
+			
+	def rank(obj as IBoxMember) as int is shared
+		if obj inherits EnumDecl, return 0
+		if obj inherits MethodSig, return 10
+		if obj inherits Box, return 20
+		if obj inherits BoxConst, return 25
+		if obj inherits BoxVar, return 30
+		if obj inherits Initializer, return 40
+		if obj inherits BoxEvent, return 45
+		if obj inherits Property, return 50
+		if obj inherits Indexer, return 60
+		if obj inherits Method, return 70
+		throw FallThroughException(obj)
Index: Source/Statements.cobra
===================================================================
--- Source/Statements.cobra	(revision 2417)
+++ Source/Statements.cobra	(working copy)
@@ -199,6 +199,10 @@
 		_onParts = onParts
 		_elsePart = elsePart
 
+	get lastToken as IToken is override
+		if _elsePart, return _elsePart.lastToken
+		return _onParts[_onParts.count-1].lastToken # _onParts[[-1]].lastToken
+
 	def addSubFields
 		base.addSubFields
 		.addField('expr', .expr)
@@ -232,6 +236,9 @@
 		_exprs = exprs
 		_block = block
 
+	get lastToken as IToken
+		return _block.lastToken
+
 	def addSubFields
 		base.addSubFields
 		.addField('exprs', _exprs)
Index: Source/Compiler.cobra
===================================================================
--- Source/Compiler.cobra	(revision 2417)
+++ Source/Compiler.cobra	(working copy)
@@ -57,8 +57,20 @@
 		"""
 		Return binary output name for compilation of files for this backend.
 		"""
+		
+class NilBackEnd inherits BackEnd
+	"""Null BackEnd Implementation for suppressing any BackEnd processing."""
+	
+	cue init(c as Compiler)
+		base.init(c)
 
+	def makePhases(phases as IList<of Phase>) is override
+		pass
 
+	def computeOutName as String is override
+		return .compiler.computeOutNameSharp
+		
+
 class Compiler implements ITypeProvider, IWarningRecorder, IErrorRecorder, ICompilerForNodes is partial
 	"""
 	General notes:
@@ -297,6 +319,8 @@
 		.backEnd.makePhases(phases)
 		if .options.boolValue('timeit')
 			phases.add(CountNodesPhase(this))
+		if .options.containsKey('metrics')
+			phases.add(MetricsPhase(this))
 		for i, phase in phases.numbered, phase.stableOrder = i
 		phases.sort  # see Phase.order and .compareTo
 		return phases
@@ -1417,6 +1442,7 @@
 			on 'clr',  _backEnd = ClrBackEnd(this)
 			on 'jvm',  _backEnd = JvmBackEnd(this)
 			on 'objc', _backEnd = ObjcBackEnd(this)
+			on 'nil',  _backEnd = NilBackEnd(this) # Do FrontEnd execution only
 			else, throw FallThroughException(.options.get('back-end'))
 
 
Index: Source/CommandLine.cobra
===================================================================
--- Source/CommandLine.cobra	(revision 2417)
+++ Source/CommandLine.cobra	(working copy)
@@ -34,7 +34,7 @@
 	
 	get type as String
 		require .isUnpacked
-		ensure result in ['accumulator', 'args-list', 'bool', 'int', 'menu', 'set', 'string']
+		ensure result in ['accumulator', 'args-list', 'bool', 'int', 'menu', 'set', 'string', 'multival']
 		return _type
 
 	get synonyms from var
@@ -77,7 +77,7 @@
 		if _type == 'main'
 			_isMain, _type = true, 'bool'
 		if .containsKey('is-main'), _isMain = this['is-main'] to bool
-		assert _type in ['accumulator', 'args-list', 'bool', 'int', 'menu', 'set', 'string']
+		assert _type in ['accumulator', 'args-list', 'bool', 'int', 'menu', 'set', 'string', 'multival']
 		if .containsKey('synonyms')
 			for syn in this['synonyms']
 				_synonyms.add(syn)
@@ -94,7 +94,10 @@
 		_unpackPlatforms
 		_isUnpacked = true
 		assert .type=='menu' implies .choices.count > 0
-		assert .choices.count <> 0 implies .type in ['menu', 'set']
+		assert .choices.count <> 0 implies .type in ['menu', 'set', 'multival']
+		assert .type=='multival' implies .containsKey('subOptions') and .containsKey('choices')
+		assert .type=='multival' implies this['subOptions'].specsList.count == this['choices'].count
+		assert .type=='multival' implies .choices.count > 0
 
 	def _unpackPlatforms
 		if not .containsKey('platforms')
@@ -103,7 +106,42 @@
 			assert platform in ['jvm', '.net', 'objc']
 		this['platforms'] = Set<of String>(this['platforms'] to List<of String>)
 
+	def defaultIfOn as dynamic
+		if .containsKey('defaultIfOn'), return this['defaultIfOn']
+		if .containsKey('default'), return this['default']
+		return 'on'	
+		
+	def subOptions as List<of OptionSpec>
+		"""Return SubOption list ( of OptionSpec) or empty list if no subOptions"""
+		if .containsKey('subOptions')  
+			clos = this['subOptions']
+			assert clos inherits CommandLineOptionSpecs
+			return clos.specsList 
+		return List<of OptionSpec>()
+		# TODO: cache this in unpack and just return cached value from here
 
+	def subOptionValue(name as String, keyList as vari String) as dynamic
+		"""
+		Find subOptions spec with given name; using keys from keyList return the value for 
+		the first existing key in spec. 
+		Return empty string if no subOptions or no matching keys found.
+		"""
+		for subOpt in .subOptions
+			if subOpt['name'] == name  
+				for key in keyList
+					if subOpt.containsKey(key), return subOpt[key]
+				break	
+		return ''	
+		
+	def subOptionSpec(name as String) as OptionSpec
+		"""Return subOption for name or fail"""
+		require .containsKey('subOptions')	
+		for subOpt in .subOptions
+			if subOpt['name'] == name  
+				return subOpt
+		assert false, name
+		return  OptionSpec()
+		
 class CommandLineOptionSpecs
 	"""
 	Returns the command line option specifications, via .specsList, as a List of OptionSpec.
@@ -114,23 +152,57 @@
 
 	cue init
 		base.init
-		for rawSpec in _rawCommandLineOptionSpecs
+		__initInternal(_rawCommandLineOptionSpecs)
+			
+	cue init(rawOptionSpecs) 
+		base.init
+		__initInternal(rawOptionSpecs)
+		
+	def __initInternal(rawOptionSpecs) 
+		assert rawOptionSpecs.count
+		for rawSpec in rawOptionSpecs
 			spec = OptionSpec()
 			if rawSpec inherits Dictionary<of String, Object>
-				for key in rawSpec.keys, spec[key] = rawSpec[key]
+				for key in rawSpec.keys
+					spec[key] = if(key == 'subOptions', CommandLineOptionSpecs(rawSpec[key]), rawSpec[key])
 			else if rawSpec inherits Dictionary<of String, String>
 				for key in rawSpec.keys, spec[key] = rawSpec[key]
 			else
 				throw FallThroughException(rawSpec.getType)
 			spec.unpack
 			_specs.add(spec)
-
+				
 	def specsList as List<of OptionSpec>
 		"""
 		Returns the option specs in the order they were declared.
 		"""
 		return _specs
 
+	# Spec for each cobra compiler commandline option - List of Dictionaries
+	# TODO: doc the types
+	#type: main
+	#type: string
+	#type: int
+	#type: bool
+	#type: set
+	#type: menu
+	#
+	# type: multival supports a single cmdline option supporting multiple subOptions as comma separated name[=value] pairs
+	#	e.g. -multiValueDefault  -multival:key0,key1=15,key2=fred 
+	#	Uses standard entries 
+	# 	(e.g. 'default' for a default value (commaSep list of pairs) if the cmdline option not given)
+	#	On the cmdline you can specify just the option (without any subOptions - to enable a default set)
+	#		this uses the value of entry 'defaultIfOn' (or 'default' if non existent.)
+	#	Must have a  'choices' entry which is a list of the recognised subOption names AND 
+	#	Must have a 'subOptions' entry whose contents is a list of Dictionaries, one for each subOption supported
+	#		Each subOption dict must have at least entry for 'name' but more commonly also 
+	#		'type', 'description' (for help display) and possibly entries for defaults 
+	#		'defaultIfKeyOnly' and  'default'
+	#		On cmdline can give just a subOption( without '=value'). If want to support this and its different from 
+	#			a value for 'default' (below) provide an entry for 'defaultIfKeyOnly' giving the value for this case.
+	#		If want a subOption value set if the option is enabled regardless of the subOption being given
+	#			then provide a 'default' entry.
+	#	
 	var _rawCommandLineOptionSpecs = [
 		{
 			'name': 'about',
@@ -332,6 +404,30 @@
 			'args': 'TYPENAME',
 		},
 		{
+			'name': 'metrics',
+			'type': 'multival',
+			'args': r'[loc[=value],][mcc[=value]...]',
+			'description': 'Enable metrics calculations; name specific metrics to be done and any threshold values as comma separated name=value pairs.',
+			'choices': ['loc', 'mcc'],  #allowable subOption keys
+			'example': ['loc=80,mcc=10', 'mcc,loc=50', 'mcc=12', 'mcc,loc'],
+			#'default': 'loc=100,mcc=25',       # default if not specify name at all (none)
+			'defaultIfOn': 'loc=100,mcc=25',    # default (what subOpt values enabled) if only specify name
+			'subOptions' : [
+				{
+					'name': 'loc',
+					'type': 'int',
+					'description': 'Show methodname and number of codelines if greater than threshold value.', 
+					'defaultIfKeyOnly': '100',  # value to set if only specify subOpt name
+				},	
+				{
+					'name': 'mcc',
+					'type': 'int',
+					'description': 'Show method name and McCabe Complexity Calculation value if omplexity more than threshold. Reasonable values are in range 12-30',
+					'defaultIfKeyOnly': '25', # value to set if only specify subOpt name
+				},
+			],
+		},
+		{
 			'name': 'namespace',
 			'synonyms': ['name-space', 'ns'],
 			'type': 'string',
@@ -415,6 +511,12 @@
 			'args': '"arg1 arg2"',
 		},
 		{
+			'name': 'parse',
+			'synonyms': ['syntax', 'p'],
+			'description': 'Parse only, no backend phases (native codegen or compilation).', 
+			'type': 'main',
+		},
+		{
 			'name': 'target',
 			'synonyms': ['t'],
 			'description': 'Build a specific target.',
@@ -429,32 +531,42 @@
 			'type': 'main',
 		},
 		{
-			'name': 'test-runner',
-			'type': 'string',
-			'description': 'Specify the method to invoke to run the unit tests. The method must be "shared". Typically the method will make use of classes in Cobra.Lang.Test to set up and initiate the test run.',
-			'default': 'Cobra.Lang.CobraCore.runAllTests',
-			'args': 'QUALIFIED-METHOD-NAME|nil',
-		},
-		{
 			'name': 'testify',
-			'description': '...',
+			'description': 'Run Cobra compiler test suite',
 			'type': 'main',
 			'developer-only': true,
 		},
 		{
-			'name': 'testify-results',
-			'description': 'The filename to write the testify results to. Progress is still written to console.',
-			'type': 'string',
-			'default': 'r-testify',
+			'name': 'testifyArgs',
+			'type': 'multival',
+			'description': 'Set names and values for controlling testify as a list of comma separated name=value pairs.',
+			'choices': ['resultsFile', 'runner', 'numThreads'],  #allowable subOption keys
+			'example': ['numThreads=4,resultsFile=testResult', 'runner=Core.myTestRunner', 'numThreads=2'],
 			'developer-only': true,
+			'default': 'resultsFile=r-testify,runner=Cobra.Lang.CobraCore.runAllTests,numThreads=1',
+			'subOptions' : [
+				{
+					'name': 'resultsFile',
+					'type': 'string',
+					'description': 'Filename to write the testify results to. Progress is still written to console.',
+					'default': 'r-testify',	       # value to default to if not specify name at all
+					#'defaultIfKeyOnly': 'r-testify', # value to set if only specify subOpt name
+				},	
+				{
+					'name': 'runner',
+					'type': 'string',
+					'description': 'method to invoke to run the unit tests. The method must be "shared". Typically the method will make use of classes in Cobra.Lang.Test to setup and initiate the test run.',
+					'default': 'Cobra.Lang.CobraCore.runAllTests',
+				},
+				{
+					'name': 'numThreads',
+					'type': 'int',
+					'description': 'Number of threads to run the testify testsuite in.',
+					'default': '1',
+				},
+			],
 		},
 		{
-			'name': 'testify-threads',
-			'description': '...',
-			'type': 'int',
-			'developer-only': true,
-		},
-		{
 			'name': 'timeit',
 			'description': 'Gives the total duration of running cobra (including the target program, if it is to be run). This is "wall time", not "cpu time".',
 			# although this option is implied by 'testify', the description does not say so, since 'testify' is a hidden option
@@ -488,8 +600,8 @@
 			'type': 'main',
 		},
 	]
+	
 
-
 class CommandLine
 	"""
 	The main options that control the command line's behavior are:
@@ -589,9 +701,8 @@
 			dest = _htmlWriter to TextWriter
 		else
 			dest = Console.out
-		if _htmlWriter
-			stylePath = Path.combine(Path.getDirectoryName(CobraCore.exePath), 'styles-output-html.css')
-			_htmlWriter.writeHtml('<html><head><link href="file://[stylePath]" rel=stylesheet type="text/css"></head><body>[_htmlWriter.newLine]')
+			
+		_runPreamble
 		print to dest
 			paths = _pathList to !
 			options = _options
@@ -609,6 +720,7 @@
 				print 'Paths:'
 				for path in paths
 					print '    [path]'
+
 			if options.boolValue('testify')
 				.doTestify(paths)
 			else if options.boolValue('run')
@@ -617,6 +729,8 @@
 				.doTest(paths)
 			else if options.boolValue('compile')
 				.doCompile(paths)
+			else if options.boolValue('parse')
+				.doParse(paths)
 			else if options.boolValue('highlight')
 				.doHighlight(paths)
 			else if options.boolValue('document')
@@ -635,7 +749,15 @@
 				.doHelp
 			else
 				.doRun(paths)
+			_runPostamble
+				
+	def _runPreamble		
 		if _htmlWriter
+			stylePath = Path.combine(Path.getDirectoryName(CobraCore.exePath), 'styles-output-html.css')
+			_htmlWriter.writeHtml('<html><head><link href="file://[stylePath]" rel=stylesheet type="text/css"></head><body>[_htmlWriter.newLine]')
+
+	def _runPostamble
+		if _htmlWriter
 			_htmlWriter.writeHtml('</body></html>[_htmlWriter.newLine]')
 
 	def doHighlight(paths as List<of String>) as Compiler
@@ -650,6 +772,14 @@
 			Node.setCompiler(nil)
 		return comp
 
+	def doParse(paths as List<of String>) as Compiler
+		"""
+		FrontEnd (parse) Processing only. Tokenise..Parse..Bindings. 
+		No Back end processing; No codegen, no native code compilation 
+		"""
+		.options['back-end'] = 'nil'
+		return .doCompile(paths, true, false, nil)
+			
 	def doCompile(paths as List<of String>) as Compiler
 		return .doCompile(paths, true, false, nil)
 
@@ -679,47 +809,46 @@
 			# Each phase of the compiler may throw an exception to stop compilation.
 			# Before doing so, it prints its errors.
 			assert c.errors.count>0
-			if _options.containsKey('editor')
-				spec = _options['editor'] to String?
-			else
-				spec = Environment.getEnvironmentVariable('COBRA_EDITOR')
-			if spec and spec <> ''
-				if spec.indexOf('FILE')==-1
-					.error('Missing FILE from editor spec.')
-				if spec.indexOf('LINE')==-1
-					.error('Missing LINE from editor spec.')
-				i = spec.indexOf('_')
-				if i == -1
-					i = spec.indexOf(' ')
-					if i == -1
-						.error('Missing underscore or space from editor spec.')
-				exeName = spec.substring(0, i)
-				args = spec.substring(i+1)
-				for error in c.errors
-					if error.isError and error.hasSourceSite
-						if error.fileName.trim <> ''
-							# trace error.fileName, error.lineNum
-							args = args.replace('FILE', error.fileName)
-							args = args.replace('LINE', error.lineNum.toString)
-							p = System.Diagnostics.Process()
-							p.startInfo.fileName = exeName
-							p.startInfo.arguments = args
-							p.startInfo.useShellExecute = false
-							if _verbosity >= 3
-								print 'Running: [p.startInfo.fileName] [p.startInfo.arguments]'
-							try
-								p.start
-								p.waitForExit  # TODO: is this really needed?
-							catch exc as Exception
-								print 'Cannot invoke editor:'
-								print '    Command: [p.startInfo.fileName] [p.startInfo.arguments]'
-								print '    Exception: [exc]'
-							break
+			editorSpec = if(_options.containsKey('editor'), _options['editor'] to String?, Environment.getEnvironmentVariable('COBRA_EDITOR'))
+			if editorSpec and editorSpec.length
+				_editorOnErrorLines(c, editorSpec to !)
+	
 		CobraMain.linesCompiled = c.linesCompiled 
 		CobraMain.nodesCompiled = c.nodesCompiled 
 		CobraMain.tokensCompiled = c.tokensCompiled 
 		return c
 
+	def _editorOnErrorLines(c as Compiler, editorSpec as String)
+		if editorSpec.indexOf('FILE')==-1, 	.error('Missing FILE from editor spec.')
+		if editorSpec.indexOf('LINE')==-1, 	.error('Missing LINE from editor spec.')
+		i = editorSpec.indexOf('_')
+		if i == -1
+			i = editorSpec.indexOf(' ')
+			if i == -1, .error('Missing underscore or space from editor spec.')
+
+		editorExe = editorSpec.substring(0, i)
+		args = editorSpec.substring(i+1)
+		for error in c.errors
+			if error.isError and error.hasSourceSite
+				if error.fileName.trim <> ''
+					# trace error.fileName, error.lineNum
+					args = args.replace('FILE', error.fileName)
+					args = args.replace('LINE', error.lineNum.toString)
+					p = System.Diagnostics.Process()
+					p.startInfo.fileName = editorExe
+					p.startInfo.arguments = args
+					p.startInfo.useShellExecute = false
+					if _verbosity >= 3
+						print 'Running: [p.startInfo.fileName] [p.startInfo.arguments]'
+					try
+						p.start
+						p.waitForExit  # TODO: is this really needed?
+					catch exc as Exception
+						print 'Cannot invoke editor:'
+						print '    Command: [p.startInfo.fileName] [p.startInfo.arguments]'
+						print '    Exception: [exc]'
+					break
+		
 	def doDocument(paths as List<of String>) as Compiler
 		comp = .doCompile(paths, false, false, do(c as Compiler)=c.lastPhase inherits BindInterfacePhase)
 		GenerateHtmlDocVisitor(do(module)=module inherits CobraModule).gen(comp)
@@ -799,7 +928,7 @@
 	
 		exeFileName as String? = nil
 		runArgs	= .options.getStringList('run-args')
-		# TODO: what's this?
+		# TODO: exeArgs as arglist of Strings exe-name exe-arg-0 exe-arg-1 ... cf argv list
 		# exeArgs = .options.getDefaultLOStr('exe-args')
 		# if exeArgs.count
 		#	exeFileName = exeArgs[0]
@@ -835,9 +964,12 @@
 		print '    * commands that operate on path(s) are:'
 		print '      -compile .... Compile only. Also, -c'
 		print '      -run ........ Run the program (compile if necessary). Also -r (Default)'
-		print '      -test ....... Run the unit tests of a library.'
+		print '      -test ....... Run the unit tests of an app or library.'
 		print '      -document ... Document the program (partial compilation). Also, -doc'
 		print '      -highlight .. Syntax highlight the program in HTML.'
+		print '      -parse   .... Parse only. No BackEnd processing. Also, -syntax, -p'
+		if Utils.isDevMachine
+			print '      -testify .... Run the compiler testsuite (cwd must be cobra Source dir).'
 		print ''
 		print '  cobra <options> <command>'
 		print '    * standalone commands are:'
@@ -877,19 +1009,15 @@
 					sep = ', '
 				print
 			s = spec.description
-			while s.length
-				if s.length < width
-					print '[leftMarginStr][s]'
-					s = ''
-				else
-					# TODO: bug in here for narrow widths. try "width = 20" to reproduce
-					j = width + 1
-					if j >= s.length, j = s.length - 1
-					while j > 0 and s[j] <> ' ', j -= 1
-					if j
-						sub = s.substring(0, j)
-						s = if(s.length, s.substring(j+1), '')
-						print '[leftMarginStr][sub]'
+			if s.length
+				_printWrapped(s, width, leftMarginStr) 
+			for subOptionSpec in spec.subOptions 
+				assert subOptionSpec.name.length
+				if subOptionSpec.containsKey('description')
+					_printWrapped('"[subOptionSpec.name]" = [subOptionSpec.description]' , width, leftMarginStr + '  ') 
+				if subOptionSpec.hasDefault
+					soDefault = subOptionSpec['default']
+					print '[leftMarginStr]    (default is [soDefault])'
 			if spec.containsKey('example')
 				if spec['example'] inherits System.Collections.IList
 					first = true
@@ -903,6 +1031,21 @@
 			if spec.containsKey('eg') # verbatim example line
 				print '[leftMarginStr]e.g. [spec["eg"]]'
 
+	def _printWrapped(s as String, width as int, leftMarginStr as String)
+		while s.length
+			if s.length < width
+				print '[leftMarginStr][s]'
+				s =''
+			else	
+				# TODO: bug in here for narrow widths. try "width = 20" to reproduce
+				j = width + 1
+				if j >= s.length, j = s.length - 1
+				while j > 0 and s[j] <> ' ', j -= 1
+				if j
+					sub = s.substring(0, j)
+					s = if(s.length, s.substring(j+1), '')
+					print '[leftMarginStr][sub]'
+
 	def doAbout
 		# CC: multiline string
 		print
@@ -1257,12 +1400,28 @@
 			
 	def _addInDefaults(valueDict as Dictionary<of String, Object>)
 		for spec in _optionSpecs
+			if spec.containsKey('subOptions') and valueDict.containsKey(spec.name) 
+				# set defaults for unspecified subOptions with defaults if any subOption is set
+				_addInSubOptionDefaults(spec, valueDict)
+
 			if not valueDict.containsKey(spec.name) and spec.hasDefault
 				defaultValue = _interpretValue(spec.default, spec) to !
 				if .verbosity
 					print 'Setting option "[spec.name]" to default value [defaultValue].'
 				valueDict[spec.name] = defaultValue
-
+				
+	def _addInSubOptionDefaults(spec as OptionSpec, valueDict as Dictionary<of String, Object>)
+		ov = valueDict[spec.name] to OptionValues # that whats been explicitly set
+		for subOptSpec in spec.subOptions
+			if subOptSpec.hasDefault
+				if not ov.containsKey(subOptSpec.name) # value not set already
+					defaultValue = _interpretValue(subOptSpec['default'], subOptSpec) to !
+					# Should we force subOptions to always have a default value ??
+					if .verbosity
+						print 'Setting option "[spec.name]:[subOptSpec.name]" to default value [defaultValue].'
+					ov[subOptSpec.name] = defaultValue
+									
+						
 	def _unpackOptions(valueDict as Dictionary<of String, Object>, fileList as List<of String>)	
 		"""
 		Unpack certain options (verbosity and timeit) into specific class fields, 
@@ -1381,6 +1540,22 @@
 					else
 						valueSet.add(choice)
 				value = valueSet
+			on 'multival' # comma separated  key=value pairs  - key1[=value],key2[=value],...
+				if valueStr =='on', valueStr = 	spec.defaultIfOn
+				pairs = List<of String>(valueStr.split(c','))
+				assert pairs.count > 0
+				subValueOpts = OptionValues()
+				for kvp in pairs
+					kve = List<of String>(kvp.trim.split(c'='))
+					assert kve.count > 0
+					key = kve[0]
+					if not key in spec.choices and key <> 'on'
+						print 'Unknown subOption key "[key]" in option "[spec.name]"'
+						continue
+					valStr = if(kve.count > 1, kve[1], spec.subOptionValue(key, 'defaultIfKeyOnly', 'default') to String)
+					iValue = _interpretValue(valStr, spec.subOptionSpec(key))	
+					subValueOpts.setKV(key, iValue)
+				value = subValueOpts	
 			else
 				throw FallThroughException(spec.type)
 		return value
@@ -1428,7 +1603,7 @@
 
 
 class OptionValues inherits Dictionary<of String, Object>
-
+	
 	var _isSpecified = Dictionary<of String, bool>()  # CC: could just be a Set
 
 	cue init
@@ -1460,7 +1635,11 @@
 
 	def get(key as String) as dynamic?
 		return this[key]
-
+		
+	def setKV(key as String, value as Object)
+		this[key] = value
+		.didSpecify(key)
+		
 	def getDefault(key as String, default as dynamic?) as dynamic?
 		if .containsKey(key)
 			return this[key]
@@ -1477,6 +1656,35 @@
 		else
 			return List<of String>()
 
+	def hasSubOpt(majorKey as String, minorKey as String) as bool
+		"""Return bool indicating existance of given Option and subOption"""
+		if not .containsKey(majorKey)
+			return false
+		so = .get(majorKey) to OptionValues
+		return  so.containsKey(minorKey)
+				
+	def getSubOpt(majorKey as String, minorKey as String, default as dynamic) as dynamic
+		"""
+		Return subOption value for the given option and subOption (for a multi key=value Option)
+		SubOptions are stored themselves as a set of OptionValues.
+		If no major or minor key value return default.	
+		"""
+		if .containsKey(majorKey)
+			so = .get(majorKey) to OptionValues
+			if  so.containsKey(minorKey)
+				return so[minorKey] 
+		return default
+				
+	def getSubOpt(majorKey as String, minorKey as String ) as dynamic
+		"""
+		Return subOption value for the given option and subOption (multivalued option,  multi key=value).
+		Both the Option and subOption must exist otherwise an assert exception is incurred
+		"""
+		assert .containsKey(majorKey)
+		so = .get(majorKey) to OptionValues
+		assert  so.containsKey(minorKey)
+		return so[minorKey] 
+
 	# CC: def getDefault<of T>(key as String, value as T) as T ...
 
 	def setValue(key as String) as Set<of String>
Index: Source/TestifyRunner.cobra
===================================================================
--- Source/TestifyRunner.cobra	(revision 2417)
+++ Source/TestifyRunner.cobra	(working copy)
@@ -58,7 +58,8 @@
 		paths = _pathList
 		if paths.count == 0
 			paths = .cobraTestPaths + [Path.getFullPath(Path.combine('..', 'HowTo'))]
-		numThreads = .options.getDefault('testify-threads', 1) to int
+		#numThreads = .options.getDefault('testify-threads', 1) to int
+		numThreads = .options.getSubOpt('testifyArgs', 'numThreads', 1) to int
 		if numThreads > 1
 			.runThreaded(numThreads, paths)
 		else
@@ -77,7 +78,7 @@
 
 		args = CobraCore.commandLineArgs
 		_subCobraExe = args[0]
-		_subCommandLineArgs = for arg in args[1:] where arg.startsWith('-') and not '-testify-threads:' in arg
+		_subCommandLineArgs = for arg in args[1:] where arg.startsWith('-') and not '-testifyArgs:' in arg
 
 		_statusWriter.writeLine('Queueing for threads:')
 		for path in paths
@@ -102,7 +103,7 @@
 			concat.append(sep)
 			concat.append(File.readAllText(fileName))
 			sep = sepNl
-		File.writeAllText(.options['testify-results'] to String, concat.toString)
+		File.writeAllText(.options.getSubOpt('testifyArgs','resultsFile') to String, concat.toString)
 		for fileName in _subResultsFileNames
 			try
 				File.delete(fileName)
@@ -110,7 +111,7 @@
 				_statusWriter.writeLine('warning: Cannot delete "[fileName]" due to: [exc]')
 
 	def _printTotals
-		resultsFileName = .options['testify-results'] to String
+		resultsFileName = .options.getSubOpt('testifyArgs', 'resultsFile') to String
 		using resultsWriter = File.appendText(resultsFileName)
 			__printTotals(resultsWriter to !)
 		__printTotals(_statusWriter to !)
@@ -133,7 +134,7 @@
 				pathIndex = _subDirQueue.count
 				resultsFileName = 'r-testify-[pathIndex]'
 				lock _subResultsFileNames, _subResultsFileNames[pathIndex] = resultsFileName
-				args = _subCommandLineArgs + ['-testify-results:[resultsFileName]', '-testify-threads:1', '"[path]"']
+				args = _subCommandLineArgs + ['-testifyArgs:resultsFile=[resultsFileName],numThreads=1', '"[path]"']
 				lock _statusWriter, _statusWriter.writeLine('Thread [tid] start: [args.join(" ")]')
 			p = Process()
 			p.startInfo.useShellExecute = false
@@ -177,7 +178,7 @@
 		_statusWriter = IndentedWriter(AutoFlushWriter(Console.out))
 		_statusWriter.indentString = '    '
 		try
-			resultsFileName = .options['testify-results'] to String
+			resultsFileName = .options.getSubOpt('testifyArgs', 'resultsFile') to String
 			using resultsWriter = File.createText(resultsFileName)
 				_resultsWriter = IndentedWriter(AutoFlushWriter(resultsWriter))
 				print to _resultsWriter to !
Index: Tests/700-command-line/912-parse-only.cobra
===================================================================
--- Tests/700-command-line/912-parse-only.cobra	(revision 0)
+++ Tests/700-command-line/912-parse-only.cobra	(revision 0)
@@ -0,0 +1,72 @@
+# Test parse only + output metrics info (No native compile or run)
+use System.Text.RegularExpressions
+
+class Test 
+
+	def main is shared 
+		cobraPath = CobraCore.findCobraExe to !
+		if 'Snapshot' in cobraPath
+			# TODO 2008-10-19: Not sure why this is happening yet.
+			print 'WARNING: Snapshot in cobra path:', cobraPath
+			cobraPath = cobraPath.replace('\\Snapshot\\', '\\')
+			cobraPath = cobraPath.replace('/Snapshot/', '/')
+		src = '911-metrics-fodder.cobra'
+		exe = '911-metrics-fodder.exe'
+		try
+			File.delete(exe)
+		catch
+			pass
+		.parseOnly(cobraPath, '-parse [src]')
+		assert not File.exists(exe)
+		
+		#default metrics mccabe >25
+		.metrics1(cobraPath, '-syntax -metrics [src]')
+		
+		#metrics explicit mccabe >10, loc >20 
+		.metrics2(cobraPath, '-p -metrics:loc=20,mcc=10 [src]')
+		assert not File.exists(exe)
+	
+		.metricsErr(cobraPath, '-p -metrics:Xmcc=20 [src]')
+		
+	def parseOnly(cobraPath as String, cmdln as String)is shared
+		p as System.Diagnostics.Process? 
+		output = CobraCore.runCobraExe(cobraPath, cmdln, out p) 
+		#print output 
+		assert 'Unhandled Exception' not in output 
+		assert 'Executed' not in output 
+		assert '...METHOD...\t\tLines\tMcCabe' not in output 
+		assert p.exitCode == 0 
+
+	#default metrics mccabe >25
+	def metrics1(cobraPath as String, cmdln as String) is shared
+		p as System.Diagnostics.Process? 
+		output = CobraCore.runCobraExe(cobraPath, cmdln, out p) 
+		#print output 
+		assert 'Unhandled Exception' not in output 
+		assert 'Executed' not in output 
+		assert '...METHOD...\t\tLines\tMcCabe' in output 
+		assert Regex.isMatch(output, r'MetricsTest\.complexExpr\s*76\s*32')
+		assert p.exitCode == 0 
+		
+	#metrics explicit mccabe >1, loc >1 (all multiline methods)
+	def metrics2(cobraPath as String, cmdln as String) is shared
+		p as System.Diagnostics.Process? 
+		output = CobraCore.runCobraExe(cobraPath, cmdln, out p) 
+		#print output 
+		assert 'Unhandled Exception' not in output 
+		assert 'Executed' not in output 
+		assert '...METHOD...\t\tLines\tMcCabe' in output 
+		assert Regex.isMatch(output, r'MetricsTest\.tryAll\s*26\s*6')
+		assert Regex.isMatch(output, r'MetricsTest\.complexExpr\s*76\s*32')
+		assert p.exitCode == 0 
+
+	def metricsErr(cobraPath as String, cmdln as String) is shared
+		p as System.Diagnostics.Process? 
+		output = CobraCore.runCobraExe(cobraPath, cmdln, out p) 
+		#print output 
+		assert 'Unhandled Exception' not in output 
+		assert 'Executed' not in output 
+		assert '...METHOD...\t\tLines\tMcCabe' not in output 
+		assert 'Unknown subOption key "Xmcc" in option "metrics"' in output
+		assert p.exitCode == 0 
+		
Index: Tests/700-command-line/910-metrics.cobra
===================================================================
--- Tests/700-command-line/910-metrics.cobra	(revision 0)
+++ Tests/700-command-line/910-metrics.cobra	(revision 0)
@@ -0,0 +1,96 @@
+# Test compile+output metrics info (after compile and run) 
+use System.Text.RegularExpressions
+
+class Test 
+
+	def main is shared 
+		cobraPath = CobraCore.findCobraExe to !
+		if 'Snapshot' in cobraPath
+			# TODO 2008-10-19: Not sure why this is happening yet.
+			print 'WARNING: Snapshot in cobra path:', cobraPath
+			cobraPath = cobraPath.replace('\\Snapshot\\', '\\')
+			cobraPath = cobraPath.replace('/Snapshot/', '/')
+			
+		src='911-metrics-fodder.cobra'	
+		.noMetrics(cobraPath, src)
+
+		#default metrics mccabe >25
+		.metrics1(cobraPath, '-metrics [src]')
+		
+		#metrics explicit mccabe >1, loc >1 ( all multiline methods)
+		.metrics2(cobraPath, '-metrics:loc=1,mcc=1 [src]')
+		
+		#mcc only no LOC
+		.metrics3(cobraPath, '-metrics:mcc=1 [src]')
+		
+		#LOC no mcc
+		.metrics4(cobraPath, '-metrics:loc=10 [src]')
+		
+	def noMetrics(cobraPath as String, cmdln as String)is shared
+		p as System.Diagnostics.Process? 
+		output = CobraCore.runCobraExe(cobraPath, cmdln, out p) 
+		#print output 
+		assert 'Unhandled Exception' not in output 
+		assert 'Executed' in output 
+		assert '...METHOD...\t\tLines\tMcCabe' not in output 
+		assert p.exitCode == 0 
+
+	#default metrics mccabe >25
+	def metrics1(cobraPath as String, cmdln as String) is shared
+		p as System.Diagnostics.Process? 
+		output = CobraCore.runCobraExe(cobraPath, cmdln, out p) 
+		#print output 
+		assert 'Unhandled Exception' not in output 
+		assert 'Executed' in output 
+		assert '...METHOD...\t\tLines\tMcCabe' in output 
+		assert Regex.isMatch(output, r'MetricsTest\.complexExpr\s*76\s*32')
+		assert p.exitCode == 0 
+		
+	#metrics explicit mccabe >1, loc >1 (all multiline methods)
+	def metrics2(cobraPath as String, cmdln as String) is shared
+		p as System.Diagnostics.Process? 
+		output = CobraCore.runCobraExe(cobraPath, cmdln, out p) 
+		#print output 
+		assert 'Unhandled Exception' not in output 
+		assert 'Executed' in output 
+		assert '...METHOD...\t\tLines\tMcCabe' in output 
+		assert Regex.isMatch(output, r'MetricsTest\.main\s*5\s*0')
+		assert Regex.isMatch(output, r'MetricsTest\.none\s*19\s*0')
+		assert Regex.isMatch(output, r'MetricsTest\.small\s*7\s*3')
+		assert Regex.isMatch(output, r'MetricsTest\.tryAll\s*26\s*6')
+		assert Regex.isMatch(output, r'MetricsTest\.chkBranch\s*6\s*1')
+		assert Regex.isMatch(output, r'MetricsTest\.chkFor\s*4\s*1')
+		assert Regex.isMatch(output, r'MetricsTest\.chkIf\s*3\s*1')
+		assert Regex.isMatch(output, r'MetricsTest\.chkIfElse\s*4\s*1')
+		assert Regex.isMatch(output, r'MetricsTest\.chkIfElseIf\s*5\s*2')
+		assert Regex.isMatch(output, r'MetricsTest\.chkPostWhile\s*5\s*1')
+		assert Regex.isMatch(output, r'MetricsTest\.complexExpr\s*76\s*32')
+		assert p.exitCode == 0 
+
+	#metrics explicit mcc only no LOC
+	def metrics3(cobraPath as String, cmdln as String) is shared
+		p as System.Diagnostics.Process? 
+		output = CobraCore.runCobraExe(cobraPath, cmdln, out p) 
+		#print output 
+		assert 'Unhandled Exception' not in output 
+		assert 'Executed' in output 
+		assert '...METHOD...\t\tMcCabe' in output 
+		assert Regex.isMatch(output, r'MetricsTest\.small\s*3')
+		assert Regex.isMatch(output, r'MetricsTest\.tryAll\s*6')
+		assert Regex.isMatch(output, r'MetricsTest\.chkIfElseIf\s*2')
+		assert Regex.isMatch(output, r'MetricsTest\.complexExpr\s*32')
+		assert p.exitCode == 0 
+		
+	#metrics explicit LOC only no mcc
+	def metrics4(cobraPath as String, cmdln as String) is shared
+		p as System.Diagnostics.Process? 
+		output = CobraCore.runCobraExe(cobraPath, cmdln, out p) 
+		#print output 
+		assert 'Unhandled Exception' not in output 
+		assert 'Executed' in output 
+		assert '...METHOD...\t\tLines' in output 
+		assert Regex.isMatch(output, r'MetricsTest\.none\s*19')
+		assert Regex.isMatch(output, r'MetricsTest\.tryAll\s*26')
+		assert Regex.isMatch(output, r'MetricsTest\.complexExpr\s*76')
+		assert p.exitCode == 0 
+		
Index: Tests/700-command-line/911-metrics-fodder.cobra
===================================================================
--- Tests/700-command-line/911-metrics-fodder.cobra	(revision 0)
+++ Tests/700-command-line/911-metrics-fodder.cobra	(revision 0)
@@ -0,0 +1,213 @@
+# .skip. 
+# program run to test LOC/McCabe metrics generation
+class MetricsTest
+	def main is shared
+		a = CobraCore.commandLineArgs
+		CobraCore.noOp(a)
+		# .small(99)
+		# emit something to show compilation worked
+		print 'Executed'
+		
+	# the following methods contents are nonsensical and exist only to provide
+	# examples of cobra statements that do and dont get processed for metrics
+
+	# if any of these methods change size/linecount the LOC values in 
+	# 910-metrics.cobra also need to be changed
+		
+	#mccabe 1
+	def chkIf(v as int)
+		if v < 1
+			return	
+		print v
+	
+	#mccabe 1
+	def chkIfElse(v as int)
+		if v < 1
+			return	
+		else
+			print v
+		
+	#mccabe 2
+	def chkIfElseIf(v as int)
+		if v < 1
+			return	
+		else if v > 21
+			v = 21
+		print v
+	
+	#mccabe 1
+	def chkFor(v as int)
+		j =0
+		for i in v
+			j = j +v
+		print j
+		
+	#mccabe 1
+	def chkWhile(v as int)
+		i = j = 0
+		while i < v
+			j = j + v
+			i += 1
+			break
+			continue # (:-)
+		print j
+	
+	#mccabe 1
+	def chkPostWhile(v as int)
+		i = j = 0
+		post while i < v
+			j = j + v
+			i += 1
+		print j
+		
+	#mccabe 1
+	def chkBranch(v as int)
+		branch v
+			on 1, ones =1
+			on 11, ones =2
+			on 111, ones =3
+			else, ones =-1
+		print ones
+			
+	#mccabe 3
+	def small( v as int) is shared
+		if v < 1
+			return	
+		if v >10
+			print 'v over 10'
+		
+		if v >1 and v < 5
+			print 'tween 1 and 5'
+		
+	#mccabe 0
+	def none(v as int) as int is shared
+		assert v >0
+		try
+			v = 100-v
+			v2 = 100/v
+		catch
+			print 'caught'
+		finally
+			v += 1
+		CobraCore.noOp(v2)
+		
+		trace v
+		throw ArgumentException()
+		expect ArgumentException
+			throw ArgumentException('expect')
+		#listen,ignore
+		#using, yield
+		lock Object()
+			pass
+		return v
+		
+	#mccabe 6
+	def tryAll(v as int) is shared
+		if v <= 0	#1
+			return
+		else if v > 21 #2
+			v = 21
+			
+		j=0
+		for i in v	#3
+			j += i		
+		
+		v1 =v
+		while v/10 > 1 #4
+			v = v-5		
+
+		v = v1
+		post while v <30 #5
+			v = v +5	
+
+		v = v1	
+		branch v		# 6
+			on 10
+				tens='1'
+			on 20, tens='2'
+			on 30, tens='3'
+			else
+				tens='unknown'
+		print v, tens		
+		
+	#mccabe 32
+	def complexExpr(peek as String) as String
+		if peek=='LPAREN'
+			expr='('
+		else if peek[0]=='DOT'
+			expr='.'
+			try
+				peek = peek[1:]
+				if peek=='ID' 
+					expr = 'MemberExpr(memberToken) to Expr'
+				else if peek=='OPEN_CALL'
+					expr = '.callExpr'
+				else if peek=='OPEN_GENERIC'
+					expr = '.callExpr'
+				else
+					.throwError('Syntax error after "."')
+			finally
+				print '.opStack.pop'
+			return 'BinaryOpExpr.make'
+		else if peek=='NIL'
+			return 'NilLiteral(.grab)'
+		else if peek=='TRUE'
+			return 'BoolLit(.grab)'
+		else if peek=='FALSE'
+			return 'BoolLit(.grab)'
+		else if peek=='THIS'
+			return 'ThisLit(.grab)'
+		else if peek=='BASE'
+			return 'BaseLit(.grab)'
+		else if peek=='VAR'
+			assert peek.length #_curCodePart
+			if true #_curCodePart inherits ProperDexerXetter
+				return 'VarLit(.grab, _curCodePart)'
+			else
+				.throwError('Cannot refer to `var` in expressions outside of a property `get` or `set`.')
+				throw Exception() # stop a warning
+		else if peek=='CHAR_LIT_SINGLE'
+			return 'CharLit(.grab)'
+		else if peek=='CHAR_LIT_DOUBLE'
+			return 'CharLit(.grab)'
+		else if peek=='STRING_START_SINGLE'
+			return ".stringWithSubstitutionLit('STRING_START_SINGLE', 'STRING_PART_SINGLE', 'STRING_STOP_SINGLE')"
+		else if peek=='STRING_START_DOUBLE'
+			return ".stringWithSubstitutionLit('STRING_START_DOUBLE', 'STRING_PART_DOUBLE', 'STRING_STOP_DOUBLE')"
+		else if peek=='STRING_SINGLE'
+			return 'StringLit(.grab)'
+		else if peek=='STRING_DOUBLE'
+			return 'StringLit(.grab)'
+		else if peek=='INTEGER_LIT'
+			return 'IntegerLit(.grab)'
+		else if peek=='DECIMAL_LIT'
+			return 'DecimalLit(.grab)'
+		else if peek=='FRACTIONAL_LIT'
+			return 'FractionalLit(.grab)'
+		else if peek=='FLOAT_LIT'
+			return 'FloatLit(.grab)'
+		else if peek=='LBRACKET'
+			return '.literalList'
+		else if peek=='ARRAY_OPEN'
+			return '.literalArray'
+		else if peek=='LCURLY'
+			return '.literalDictOrSet'
+		else if peek in ['OPEN_DO', 'DO']
+			return '.doExpr'
+		else if peek=='OPEN_IF'
+			return '.ifExpr'
+		else if peek=='FOR'
+			return '.forExpr'
+		else if peek=='OPEN_CALL'
+			return '.callExpr'
+		else if peek=='OPEN_GENERIC'
+			if true #.opStack.count and .opStack.peek == 'DOT'
+				return '.callExpr'
+			else
+				return 'TypeExpr(.typeId)'
+		else if peek=='ID'
+			return '.identifierExpr'
+		return expr
+		
+	def throwError(s as String)
+		pass	
Index: Developer/IntermediateReleaseNotes.text
===================================================================
--- Developer/IntermediateReleaseNotes.text	(revision 2417)
+++ Developer/IntermediateReleaseNotes.text	(working copy)
@@ -64,7 +64,9 @@
 
 * Generate warning on use of deprecated `$sharp('...')` syntax in favor of newer alternate sharp-string-literal form: `sharp'...'` and `sharp"..."`.
 
+* Add support for code metrics, LOC and McCabe so far. ticket:245
 
+
 ================================================================================
 Library
 ================================================================================
@@ -94,7 +96,12 @@
 
 * Added -verbosity-ref option to output information about resolving references to libraries.
 
-
+* Added support for specifying a cmdline option with subOptions and modified  
+    -test-runner, -testify-Threads, -testify-results to use it becoming a new multival option 
+    -testifyArgs with subOptions runner,results and numThreads.
+    e.g. -testifyArgs:results=tResults,numThreads=4,runner=Core.Runner
+         -testifyArgs:numThreads=4
+        
 ================================================================================
 Samples etc.
 ================================================================================
@@ -475,3 +482,6 @@
 * Fixed: Dynamic binding cannot find `shared` methods.  ticket:208
 
 * Fixed: File and line number are duplicated in some compiler error messages.  ticket:212
+
+* Fixed: Better error message for unrecognised docString : ticket:218
+

