__init__.py
Go to the documentation of this file.
00001 # Copyright (c) 2013, Willow Garage, Inc.
00002 # Copyright (c) 2014, Open Source Robotics Foundation, Inc.
00003 # All rights reserved.
00004 #
00005 # Redistribution and use in source and binary forms, with or without
00006 # modification, are permitted provided that the following conditions are met:
00007 #
00008 #     * Redistributions of source code must retain the above copyright
00009 #       notice, this list of conditions and the following disclaimer.
00010 #     * Redistributions in binary form must reproduce the above copyright
00011 #       notice, this list of conditions and the following disclaimer in the
00012 #       documentation and/or other materials provided with the distribution.
00013 #     * Neither the name of the Open Source Robotics Foundation, Inc.
00014 #       nor the names of its contributors may be used to endorse or promote
00015 #       products derived from this software without specific prior
00016 #       written permission.
00017 #
00018 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
00019 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
00020 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
00021 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
00022 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
00023 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
00024 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
00025 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
00026 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
00027 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00028 # POSSIBILITY OF SUCH DAMAGE.
00029 
00030 # Author: Stuart Glaser
00031 # Maintainer: William Woodall <william@osrfoundation.org>
00032 
00033 from __future__ import print_function
00034 
00035 import getopt
00036 import glob
00037 import os
00038 import re
00039 import string
00040 import sys
00041 import xml
00042 
00043 from xml.dom.minidom import parse
00044 
00045 from roslaunch import substitution_args
00046 from rosgraph.names import load_mappings
00047 
00048 try:
00049     _basestr = basestring
00050 except NameError:
00051     _basestr = str
00052 
00053 # Dictionary of subtitution args
00054 substitution_args_context = {}
00055 
00056 
00057 class XacroException(Exception):
00058     pass
00059 
00060 
00061 def isnumber(x):
00062     return hasattr(x, '__int__')
00063 
00064 
00065 def eval_extension(str):
00066     return substitution_args.resolve_args(str, context=substitution_args_context, resolve_anon=False)
00067 
00068 
00069 # Better pretty printing of xml
00070 # Taken from http://ronrothman.com/public/leftbraned/xml-dom-minidom-toprettyxml-and-silly-whitespace/
00071 def fixed_writexml(self, writer, indent="", addindent="", newl=""):
00072     # indent = current indentation
00073     # addindent = indentation to add to higher levels
00074     # newl = newline string
00075     writer.write(indent + "<" + self.tagName)
00076 
00077     attrs = self._get_attributes()
00078     a_names = list(attrs.keys())
00079     a_names.sort()
00080 
00081     for a_name in a_names:
00082         writer.write(" %s=\"" % a_name)
00083         xml.dom.minidom._write_data(writer, attrs[a_name].value)
00084         writer.write("\"")
00085     if self.childNodes:
00086         if len(self.childNodes) == 1 \
00087            and self.childNodes[0].nodeType == xml.dom.minidom.Node.TEXT_NODE:
00088             writer.write(">")
00089             self.childNodes[0].writexml(writer, "", "", "")
00090             writer.write("</%s>%s" % (self.tagName, newl))
00091             return
00092         writer.write(">%s" % (newl))
00093         for node in self.childNodes:
00094             if node.nodeType is not xml.dom.minidom.Node.TEXT_NODE:  # 3:
00095                 node.writexml(writer, indent + addindent, addindent, newl)
00096                 #node.writexml(writer,indent+addindent,addindent,newl)
00097         writer.write("%s</%s>%s" % (indent, self.tagName, newl))
00098     else:
00099         writer.write("/>%s" % (newl))
00100 # replace minidom's function with ours
00101 xml.dom.minidom.Element.writexml = fixed_writexml
00102 
00103 
00104 class Table:
00105     def __init__(self, parent=None):
00106         self.parent = parent
00107         self.table = {}
00108 
00109     def __getitem__(self, key):
00110         if key in self.table:
00111             return self.table[key]
00112         elif self.parent:
00113             return self.parent[key]
00114         else:
00115             raise KeyError(key)
00116 
00117     def __setitem__(self, key, value):
00118         self.table[key] = value
00119 
00120     def __contains__(self, key):
00121         return \
00122             key in self.table or \
00123             (self.parent and key in self.parent)
00124 
00125 
00126 class QuickLexer(object):
00127     def __init__(self, **res):
00128         self.str = ""
00129         self.top = None
00130         self.res = []
00131         for k, v in res.items():
00132             self.__setattr__(k, len(self.res))
00133             self.res.append(v)
00134 
00135     def lex(self, str):
00136         self.str = str
00137         self.top = None
00138         self.next()
00139 
00140     def peek(self):
00141         return self.top
00142 
00143     def next(self):
00144         result = self.top
00145         self.top = None
00146         for i in range(len(self.res)):
00147             m = re.match(self.res[i], self.str)
00148             if m:
00149                 self.top = (i, m.group(0))
00150                 self.str = self.str[m.end():]
00151                 break
00152         return result
00153 
00154 
00155 def first_child_element(elt):
00156     c = elt.firstChild
00157     while c:
00158         if c.nodeType == xml.dom.Node.ELEMENT_NODE:
00159             return c
00160         c = c.nextSibling
00161     return None
00162 
00163 
00164 def next_sibling_element(elt):
00165     c = elt.nextSibling
00166     while c:
00167         if c.nodeType == xml.dom.Node.ELEMENT_NODE:
00168             return c
00169         c = c.nextSibling
00170     return None
00171 
00172 
00173 # Pre-order traversal of the elements
00174 def next_element(elt):
00175     child = first_child_element(elt)
00176     if child:
00177         return child
00178     while elt and elt.nodeType == xml.dom.Node.ELEMENT_NODE:
00179         next = next_sibling_element(elt)
00180         if next:
00181             return next
00182         elt = elt.parentNode
00183     return None
00184 
00185 
00186 # Pre-order traversal of all the nodes
00187 def next_node(node):
00188     if node.firstChild:
00189         return node.firstChild
00190     while node:
00191         if node.nextSibling:
00192             return node.nextSibling
00193         node = node.parentNode
00194     return None
00195 
00196 
00197 def child_elements(elt):
00198     c = elt.firstChild
00199     while c:
00200         if c.nodeType == xml.dom.Node.ELEMENT_NODE:
00201             yield c
00202         c = c.nextSibling
00203 
00204 all_includes = []
00205 
00206 # Deprecated message for <include> tags that don't have <xacro:include> prepended:
00207 deprecated_include_msg = """DEPRECATED IN HYDRO:
00208   The <include> tag should be prepended with 'xacro' if that is the intended use
00209   of it, such as <xacro:include ...>. Use the following script to fix incorrect
00210   xacro includes:
00211      sed -i 's/<include/<xacro:include/g' `find . -iname *.xacro`"""
00212 
00213 include_no_matches_msg = """Include tag filename spec \"{}\" matched no files."""
00214 
00215 
00216 ## @throws XacroException if a parsing error occurs with an included document
00217 def process_includes(doc, base_dir):
00218     namespaces = {}
00219     previous = doc.documentElement
00220     elt = next_element(previous)
00221     while elt:
00222         # Xacro should not use plain 'include' tags but only namespaced ones. Causes conflicts with
00223         # other XML elements including Gazebo's <gazebo> extensions
00224         is_include = False
00225         if elt.tagName == 'xacro:include' or elt.tagName == 'include':
00226 
00227             is_include = True
00228             # Temporary fix for ROS Hydro and the xacro include scope problem
00229             if elt.tagName == 'include':
00230 
00231                 # check if there is any element within the <include> tag. mostly we are concerned
00232                 # with Gazebo's <uri> element, but it could be anything. also, make sure the child
00233                 # nodes aren't just a single Text node, which is still considered a deprecated
00234                 # instance
00235                 if elt.childNodes and not (len(elt.childNodes) == 1 and
00236                                            elt.childNodes[0].nodeType == elt.TEXT_NODE):
00237                     # this is not intended to be a xacro element, so we can ignore it
00238                     is_include = False
00239                 else:
00240                     # throw a deprecated warning
00241                     print(deprecated_include_msg, file=sys.stderr)
00242 
00243         # Process current element depending on previous conditions
00244         if is_include:
00245             filename_spec = eval_text(elt.getAttribute('filename'), {})
00246             if not os.path.isabs(filename_spec):
00247                 filename_spec = os.path.join(base_dir, filename_spec)
00248 
00249             if re.search('[*[?]+', filename_spec):
00250                 # Globbing behaviour
00251                 filenames = sorted(glob.glob(filename_spec))
00252                 if len(filenames) == 0:
00253                     print(include_no_matches_msg.format(filename_spec), file=sys.stderr)
00254             else:
00255                 # Default behaviour
00256                 filenames = [filename_spec]
00257 
00258             for filename in filenames:
00259                 global all_includes
00260                 all_includes.append(filename)
00261                 try:
00262                     with open(filename) as f:
00263                         try:
00264                             included = parse(f)
00265                         except Exception as e:
00266                             raise XacroException(
00267                                 "included file \"%s\" generated an error during XML parsing: %s"
00268                                 % (filename, str(e)))
00269                 except IOError as e:
00270                     raise XacroException("included file \"%s\" could not be opened: %s" % (filename, str(e)))
00271 
00272                 # Replaces the include tag with the elements of the included file
00273                 for c in child_elements(included.documentElement):
00274                     elt.parentNode.insertBefore(c.cloneNode(1), elt)
00275 
00276                 # Grabs all the declared namespaces of the included document
00277                 for name, value in included.documentElement.attributes.items():
00278                     if name.startswith('xmlns:'):
00279                         namespaces[name] = value
00280 
00281             elt.parentNode.removeChild(elt)
00282             elt = None
00283         else:
00284             previous = elt
00285 
00286         elt = next_element(previous)
00287 
00288     # Makes sure the final document declares all the namespaces of the included documents.
00289     for k, v in namespaces.items():
00290         doc.documentElement.setAttribute(k, v)
00291 
00292 
00293 # Returns a dictionary: { macro_name => macro_xml_block }
00294 def grab_macros(doc):
00295     macros = {}
00296 
00297     previous = doc.documentElement
00298     elt = next_element(previous)
00299     while elt:
00300         if elt.tagName == 'macro' or elt.tagName == 'xacro:macro':
00301             name = elt.getAttribute('name')
00302 
00303             macros[name] = elt
00304             macros['xacro:' + name] = elt
00305 
00306             elt.parentNode.removeChild(elt)
00307             elt = None
00308         else:
00309             previous = elt
00310 
00311         elt = next_element(previous)
00312     return macros
00313 
00314 
00315 # Returns a Table of the properties
00316 def grab_properties(doc):
00317     table = Table()
00318 
00319     previous = doc.documentElement
00320     elt = next_element(previous)
00321     while elt:
00322         if elt.tagName == 'property' or elt.tagName == 'xacro:property':
00323             name = elt.getAttribute('name')
00324             value = None
00325 
00326             if elt.hasAttribute('value'):
00327                 value = elt.getAttribute('value')
00328             else:
00329                 name = '**' + name
00330                 value = elt  # debug
00331 
00332             bad = string.whitespace + "${}"
00333             has_bad = False
00334             for b in bad:
00335                 if b in name:
00336                     has_bad = True
00337                     break
00338 
00339             if has_bad:
00340                 sys.stderr.write('Property names may not have whitespace, ' +
00341                                  '"{", "}", or "$" : "' + name + '"')
00342             else:
00343                 table[name] = value
00344 
00345             elt.parentNode.removeChild(elt)
00346             elt = None
00347         else:
00348             previous = elt
00349 
00350         elt = next_element(previous)
00351     return table
00352 
00353 
00354 def eat_ignore(lex):
00355     while lex.peek() and lex.peek()[0] == lex.IGNORE:
00356         lex.next()
00357 
00358 
00359 def eval_lit(lex, symbols):
00360     eat_ignore(lex)
00361     if lex.peek()[0] == lex.NUMBER:
00362         return float(lex.next()[1])
00363     if lex.peek()[0] == lex.SYMBOL:
00364         try:
00365             value = symbols[lex.next()[1]]
00366         except KeyError as ex:
00367             raise XacroException("Property wasn't defined: %s" % str(ex))
00368         if not (isnumber(value) or isinstance(value, _basestr)):
00369             raise XacroException("WTF2")
00370         try:
00371             return int(value)
00372         except:
00373             try:
00374                 return float(value)
00375             except:
00376                 return value
00377     raise XacroException("Bad literal")
00378 
00379 
00380 def eval_factor(lex, symbols):
00381     eat_ignore(lex)
00382 
00383     neg = 1
00384     if lex.peek()[1] == '-':
00385         lex.next()
00386         neg = -1
00387 
00388     if lex.peek()[0] in [lex.NUMBER, lex.SYMBOL]:
00389         return neg * eval_lit(lex, symbols)
00390     if lex.peek()[0] == lex.LPAREN:
00391         lex.next()
00392         eat_ignore(lex)
00393         result = eval_expr(lex, symbols)
00394         eat_ignore(lex)
00395         if lex.next()[0] != lex.RPAREN:
00396             raise XacroException("Unmatched left paren")
00397         eat_ignore(lex)
00398         return neg * result
00399 
00400     raise XacroException("Misplaced operator")
00401 
00402 
00403 def eval_term(lex, symbols):
00404     eat_ignore(lex)
00405 
00406     result = 0
00407     if lex.peek()[0] in [lex.NUMBER, lex.SYMBOL, lex.LPAREN] \
00408             or lex.peek()[1] == '-':
00409         result = eval_factor(lex, symbols)
00410 
00411     eat_ignore(lex)
00412     while lex.peek() and lex.peek()[1] in ['*', '/']:
00413         op = lex.next()[1]
00414         n = eval_factor(lex, symbols)
00415 
00416         if op == '*':
00417             result = float(result) * float(n)
00418         elif op == '/':
00419             result = float(result) / float(n)
00420         else:
00421             raise XacroException("WTF")
00422         eat_ignore(lex)
00423     return result
00424 
00425 
00426 def eval_expr(lex, symbols):
00427     eat_ignore(lex)
00428 
00429     op = None
00430     if lex.peek()[0] == lex.OP:
00431         op = lex.next()[1]
00432         if not op in ['+', '-']:
00433             raise XacroException("Invalid operation. Must be '+' or '-'")
00434 
00435     result = eval_term(lex, symbols)
00436     if op == '-':
00437         result = -float(result)
00438 
00439     eat_ignore(lex)
00440     while lex.peek() and lex.peek()[1] in ['+', '-']:
00441         op = lex.next()[1]
00442         n = eval_term(lex, symbols)
00443 
00444         if op == '+':
00445             result = float(result) + float(n)
00446         if op == '-':
00447             result = float(result) - float(n)
00448         eat_ignore(lex)
00449     return result
00450 
00451 
00452 def eval_text(text, symbols):
00453     def handle_expr(s):
00454         lex = QuickLexer(IGNORE=r"\s+",
00455                          NUMBER=r"(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?",
00456                          SYMBOL=r"[a-zA-Z_]\w*",
00457                          OP=r"[\+\-\*/^]",
00458                          LPAREN=r"\(",
00459                          RPAREN=r"\)")
00460         lex.lex(s)
00461         return eval_expr(lex, symbols)
00462 
00463     def handle_extension(s):
00464         return eval_extension("$(%s)" % s)
00465 
00466     results = []
00467     lex = QuickLexer(DOLLAR_DOLLAR_BRACE=r"\$\$+\{",
00468                      EXPR=r"\$\{[^\}]*\}",
00469                      EXTENSION=r"\$\([^\)]*\)",
00470                      TEXT=r"([^\$]|\$[^{(]|\$$)+")
00471     lex.lex(text)
00472     while lex.peek():
00473         if lex.peek()[0] == lex.EXPR:
00474             results.append(handle_expr(lex.next()[1][2:-1]))
00475         elif lex.peek()[0] == lex.EXTENSION:
00476             results.append(handle_extension(lex.next()[1][2:-1]))
00477         elif lex.peek()[0] == lex.TEXT:
00478             results.append(lex.next()[1])
00479         elif lex.peek()[0] == lex.DOLLAR_DOLLAR_BRACE:
00480             results.append(lex.next()[1][1:])
00481     return ''.join(map(str, results))
00482 
00483 
00484 # Expands macros, replaces properties, and evaluates expressions
00485 def eval_all(root, macros, symbols):
00486     # Evaluates the attributes for the root node
00487     for at in root.attributes.items():
00488         result = eval_text(at[1], symbols)
00489         root.setAttribute(at[0], result)
00490 
00491     previous = root
00492     node = next_node(previous)
00493     while node:
00494         if node.nodeType == xml.dom.Node.ELEMENT_NODE:
00495             if node.tagName in macros:
00496                 body = macros[node.tagName].cloneNode(deep=True)
00497                 params = body.getAttribute('params').split()
00498 
00499                 # Expands the macro
00500                 scoped = Table(symbols)
00501                 for name, value in node.attributes.items():
00502                     if not name in params:
00503                         raise XacroException("Invalid parameter \"%s\" while expanding macro \"%s\"" %
00504                                              (str(name), str(node.tagName)))
00505                     params.remove(name)
00506                     scoped[name] = eval_text(value, symbols)
00507 
00508                 # Pulls out the block arguments, in order
00509                 cloned = node.cloneNode(deep=True)
00510                 eval_all(cloned, macros, symbols)
00511                 block = cloned.firstChild
00512                 for param in params[:]:
00513                     if param[0] == '*':
00514                         while block and block.nodeType != xml.dom.Node.ELEMENT_NODE:
00515                             block = block.nextSibling
00516                         if not block:
00517                             raise XacroException("Not enough blocks while evaluating macro %s" % str(node.tagName))
00518                         params.remove(param)
00519                         scoped[param] = block
00520                         block = block.nextSibling
00521 
00522                 if params:
00523                     raise XacroException("Some parameters were not set for macro %s" %
00524                                          str(node.tagName))
00525                 eval_all(body, macros, scoped)
00526 
00527                 # Replaces the macro node with the expansion
00528                 for e in list(child_elements(body)):  # Ew
00529                     node.parentNode.insertBefore(e, node)
00530                 node.parentNode.removeChild(node)
00531 
00532                 node = None
00533             elif node.tagName == 'insert_block' or node.tagName == 'xacro:insert_block':
00534                 name = node.getAttribute('name')
00535 
00536                 if ("**" + name) in symbols:
00537                     # Multi-block
00538                     block = symbols['**' + name]
00539 
00540                     for e in list(child_elements(block)):
00541                         node.parentNode.insertBefore(e.cloneNode(deep=True), node)
00542                     node.parentNode.removeChild(node)
00543                 elif ("*" + name) in symbols:
00544                     # Single block
00545                     block = symbols['*' + name]
00546 
00547                     node.parentNode.insertBefore(block.cloneNode(deep=True), node)
00548                     node.parentNode.removeChild(node)
00549                 else:
00550                     raise XacroException("Block \"%s\" was never declared" % name)
00551 
00552                 node = None
00553             elif node.tagName == 'if' or node.tagName == 'xacro:if':
00554                 value = eval_text(node.getAttribute('value'), symbols)
00555                 try:
00556                     value = int(float(value))
00557                 except ValueError:
00558                     pass
00559                 if value == 1 or value == 'true':
00560                     for e in list(child_elements(node)):
00561                         cloned = node.cloneNode(deep=True)
00562                         eval_all(cloned, macros, symbols)
00563                         node.parentNode.insertBefore(e, node)
00564                 elif value != 0 and value != 'false':
00565                     raise XacroException("""\
00566 Xacro conditional evaluated to \"%s\". Acceptable evaluations are one of [\"1\",\"true\",\"0\",\"false\"]\
00567 """ % value)
00568                 node.parentNode.removeChild(node)
00569             elif node.tagName == 'unless' or node.tagName == 'xacro:unless':
00570                 value = eval_text(node.getAttribute('value'), symbols)
00571                 if value == 0 or value == 'false':
00572                     for e in list(child_elements(node)):
00573                         cloned = node.cloneNode(deep=True)
00574                         eval_all(cloned, macros, symbols)
00575                         node.parentNode.insertBefore(e, node)
00576                 elif value != 1 and value != 'true':
00577                     raise XacroException("""\
00578 Xacro conditional evaluated to \"%s\". Acceptable evaluations are one of [\"1\",\"true\",\"0\",\"false\"]\
00579 """ % value)
00580                 node.parentNode.removeChild(node)
00581             else:
00582                 # Evals the attributes
00583                 for at in node.attributes.items():
00584                     result = eval_text(at[1], symbols)
00585                     node.setAttribute(at[0], result)
00586                 previous = node
00587         elif node.nodeType == xml.dom.Node.TEXT_NODE:
00588             node.data = eval_text(node.data, symbols)
00589             previous = node
00590         else:
00591             previous = node
00592 
00593         node = next_node(previous)
00594     return macros
00595 
00596 
00597 # Expands everything except includes
00598 def eval_self_contained(doc):
00599     macros = grab_macros(doc)
00600     symbols = grab_properties(doc)
00601     eval_all(doc.documentElement, macros, symbols)
00602 
00603 
00604 def print_usage(exit_code=0):
00605     print("Usage: %s [-o <output>] <input>" % 'xacro.py')
00606     print("       %s --deps       Prints dependencies" % 'xacro.py')
00607     print("       %s --includes   Only evalutes includes" % 'xacro.py')
00608     sys.exit(exit_code)
00609 
00610 
00611 def set_substitution_args_context(context={}):
00612     substitution_args_context['arg'] = context
00613 
00614 
00615 def main():
00616     try:
00617         opts, args = getopt.gnu_getopt(sys.argv[1:], "ho:", ['deps', 'includes'])
00618     except getopt.GetoptError as err:
00619         print(str(err))
00620         print_usage(2)
00621 
00622     just_deps = False
00623     just_includes = False
00624 
00625     output = sys.stdout
00626     for o, a in opts:
00627         if o == '-h':
00628             print_usage(0)
00629         elif o == '-o':
00630             output = open(a, 'w')
00631         elif o == '--deps':
00632             just_deps = True
00633         elif o == '--includes':
00634             just_includes = True
00635 
00636     if len(args) < 1:
00637         print("No input given")
00638         print_usage(2)
00639 
00640     # Process substitution args
00641     set_substitution_args_context(load_mappings(sys.argv))
00642 
00643     f = open(args[0])
00644     doc = None
00645     try:
00646         doc = parse(f)
00647     except xml.parsers.expat.ExpatError:
00648         sys.stderr.write("Expat parsing error.  Check that:\n")
00649         sys.stderr.write(" - Your XML is correctly formed\n")
00650         sys.stderr.write(" - You have the xacro xmlns declaration: " +
00651                          "xmlns:xacro=\"http://www.ros.org/wiki/xacro\"\n")
00652         sys.stderr.write("\n")
00653         raise
00654     finally:
00655         f.close()
00656 
00657     process_includes(doc, os.path.dirname(args[0]))
00658     if just_deps:
00659         for inc in all_includes:
00660             sys.stdout.write(inc + " ")
00661         sys.stdout.write("\n")
00662     elif just_includes:
00663         doc.writexml(output)
00664         print()
00665     else:
00666         eval_self_contained(doc)
00667         banner = [xml.dom.minidom.Comment(c) for c in
00668                   [" %s " % ('=' * 83),
00669                    " |    This document was autogenerated by xacro from %-30s | " % args[0],
00670                    " |    EDITING THIS FILE BY HAND IS NOT RECOMMENDED  %-30s | " % "",
00671                    " %s " % ('=' * 83)]]
00672         first = doc.firstChild
00673         for comment in banner:
00674             doc.insertBefore(comment, first)
00675 
00676         output.write(doc.toprettyxml(indent='  '))
00677         print()


xacro
Author(s): Stuart Glaser
autogenerated on Thu Aug 27 2015 15:44:55