Index: fedd/compose.py
===================================================================
--- fedd/compose.py	(revision 4a53c72f07bbf042725d59bf3e0dff8e2b4a19e3)
+++ fedd/compose.py	(revision 5e4025d019054e3865a45e39017cf978a9e1d51d)
@@ -24,4 +24,53 @@
 	return "%s:%s:%s:%s" % (self.name, self.required,
 		",".join(self.provides), ",".join(self.accepts))
+
+	
+class ComposeOptionParser(OptionParser):
+    """
+    This class encapsulates the options to this script in one place.  It also
+    holds the callback for the multifile choice.
+    """
+    def __init__(self):
+	OptionParser.__init__(self)
+	self.add_option('--url', dest='url', default="http://localhost:13235",
+		help='url of ns2 to topdl service')
+	self.add_option('--certfile', dest='cert', default=None,
+		help='Certificate to use as identity')
+	self.add_option('--seed', dest='seed', type='int', default=None,
+		help='Random number seed')
+	self.add_option('--multifile', dest='files', default=[], type='string', 
+		action='callback', callback=self.multi_callback, 
+		help="Include file multiple times")
+	self.add_option('--output', dest='outfile', default=None,
+		help='Output file name')
+	self.add_option('--format', dest='format', default="xml",
+		help='Output file format')
+
+    @staticmethod
+    def multi_callback(option, opt_str, value, parser):
+	"""
+	Parse a --multifile command line option.  The parameter is of the form
+	filename,count.  This splits the argument at the rightmost comma and
+	inserts the filename, count tuple into the "files" option.  It handles
+	a couple error cases, too.
+	"""
+	idx = value.rfind(',')
+	if idx != -1:
+	    try:
+		parser.values.files.append((value[0:idx], int(value[idx+1:])))
+	    except ValueError, e:
+		raise OptionValueError(
+			"Can't convert %s to int in multifile (%s)" % \
+				(value[idx+1:], value))
+	else:
+	    raise OptionValueError(
+		    "Bad format (need a comma) for multifile: %s" % value)
+
+def warn(msg):
+    """
+    Exactly what you think.  Print a message to stderr
+    """
+    print >>sys.stderr, msg
+
 
 def make_new_name(names, prefix="name"):
@@ -159,5 +208,5 @@
 	return None
 
-def connect_composition_points(top, contraints):
+def connect_composition_points(top, contraints, names):
     """
     top is a topology containing copies of all the topologies represented in
@@ -222,4 +271,6 @@
     """
 
+    const_re = re.compile("\s*#\s*COMPOSITION:\s*([^:]+:[^:]+:.*)")
+
     marks = { }
     for l in contents:
@@ -228,4 +279,7 @@
 	    exp = re.sub('\s', '', m.group(1))
 	    nn, r, p, a = exp.split(":")
+	    if nn.find('(') != -1:
+		# Convert array references into topdl names
+		nn = re.sub('\\((\\d)\\)', '-\\1', nn)
 	    p = p.split(",")
 	    a = a.split(",")
@@ -234,4 +288,29 @@
 	    marks[nn] = c
     return marks
+
+def add_constraint_attributes(top, marks):
+    """
+    Take the constraints in marks and add attributes representing them to the
+    nodes in top.
+    """
+    for e in [e for e in top.elements if isinstance(e, topdl.Computer)]:
+	if e.name in marks and not e.get_attribute('composition_point'):
+	    c = marks[e.name]
+	    e.set_attribute('composition_point', 'true')
+	    e.set_attribute('required', '%s' % c.required)
+	    e.set_attribute('provides', ",".join(c.provides))
+	    e.set_attribute('accepts', ",".join(c.accepts))
+
+def remove_constraint_attributes(top, marks):
+    """
+    Remove constraint attributes that match the ones in the dict from the
+    topology.
+    """
+    for e in [e for e in top.elements \
+	    if isinstance(e, topdl.Computer) and e.name in marks]:
+	e.remove_attribute('composition_point')
+	e.remove_attribute('required')
+	e.remove_attribute('provides')
+	e.remove_attribute('accepts')
 
 def import_ns2_component(fn):
@@ -252,8 +331,17 @@
     if not top:
 	raise RuntimeError("Cannot create topology from: %s" % fn)
+    add_constraint_attributes(top, marks)
 
     return (top, marks)
 
 def import_topdl_component(fn):
+    """
+    Pull a component in from a topdl description.  The topology generation is
+    straightforward and the constraints are pulled from the attributes of
+    Computer nodes in the experiment.  The compisition_point attribute being
+    true marks a node as a composition point.  The required, provides and
+    accepts attributes map directly into those constraint fields.  The dict of
+    constraints and the topology are returned.
+    """
     top = topdl.topology_from_xml(filename=fn, top='experiment')
     marks = { }
@@ -271,6 +359,6 @@
 def index_constraints(constraints, provides, accepts):
     """
-    Add constraints to the provides and accepts indices based on what the
-    attributes of the contstraints.
+    Add constraints to the provides and accepts indices based on the attributes
+    of the contstraints.
     """
     for c in constraints:
