123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705 |
- #! /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 <[email protected]>
- 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>%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>%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 <include> tags that don't have <xacro:include> prepended:
- deprecated_include_msg = """DEPRECATED IN HYDRO:
- The <include> tag should be prepended with 'xacro' if that is the intended use
- of it, such as <xacro:include ...>. Use the following script to fix incorrect
- xacro includes:
- sed -i 's/<include/<xacro:include/g' `find . -iname *.xacro`"""
- include_no_matches_msg = """Include tag filename spec \"{}\" matched no files."""
- ## @throws XacroException if a parsing error occurs with an included document
- def process_includes(doc, base_dir):
- namespaces = {}
- previous = doc.documentElement
- elt = next_element(previous)
- while elt:
- # Xacro should not use plain 'include' tags but only namespaced ones. Causes conflicts with
- # other XML elements including Gazebo's <gazebo> 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 <include> tag. mostly we are concerned
- # with Gazebo's <uri> 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 <output>] <input>" % '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()
|