diff --git a/data/xacro_standalone.py b/data/xacro_standalone.py new file mode 100755 index 000000000..a1e77f6f9 --- /dev/null +++ b/data/xacro_standalone.py @@ -0,0 +1,705 @@ +#! /usr/bin/env python +# Copyright (c) 2013, Willow Garage, Inc. +# Copyright (c) 2014, Open Source Robotics Foundation, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the Open Source Robotics Foundation, Inc. +# nor the names of its contributors may be used to endorse or promote +# products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# Author: Stuart Glaser +# Maintainer: William Woodall + +from __future__ import print_function + +import getopt +import glob +import os +import re +import string +import sys +import xml + +from xml.dom.minidom import parse + +try: + _basestr = basestring +except NameError: + _basestr = str + +# Dictionary of subtitution args +substitution_args_context = {} + + +class XacroException(Exception): + pass + + +def isnumber(x): + return hasattr(x, '__int__') + + +# Better pretty printing of xml +# Taken from http://ronrothman.com/public/leftbraned/xml-dom-minidom-toprettyxml-and-silly-whitespace/ +def fixed_writexml(self, writer, indent="", addindent="", newl=""): + # indent = current indentation + # addindent = indentation to add to higher levels + # newl = newline string + writer.write(indent + "<" + self.tagName) + + attrs = self._get_attributes() + a_names = list(attrs.keys()) + a_names.sort() + + for a_name in a_names: + writer.write(" %s=\"" % a_name) + xml.dom.minidom._write_data(writer, attrs[a_name].value) + writer.write("\"") + if self.childNodes: + if len(self.childNodes) == 1 \ + and self.childNodes[0].nodeType == xml.dom.minidom.Node.TEXT_NODE: + writer.write(">") + self.childNodes[0].writexml(writer, "", "", "") + writer.write("%s" % (self.tagName, newl)) + return + writer.write(">%s" % (newl)) + for node in self.childNodes: + # skip whitespace-only text nodes + if node.nodeType == xml.dom.minidom.Node.TEXT_NODE and \ + not node.data.strip(): + continue + node.writexml(writer, indent + addindent, addindent, newl) + writer.write("%s%s" % (indent, self.tagName, newl)) + else: + writer.write("/>%s" % (newl)) +# replace minidom's function with ours +xml.dom.minidom.Element.writexml = fixed_writexml + + +class Table: + def __init__(self, parent=None): + self.parent = parent + self.table = {} + + def __getitem__(self, key): + if key in self.table: + return self.table[key] + elif self.parent: + return self.parent[key] + else: + raise KeyError(key) + + def __setitem__(self, key, value): + self.table[key] = value + + def __contains__(self, key): + return \ + key in self.table or \ + (self.parent and key in self.parent) + + +class QuickLexer(object): + def __init__(self, **res): + self.str = "" + self.top = None + self.res = [] + for k, v in res.items(): + self.__setattr__(k, len(self.res)) + self.res.append(v) + + def lex(self, str): + self.str = str + self.top = None + self.next() + + def peek(self): + return self.top + + def next(self): + result = self.top + self.top = None + for i in range(len(self.res)): + m = re.match(self.res[i], self.str) + if m: + self.top = (i, m.group(0)) + self.str = self.str[m.end():] + break + return result + + +def first_child_element(elt): + c = elt.firstChild + while c: + if c.nodeType == xml.dom.Node.ELEMENT_NODE: + return c + c = c.nextSibling + return None + + +def next_sibling_element(elt): + c = elt.nextSibling + while c: + if c.nodeType == xml.dom.Node.ELEMENT_NODE: + return c + c = c.nextSibling + return None + + +# Pre-order traversal of the elements +def next_element(elt): + child = first_child_element(elt) + if child: + return child + while elt and elt.nodeType == xml.dom.Node.ELEMENT_NODE: + next = next_sibling_element(elt) + if next: + return next + elt = elt.parentNode + return None + + +# Pre-order traversal of all the nodes +def next_node(node): + if node.firstChild: + return node.firstChild + while node: + if node.nextSibling: + return node.nextSibling + node = node.parentNode + return None + + +def child_nodes(elt): + c = elt.firstChild + while c: + yield c + c = c.nextSibling + +all_includes = [] + +# Deprecated message for tags that don't have prepended: +deprecated_include_msg = """DEPRECATED IN HYDRO: + The tag should be prepended with 'xacro' if that is the intended use + of it, such as . Use the following script to fix incorrect + xacro includes: + sed -i 's/ extensions + is_include = False + if elt.tagName == 'xacro:include' or elt.tagName == 'include': + + is_include = True + # Temporary fix for ROS Hydro and the xacro include scope problem + if elt.tagName == 'include': + + # check if there is any element within the tag. mostly we are concerned + # with Gazebo's element, but it could be anything. also, make sure the child + # nodes aren't just a single Text node, which is still considered a deprecated + # instance + if elt.childNodes and not (len(elt.childNodes) == 1 and + elt.childNodes[0].nodeType == elt.TEXT_NODE): + # this is not intended to be a xacro element, so we can ignore it + is_include = False + else: + # throw a deprecated warning + print(deprecated_include_msg, file=sys.stderr) + + # Process current element depending on previous conditions + if is_include: + filename_spec = eval_text(elt.getAttribute('filename'), {}) + if not os.path.isabs(filename_spec): + filename_spec = os.path.join(base_dir, filename_spec) + + if re.search('[*[?]+', filename_spec): + # Globbing behaviour + filenames = sorted(glob.glob(filename_spec)) + if len(filenames) == 0: + print(include_no_matches_msg.format(filename_spec), file=sys.stderr) + else: + # Default behaviour + filenames = [filename_spec] + + for filename in filenames: + global all_includes + all_includes.append(filename) + try: + with open(filename) as f: + try: + included = parse(f) + except Exception as e: + raise XacroException( + "included file \"%s\" generated an error during XML parsing: %s" + % (filename, str(e))) + except IOError as e: + raise XacroException("included file \"%s\" could not be opened: %s" % (filename, str(e))) + + # Replaces the include tag with the elements of the included file + for c in child_nodes(included.documentElement): + elt.parentNode.insertBefore(c.cloneNode(deep=True), elt) + + # Grabs all the declared namespaces of the included document + for name, value in included.documentElement.attributes.items(): + if name.startswith('xmlns:'): + namespaces[name] = value + + elt.parentNode.removeChild(elt) + elt = None + else: + previous = elt + + elt = next_element(previous) + + # Makes sure the final document declares all the namespaces of the included documents. + for k, v in namespaces.items(): + doc.documentElement.setAttribute(k, v) + + +# Returns a dictionary: { macro_name => macro_xml_block } +def grab_macros(doc): + macros = {} + + previous = doc.documentElement + elt = next_element(previous) + while elt: + if elt.tagName == 'macro' or elt.tagName == 'xacro:macro': + name = elt.getAttribute('name') + + macros[name] = elt + macros['xacro:' + name] = elt + + elt.parentNode.removeChild(elt) + elt = None + else: + previous = elt + + elt = next_element(previous) + return macros + + +# Returns a Table of the properties +def grab_properties(doc): + table = Table() + + previous = doc.documentElement + elt = next_element(previous) + while elt: + if elt.tagName == 'property' or elt.tagName == 'xacro:property': + name = elt.getAttribute('name') + value = None + + if elt.hasAttribute('value'): + value = elt.getAttribute('value') + else: + name = '**' + name + value = elt # debug + + bad = string.whitespace + "${}" + has_bad = False + for b in bad: + if b in name: + has_bad = True + break + + if has_bad: + sys.stderr.write('Property names may not have whitespace, ' + + '"{", "}", or "$" : "' + name + '"') + else: + table[name] = value + + elt.parentNode.removeChild(elt) + elt = None + else: + previous = elt + + elt = next_element(previous) + return table + + +def eat_ignore(lex): + while lex.peek() and lex.peek()[0] == lex.IGNORE: + lex.next() + + +def eval_lit(lex, symbols): + eat_ignore(lex) + if lex.peek()[0] == lex.NUMBER: + return float(lex.next()[1]) + if lex.peek()[0] == lex.SYMBOL: + try: + key = lex.next()[1] + value = symbols[key] + except KeyError as ex: + raise XacroException("Property wasn't defined: %s" % str(ex)) + if not (isnumber(value) or isinstance(value, _basestr)): + if value is None: + raise XacroException("Property %s recursively used" % key) + raise XacroException("WTF2") + try: + return int(value) + except: + try: + return float(value) + except: + # prevent infinite recursion + symbols[key] = None + result = eval_text(value, symbols) + # restore old entry + symbols[key] = value + return result + raise XacroException("Bad literal") + + +def eval_factor(lex, symbols): + eat_ignore(lex) + + neg = 1 + if lex.peek()[1] == '-': + lex.next() + neg = -1 + + if lex.peek()[0] in [lex.NUMBER, lex.SYMBOL]: + return neg * eval_lit(lex, symbols) + if lex.peek()[0] == lex.LPAREN: + lex.next() + eat_ignore(lex) + result = eval_expr(lex, symbols) + eat_ignore(lex) + if lex.next()[0] != lex.RPAREN: + raise XacroException("Unmatched left paren") + eat_ignore(lex) + return neg * result + + raise XacroException("Misplaced operator") + + +def eval_term(lex, symbols): + eat_ignore(lex) + + result = 0 + if lex.peek()[0] in [lex.NUMBER, lex.SYMBOL, lex.LPAREN] \ + or lex.peek()[1] == '-': + result = eval_factor(lex, symbols) + + eat_ignore(lex) + while lex.peek() and lex.peek()[1] in ['*', '/']: + op = lex.next()[1] + n = eval_factor(lex, symbols) + + if op == '*': + result = float(result) * float(n) + elif op == '/': + result = float(result) / float(n) + else: + raise XacroException("WTF") + eat_ignore(lex) + return result + + +def eval_expr(lex, symbols): + eat_ignore(lex) + + op = None + if lex.peek()[0] == lex.OP: + op = lex.next()[1] + if not op in ['+', '-']: + raise XacroException("Invalid operation. Must be '+' or '-'") + + result = eval_term(lex, symbols) + if op == '-': + result = -float(result) + + eat_ignore(lex) + while lex.peek() and lex.peek()[1] in ['+', '-']: + op = lex.next()[1] + n = eval_term(lex, symbols) + + if op == '+': + result = float(result) + float(n) + if op == '-': + result = float(result) - float(n) + eat_ignore(lex) + return result + + +def eval_text(text, symbols): + def handle_expr(s): + lex = QuickLexer(IGNORE=r"\s+", + NUMBER=r"(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?", + SYMBOL=r"[a-zA-Z_]\w*", + OP=r"[\+\-\*/^]", + LPAREN=r"\(", + RPAREN=r"\)") + lex.lex(s) + return eval_expr(lex, symbols) + + def handle_extension(s): + return ("$(%s)" % s) + + results = [] + lex = QuickLexer(DOLLAR_DOLLAR_BRACE=r"\$\$+\{", + EXPR=r"\$\{[^\}]*\}", + EXTENSION=r"\$\([^\)]*\)", + TEXT=r"([^\$]|\$[^{(]|\$$)+") + lex.lex(text) + while lex.peek(): + if lex.peek()[0] == lex.EXPR: + results.append(handle_expr(lex.next()[1][2:-1])) + elif lex.peek()[0] == lex.EXTENSION: + results.append(handle_extension(lex.next()[1][2:-1])) + elif lex.peek()[0] == lex.TEXT: + results.append(lex.next()[1]) + elif lex.peek()[0] == lex.DOLLAR_DOLLAR_BRACE: + results.append(lex.next()[1][1:]) + return ''.join(map(str, results)) + + +# Expands macros, replaces properties, and evaluates expressions +def eval_all(root, macros, symbols): + # Evaluates the attributes for the root node + for at in root.attributes.items(): + result = eval_text(at[1], symbols) + root.setAttribute(at[0], result) + + previous = root + node = next_node(previous) + while node: + if node.nodeType == xml.dom.Node.ELEMENT_NODE: + if node.tagName in macros: + body = macros[node.tagName].cloneNode(deep=True) + params = body.getAttribute('params').split() + + # Parse default values for any parameters + defaultmap = {} + for param in params[:]: + splitParam = param.split(':=') + + if len(splitParam) == 2: + defaultmap[splitParam[0]] = splitParam[1] + params.remove(param) + params.append(splitParam[0]) + + elif len(splitParam) != 1: + raise XacroException("Invalid parameter definition") + + # Expands the macro + scoped = Table(symbols) + for name, value in node.attributes.items(): + if not name in params: + raise XacroException("Invalid parameter \"%s\" while expanding macro \"%s\"" % + (str(name), str(node.tagName))) + params.remove(name) + scoped[name] = eval_text(value, symbols) + + # Pulls out the block arguments, in order + cloned = node.cloneNode(deep=True) + eval_all(cloned, macros, symbols) + block = cloned.firstChild + for param in params[:]: + if param[0] == '*': + while block and block.nodeType != xml.dom.Node.ELEMENT_NODE: + block = block.nextSibling + if not block: + raise XacroException("Not enough blocks while evaluating macro %s" % str(node.tagName)) + params.remove(param) + scoped[param] = block + block = block.nextSibling + + # Try to load defaults for any remaining non-block parameters + for param in params[:]: + if param[0] != '*' and param in defaultmap: + scoped[param] = defaultmap[param] + params.remove(param) + + if params: + raise XacroException("Parameters [%s] were not set for macro %s" % + (",".join(params), str(node.tagName))) + eval_all(body, macros, scoped) + + # Replaces the macro node with the expansion + for e in list(child_nodes(body)): # Ew + node.parentNode.insertBefore(e, node) + node.parentNode.removeChild(node) + + node = None + elif node.tagName == 'arg' or node.tagName == 'xacro:arg': + name = node.getAttribute('name') + if not name: + raise XacroException("Argument name missing") + default = node.getAttribute('default') + if default and name not in substitution_args_context['arg']: + substitution_args_context['arg'][name] = default + + node.parentNode.removeChild(node) + node = None + + elif node.tagName == 'insert_block' or node.tagName == 'xacro:insert_block': + name = node.getAttribute('name') + + if ("**" + name) in symbols: + # Multi-block + block = symbols['**' + name] + + for e in list(child_nodes(block)): + node.parentNode.insertBefore(e.cloneNode(deep=True), node) + node.parentNode.removeChild(node) + elif ("*" + name) in symbols: + # Single block + block = symbols['*' + name] + + node.parentNode.insertBefore(block.cloneNode(deep=True), node) + node.parentNode.removeChild(node) + else: + raise XacroException("Block \"%s\" was never declared" % name) + + node = None + elif node.tagName in ['if', 'xacro:if', 'unless', 'xacro:unless']: + value = eval_text(node.getAttribute('value'), symbols) + try: + if value == 'true': keep = True + elif value == 'false': keep = False + else: keep = float(value) + except ValueError: + raise XacroException("Xacro conditional evaluated to \"%s\". Acceptable evaluations are one of [\"1\",\"true\",\"0\",\"false\"]" % value) + if node.tagName in ['unless', 'xacro:unless']: keep = not keep + if keep: + for e in list(child_nodes(node)): + node.parentNode.insertBefore(e.cloneNode(deep=True), node) + + node.parentNode.removeChild(node) + else: + # Evals the attributes + for at in node.attributes.items(): + result = eval_text(at[1], symbols) + node.setAttribute(at[0], result) + previous = node + elif node.nodeType == xml.dom.Node.TEXT_NODE: + node.data = eval_text(node.data, symbols) + previous = node + else: + previous = node + + node = next_node(previous) + return macros + + +# Expands everything except includes +def eval_self_contained(doc): + macros = grab_macros(doc) + symbols = grab_properties(doc) + eval_all(doc.documentElement, macros, symbols) + + +def print_usage(exit_code=0): + print("Usage: %s [-o ] " % 'xacro.py') + print(" %s --deps Prints dependencies" % 'xacro.py') + print(" %s --includes Only evalutes includes" % 'xacro.py') + sys.exit(exit_code) + + +def set_substitution_args_context(context={}): + substitution_args_context['arg'] = context + +def open_output(output_filename): + if output_filename is None: + return sys.stdout + else: + return open(output_filename, 'w') + +def main(): + try: + opts, args = getopt.gnu_getopt(sys.argv[1:], "ho:", ['deps', 'includes']) + except getopt.GetoptError as err: + print(str(err)) + print_usage(2) + + just_deps = False + just_includes = False + + output_filename = None + for o, a in opts: + if o == '-h': + print_usage(0) + elif o == '-o': + output_filename = a + elif o == '--deps': + just_deps = True + elif o == '--includes': + just_includes = True + + if len(args) < 1: + print("No input given") + print_usage(2) + + # Process substitution args + # set_substitution_args_context(load_mappings(sys.argv)) + set_substitution_args_context((sys.argv)) + + f = open(args[0]) + doc = None + try: + doc = parse(f) + except xml.parsers.expat.ExpatError: + sys.stderr.write("Expat parsing error. Check that:\n") + sys.stderr.write(" - Your XML is correctly formed\n") + sys.stderr.write(" - You have the xacro xmlns declaration: " + + "xmlns:xacro=\"http://www.ros.org/wiki/xacro\"\n") + sys.stderr.write("\n") + raise + finally: + f.close() + + process_includes(doc, os.path.dirname(args[0])) + if just_deps: + for inc in all_includes: + sys.stdout.write(inc + " ") + sys.stdout.write("\n") + elif just_includes: + doc.writexml(open_output(output_filename)) + print() + else: + eval_self_contained(doc) + banner = [xml.dom.minidom.Comment(c) for c in + [" %s " % ('=' * 83), + " | This document was autogenerated by xacro from %-30s | " % args[0], + " | EDITING THIS FILE BY HAND IS NOT RECOMMENDED %-30s | " % "", + " %s " % ('=' * 83)]] + first = doc.firstChild + for comment in banner: + doc.insertBefore(comment, first) + + open_output(output_filename).write(doc.toprettyxml(indent=' ')) + print() + +if __name__ == '__main__': + main()