@@ -280,38 +368,139 @@
 		else: dict[a].append(c)
 
-def multi_callback(option, opt_str, value, parser):
-    """
-    Parse a --multifile command line option.  The parameter is of the form
-    filename,count.  This splits the argument at the rightmost comma and
-    inserts the filename, count tuple into the "files" option.  It handles a
-    couple error cases, too.  This is an optparse.OptionParser callback.
-    """
-    idx = value.rfind(',')
-    if idx != -1:
+def get_suffix(fn):
+    """
+    We get filename suffixes a couple places.  It;s worth using the same code.
+    This gets the shortest . separated suffix from a filename, or None.
+    """
+    idx = fn.rfind('.')
+    if idx != -1: return [idx+1:]
+    else: return None
+
+
+def output_composition(top, constraints, outfile=None, format=None):
+    """
+    Output the composition to the file named by outfile (if any) in the format
+    given by format if any.  If both are None, output to stdout in topdl
+    """
+    def topdl_out(f, top, constraints):
+	"""
+	Output into topdl.  Just call the topdl output, as the constraint
+	attributes should be in the topology.
+	"""
+	print >>f, topdl.topology_to_xml(comp, top='experiment')
+
+    def ns2_out(f, top, constraints):
+	"""
+	Reformat the constraint data structures into ns2 constraint comments
+	and output the ns2 using the topdl routine.
+	"""
+	# Inner routines
+	# Deal with the possibility that the single string name is still in the
+	# constraint.
+	def name_format(n):
+	    if isinstance(n, tuple): return n[0]
+	    else: return n
+	    
+	    
+	# Format the required field back into a string. (ns2_out inner
+	def required_format(x):
+	    if x: return "required"
+	    else: return "optional"
+	
+	# ns2_out main line
+	for c in constraints:
+	    print >>f, "# COMPOSITION: %s:%s:%s:%s" % (
+		    topdl.to_tcl_name(name_format(c.name)),
+		    required_format(c.required), ",".join(c.provides),
+		    ",".join(c.accepts))
+	print >>f, topdl.topology_to_ns2(top)
+
+    # Info to map from format to output routine.  
+    exporters = {
+	    'xml':topdl_out, 'topdl':topdl_out,
+	    'tcl': ns2_out, 'ns': ns2_out,
+	    }
+
+    if format:
+	# Explicit format set, use it
+	if format in exporters:
+	    exporter = exporters[format]
+	else:
+	    raise RuntimeError("Unknown format %s" % format)
+    elif outfile:
+	# Determine the format from the suffix (if any)
+	s = get_suffix(outfile)
+	if s and s in exporters:
+	    exporter = exporters[s]
+	else:
+	    raise RuntimeError("Unknown format (suffix) %s" % outfile)
+    else:
+	# Both outfile and format are empty
+	exporter = topdl_out
+
+    # The actual output.  Open the file, if any, and call the exporter
+    if outfile: f = open(outfile, "w")
+    else: f = sys.stdout
+    exporter(f, top, constraints)
+    if outfile: f.close()
+
+def import_components(files):
+    """
+    Pull in the components.  The input is a sequence of tuples where each tuple
+    includes the name of the file to pull the component from and the number of
+    copies to make of it.  The routine to read a file is picked by its suffix.
+    On completion, a tuple containing the number of components successfully
+    read, a list of the topologies, a set of names in use accross all
+    topologies (for inserting later names), the constraints extracted, and
+    indexes mapping the attribute provices and accepted tinto the lists of
+    constraints that provide or accept them is returned.
+    """
+    importers = {
+	    'tcl': import_ns2_component, 
+	    'ns': import_ns2_component, 
+	    'xml':import_topdl_component,
+	    'topdl':import_topdl_component,
+	    }
+    names = set()
+    constraints = [ ]
+    provides = { }
+    accepts = { }
+    components = 0
+    topos = [ ]
+
+    for fn, cnt in files:
+	top = None
 	try:
-	    parser.values.files.append((value[0:idx], int(value[idx+1:])))
-	except ValueError, e:
-	    raise OptionValueError("Can't convert %s to int in multifile (%s)" \
-		    % (value[idx+1:], value))
-    else:
-	raise OptionValueError("Bad format (need a comma) for multifile: %s" \
-		% value)
-
-
+	    s = get_suffix(fn)
+	    if s and s in importers:
+		top, marks = importers[s](fn)
+	    else:
+		warn("Unknown suffix on file %s.  Ignored" % fn)
+		continue
+	except service_error, e:
+	    warn("Remote error on %s: %s" % (fn, e))
+	    continue
+	except EnvironmentError, e:
+	    warn("Error on %s: %s" % (fn, e))
+	    continue
+
+	# Parsed the component and sonctraints, now work through the rest of
+	# the pre-processing.  We do this once per copy of the component
+	# requested, cloning topologies and constraints each time.
+	for i in range(0, cnt):
+	    components += 1
+	    t = top.clone()
+	    m = copy.deepcopy(marks)
+	    index_constraints(m.values(), provides, accepts)
+	    add_interfaces(t, m)
+	    localize_names(t, names, m)
+	    t.incorporate_elements()
+	    constraints.extend(m.values())
+	    topos.append(t)
+
+    return (components, topos, names, constraints, provides, accepts)
 
 # Main line begins
