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


xacro
Author(s): Stuart Glaser
autogenerated on Mon Oct 6 2014 09:05:11