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


xacro
Author(s): Stuart Glaser
autogenerated on Mon Aug 19 2013 11:16:58