-
-const_re = re.compile("\s*#\s*COMPOSITION:\s*([^:]+:[^:]+:.*)")
-
-parser = OptionParser()
-parser.add_option('--url', dest='url', default="http://localhost:13235",
-	help='url of ns2 to topdl service')
-parser.add_option('--certfile', dest='cert', default=None,
-	help='Certificate to use as identity')
-parser.add_option('--seed', dest='seed', type='int', default=None,
-	help='Random number seed')
-parser.add_option('--multifile', dest='files', default=[], type='string', 
-	action='callback', callback=multi_callback, 
-	help="Include file multiple times")
+parser = ComposeOptionParser()
 
 opts, args = parser.parse_args()
@@ -329,63 +518,50 @@
 files.extend([ (a, 1) for a in args])
 
-names = set()
-constraints = [ ]
-provides = { }
-accepts = { }
-imp = ( (('.tcl', '.ns'), import_ns2_component), 
-	(('.xml', '.topdl'), import_topdl_component),
-	)
-for fn, cnt in files:
-    try:
-	for suffix, importer in imp:
-	    if fn.endswith(suffix):
-		top, marks = importer(fn)
-		break
-	else:
-	    print >>sys.stderr, "Unknown suffix on file %s.  Ignored" % fn
-	    continue
-    except service_error, e:
-	print >>sys.stderr, "Remote error on %s: %s" % (fn, e)
-	continue
-    except EnvironmentError, e:
-	print >>sys.stderr, "Error on %s: %s" % (fn, e)
-	continue
-
-    for i in range(0, cnt):
-	t = top.clone()
-	m = copy.deepcopy(marks)
-	index_constraints(m.values(), provides, accepts)
-	add_interfaces(t, m)
-	localize_names(t, names, m)
-	t.incorporate_elements()
-	constraints.extend(m.values())
+# Pull 'em in.
+components, topos, names, constraints, provides, accepts = \
+	import_components(files)
 
 # Let the user know if they messed up on specifying constraints.
 if any([ not isinstance(c.name, tuple) for c in constraints]):
-    print >>sys.stderr, "nodes not found for constraints on %s" % \
+    warn("nodes not found for constraints on %s" % \
 	    ",".join([ c.name for c in constraints \
-	    if isinstance(c.name, basestring)])
+	    if isinstance(c.name, basestring)]))
     constraints = [ c for c in constraints if isinstance(c.name, tuple )]
 
-# Mix up the constraint indexes 
-randomize_constraint_order((provides, accepts))
-
-# Now the various components live in the same namespace and are marked with
-# their composition requirements. 
-
-if not meet_constraints([c for c in constraints if c.required], 
-	provides, accepts):
-    sys.exit("Could not meet all required constraints")
-meet_constraints([ c for c in constraints if not c.match ], provides, accepts)
-
-# Make a topology containing all elements and substrates from components that
-# had matches.
-comp = topdl.Topology()
-for t in set([ c.name[2] for c in constraints if c.match]):
-    comp.elements.extend([e.clone() for e in t.elements])
-    comp.substrates.extend([s.clone() for s in t.substrates])
-
-# Add substrates and connections corresponding to composition points
-connect_composition_points(comp, constraints)
-
-print topdl.topology_to_xml(comp, top='experiment')
+# If more than one component was given, actually do the composition, otherwise
+# this is probably a format conversion. 
+if components > 1:
+    # Mix up the constraint indexes 
+    randomize_constraint_order((provides, accepts))
+
+    # Now the various components live in the same namespace and are marked with
+    # their composition requirements. 
+
+    if not meet_constraints([c for c in constraints if c.required], 
+	    provides, accepts):
+	sys.exit("Could not meet all required constraints")
+
+    meet_constraints([ c for c in constraints if not c.match ], 
+	    provides, accepts)
+
+    # Make a topology containing all elements and substrates from components
+    # that had matches.
+    comp = topdl.Topology()
+    for t in set([ c.name[2] for c in constraints if c.match]):
+	comp.elements.extend([e.clone() for e in t.elements])
+	comp.substrates.extend([s.clone() for s in t.substrates])
+
+    # Add substrates and connections corresponding to composition points
+    connect_composition_points(comp, constraints, names)
+
+    # Remove composition attributes for satisfied constraints
+    remove_constraint_attributes(comp, dict(
+	[(c.name[0], c) for c in constraints if c.match]))
+elif topos == 1:
+    comp = topos[0]
+else:
+    sys.exit("Did not read any components.")
+
+# Put out the composition with only the unmatched constriaints
+output_composition(comp, [c for c in constraints if not c.match], 
+	opts.outfile, opts.format)
