xacro.py
Go to the documentation of this file.
1 # Copyright (c) 2013, Willow Garage, Inc.
2 # Copyright (c) 2014, Open Source Robotics Foundation, Inc.
3 # All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are met:
7 #
8 # * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above copyright
11 # notice, this list of conditions and the following disclaimer in the
12 # documentation and/or other materials provided with the distribution.
13 # * Neither the name of the Open Source Robotics Foundation, Inc.
14 # nor the names of its contributors may be used to endorse or promote
15 # products derived from this software without specific prior
16 # written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
22 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28 # POSSIBILITY OF SUCH DAMAGE.
29 
30 # Author: Stuart Glaser
31 # Maintainer: William Woodall <william@osrfoundation.org>
32 
33 from __future__ import print_function
34 
35 import getopt
36 import glob
37 import os
38 import re
39 import string
40 import sys
41 import xml
42 
43 from xml.dom.minidom import parse
44 
45 import substitution_args
46 from names import load_mappings
47 
48 try:
49  _basestr = basestring
50 except NameError:
51  _basestr = str
52 
53 # Dictionary of subtitution args
54 substitution_args_context = {}
55 
56 
57 class XacroException(Exception):
58  pass
59 
60 
61 def isnumber(x):
62  return hasattr(x, '__int__')
63 
64 
65 def eval_extension(str):
66  return substitution_args.resolve_args(str, context=substitution_args_context, resolve_anon=False)
67 
68 
69 # Better pretty printing of xml
70 # Taken from http://ronrothman.com/public/leftbraned/xml-dom-minidom-toprettyxml-and-silly-whitespace/
71 def fixed_writexml(self, writer, indent="", addindent="", newl=""):
72  # indent = current indentation
73  # addindent = indentation to add to higher levels
74  # newl = newline string
75  writer.write(indent + "<" + self.tagName)
76 
77  attrs = self._get_attributes()
78  a_names = list(attrs.keys())
79  a_names.sort()
80 
81  for a_name in a_names:
82  writer.write(" %s=\"" % a_name)
83  xml.dom.minidom._write_data(writer, attrs[a_name].value)
84  writer.write("\"")
85  if self.childNodes:
86  if len(self.childNodes) == 1 \
87  and self.childNodes[0].nodeType == xml.dom.minidom.Node.TEXT_NODE:
88  writer.write(">")
89  self.childNodes[0].writexml(writer, "", "", "")
90  writer.write("</%s>%s" % (self.tagName, newl))
91  return
92  writer.write(">%s" % (newl))
93  for node in self.childNodes:
94  # skip whitespace-only text nodes
95  if node.nodeType == xml.dom.minidom.Node.TEXT_NODE and \
96  not node.data.strip():
97  continue
98  node.writexml(writer, indent + addindent, addindent, newl)
99  writer.write("%s</%s>%s" % (indent, self.tagName, newl))
100  else:
101  writer.write("/>%s" % (newl))
102 # replace minidom's function with ours
103 xml.dom.minidom.Element.writexml = fixed_writexml
104 
105 
106 class Table:
107  def __init__(self, parent=None):
108  self.parent = parent
109  self.table = {}
110 
111  def __getitem__(self, key):
112  if key in self.table:
113  return self.table[key]
114  elif self.parent:
115  return self.parent[key]
116  else:
117  raise KeyError(key)
118 
119  def __setitem__(self, key, value):
120  self.table[key] = value
121 
122  def __contains__(self, key):
123  return \
124  key in self.table or \
125  (self.parent and key in self.parent)
126 
127 
128 class QuickLexer(object):
129  def __init__(self, **res):
130  self.str = ""
131  self.top = None
132  self.res = []
133  for k, v in res.items():
134  self.__setattr__(k, len(self.res))
135  self.res.append(v)
136 
137  def lex(self, str):
138  self.str = str
139  self.top = None
140  self.next()
141 
142  def peek(self):
143  return self.top
144 
145  def next(self):
146  result = self.top
147  self.top = None
148  for i in range(len(self.res)):
149  m = re.match(self.res[i], self.str)
150  if m:
151  self.top = (i, m.group(0))
152  self.str = self.str[m.end():]
153  break
154  return result
155 
156 
158  c = elt.firstChild
159  while c:
160  if c.nodeType == xml.dom.Node.ELEMENT_NODE:
161  return c
162  c = c.nextSibling
163  return None
164 
165 
167  c = elt.nextSibling
168  while c:
169  if c.nodeType == xml.dom.Node.ELEMENT_NODE:
170  return c
171  c = c.nextSibling
172  return None
173 
174 
175 # Pre-order traversal of the elements
176 def next_element(elt):
177  child = first_child_element(elt)
178  if child:
179  return child
180  while elt and elt.nodeType == xml.dom.Node.ELEMENT_NODE:
181  next = next_sibling_element(elt)
182  if next:
183  return next
184  elt = elt.parentNode
185  return None
186 
187 
188 # Pre-order traversal of all the nodes
189 def next_node(node):
190  if node.firstChild:
191  return node.firstChild
192  while node:
193  if node.nextSibling:
194  return node.nextSibling
195  node = node.parentNode
196  return None
197 
198 
199 def child_nodes(elt):
200  c = elt.firstChild
201  while c:
202  yield c
203  c = c.nextSibling
204 
205 all_includes = []
206 
207 # Deprecated message for <include> tags that don't have <xacro:include> prepended:
208 deprecated_include_msg = """DEPRECATED IN HYDRO:
209  The <include> tag should be prepended with 'xacro' if that is the intended use
210  of it, such as <xacro:include ...>. Use the following script to fix incorrect
211  xacro includes:
212  sed -i 's/<include/<xacro:include/g' `find . -iname *.xacro`"""
213 
214 include_no_matches_msg = """Include tag filename spec \"{}\" matched no files."""
215 
216 
217 
218 def process_includes(doc, base_dir):
219  namespaces = {}
220  previous = doc.documentElement
221  elt = next_element(previous)
222  while elt:
223  # Xacro should not use plain 'include' tags but only namespaced ones. Causes conflicts with
224  # other XML elements including Gazebo's <gazebo> extensions
225  is_include = False
226  if elt.tagName == 'xacro:include' or elt.tagName == 'include':
227 
228  is_include = True
229  # Temporary fix for ROS Hydro and the xacro include scope problem
230  if elt.tagName == 'include':
231 
232  # check if there is any element within the <include> tag. mostly we are concerned
233  # with Gazebo's <uri> element, but it could be anything. also, make sure the child
234  # nodes aren't just a single Text node, which is still considered a deprecated
235  # instance
236  if elt.childNodes and not (len(elt.childNodes) == 1 and
237  elt.childNodes[0].nodeType == elt.TEXT_NODE):
238  # this is not intended to be a xacro element, so we can ignore it
239  is_include = False
240  else:
241  # throw a deprecated warning
242  print(deprecated_include_msg, file=sys.stderr)
243 
244  # Process current element depending on previous conditions
245  if is_include:
246  filename_spec = eval_text(elt.getAttribute('filename'), {})
247  if not os.path.isabs(filename_spec):
248  filename_spec = os.path.join(base_dir, filename_spec)
249 
250  if re.search('[*[?]+', filename_spec):
251  # Globbing behaviour
252  filenames = sorted(glob.glob(filename_spec))
253  if len(filenames) == 0:
254  print(include_no_matches_msg.format(filename_spec), file=sys.stderr)
255  else:
256  # Default behaviour
257  filenames = [filename_spec]
258 
259  for filename in filenames:
260  global all_includes
261  all_includes.append(filename)
262  try:
263  with open(filename) as f:
264  try:
265  included = parse(f)
266  except Exception as e:
267  raise XacroException(
268  "included file \"%s\" generated an error during XML parsing: %s"
269  % (filename, str(e)))
270  except IOError as e:
271  raise XacroException("included file \"%s\" could not be opened: %s" % (filename, str(e)))
272 
273  # Replaces the include tag with the elements of the included file
274  for c in child_nodes(included.documentElement):
275  elt.parentNode.insertBefore(c.cloneNode(deep=True), elt)
276 
277  # Grabs all the declared namespaces of the included document
278  for name, value in included.documentElement.attributes.items():
279  if name.startswith('xmlns:'):
280  namespaces[name] = value
281 
282  elt.parentNode.removeChild(elt)
283  elt = None
284  else:
285  previous = elt
286 
287  elt = next_element(previous)
288 
289  # Makes sure the final document declares all the namespaces of the included documents.
290  for k, v in namespaces.items():
291  doc.documentElement.setAttribute(k, v)
292 
293 
294 # Returns a dictionary: { macro_name => macro_xml_block }
295 def grab_macros(doc):
296  macros = {}
297 
298  previous = doc.documentElement
299  elt = next_element(previous)
300  while elt:
301  if elt.tagName == 'macro' or elt.tagName == 'xacro:macro':
302  name = elt.getAttribute('name')
303 
304  macros[name] = elt
305  macros['xacro:' + name] = elt
306 
307  elt.parentNode.removeChild(elt)
308  elt = None
309  else:
310  previous = elt
311 
312  elt = next_element(previous)
313  return macros
314 
315 
316 # Returns a Table of the properties
318  table = Table()
319 
320  previous = doc.documentElement
321  elt = next_element(previous)
322  while elt:
323  if elt.tagName == 'property' or elt.tagName == 'xacro:property':
324  name = elt.getAttribute('name')
325  value = None
326 
327  if elt.hasAttribute('value'):
328  value = elt.getAttribute('value')
329  else:
330  name = '**' + name
331  value = elt # debug
332 
333  bad = string.whitespace + "${}"
334  has_bad = False
335  for b in bad:
336  if b in name:
337  has_bad = True
338  break
339 
340  if has_bad:
341  sys.stderr.write('Property names may not have whitespace, ' +
342  '"{", "}", or "$" : "' + name + '"')
343  else:
344  table[name] = value
345 
346  elt.parentNode.removeChild(elt)
347  elt = None
348  else:
349  previous = elt
350 
351  elt = next_element(previous)
352  return table
353 
354 
355 def eat_ignore(lex):
356  while lex.peek() and lex.peek()[0] == lex.IGNORE:
357  lex.next()
358 
359 
360 def eval_lit(lex, symbols):
361  eat_ignore(lex)
362  if lex.peek()[0] == lex.NUMBER:
363  return float(lex.next()[1])
364  if lex.peek()[0] == lex.SYMBOL:
365  try:
366  key = lex.next()[1]
367  value = symbols[key]
368  except KeyError as ex:
369  raise XacroException("Property wasn't defined: %s" % str(ex))
370  if not (isnumber(value) or isinstance(value, _basestr)):
371  if value is None:
372  raise XacroException("Property %s recursively used" % key)
373  raise XacroException("WTF2")
374  try:
375  return int(value)
376  except:
377  try:
378  return float(value)
379  except:
380  # prevent infinite recursion
381  symbols[key] = None
382  result = eval_text(value, symbols)
383  # restore old entry
384  symbols[key] = value
385  return result
386  raise XacroException("Bad literal")
387 
388 
389 def eval_factor(lex, symbols):
390  eat_ignore(lex)
391 
392  neg = 1
393  if lex.peek()[1] == '-':
394  lex.next()
395  neg = -1
396 
397  if lex.peek()[0] in [lex.NUMBER, lex.SYMBOL]:
398  return neg * eval_lit(lex, symbols)
399  if lex.peek()[0] == lex.LPAREN:
400  lex.next()
401  eat_ignore(lex)
402  result = eval_expr(lex, symbols)
403  eat_ignore(lex)
404  if lex.next()[0] != lex.RPAREN:
405  raise XacroException("Unmatched left paren")
406  eat_ignore(lex)
407  return neg * result
408 
409  raise XacroException("Misplaced operator")
410 
411 
412 def eval_term(lex, symbols):
413  eat_ignore(lex)
414 
415  result = 0
416  if lex.peek()[0] in [lex.NUMBER, lex.SYMBOL, lex.LPAREN] \
417  or lex.peek()[1] == '-':
418  result = eval_factor(lex, symbols)
419 
420  eat_ignore(lex)
421  while lex.peek() and lex.peek()[1] in ['*', '/']:
422  op = lex.next()[1]
423  n = eval_factor(lex, symbols)
424 
425  if op == '*':
426  result = float(result) * float(n)
427  elif op == '/':
428  result = float(result) / float(n)
429  else:
430  raise XacroException("WTF")
431  eat_ignore(lex)
432  return result
433 
434 
435 def eval_expr(lex, symbols):
436  eat_ignore(lex)
437 
438  op = None
439  if lex.peek()[0] == lex.OP:
440  op = lex.next()[1]
441  if not op in ['+', '-']:
442  raise XacroException("Invalid operation. Must be '+' or '-'")
443 
444  result = eval_term(lex, symbols)
445  if op == '-':
446  result = -float(result)
447 
448  eat_ignore(lex)
449  while lex.peek() and lex.peek()[1] in ['+', '-']:
450  op = lex.next()[1]
451  n = eval_term(lex, symbols)
452 
453  if op == '+':
454  result = float(result) + float(n)
455  if op == '-':
456  result = float(result) - float(n)
457  eat_ignore(lex)
458  return result
459 
460 
461 def eval_text(text, symbols):
462  def handle_expr(s):
463  lex = QuickLexer(IGNORE=r"\s+",
464  NUMBER=r"(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?",
465  SYMBOL=r"[a-zA-Z_]\w*",
466  OP=r"[\+\-\*/^]",
467  LPAREN=r"\(",
468  RPAREN=r"\)")
469  lex.lex(s)
470  return eval_expr(lex, symbols)
471 
472  def handle_extension(s):
473  return eval_extension("$(%s)" % s)
474 
475  results = []
476  lex = QuickLexer(DOLLAR_DOLLAR_BRACE=r"\$\$+\{",
477  EXPR=r"\$\{[^\}]*\}",
478  EXTENSION=r"\$\([^\)]*\)",
479  TEXT=r"([^\$]|\$[^{(]|\$$)+")
480  lex.lex(text)
481  while lex.peek():
482  if lex.peek()[0] == lex.EXPR:
483  results.append(handle_expr(lex.next()[1][2:-1]))
484  elif lex.peek()[0] == lex.EXTENSION:
485  results.append(handle_extension(lex.next()[1][2:-1]))
486  elif lex.peek()[0] == lex.TEXT:
487  results.append(lex.next()[1])
488  elif lex.peek()[0] == lex.DOLLAR_DOLLAR_BRACE:
489  results.append(lex.next()[1][1:])
490  return ''.join(map(str, results))
491 
492 
493 # Expands macros, replaces properties, and evaluates expressions
494 def eval_all(root, macros, symbols):
495  # Evaluates the attributes for the root node
496  for at in root.attributes.items():
497  result = eval_text(at[1], symbols)
498  root.setAttribute(at[0], result)
499 
500  previous = root
501  node = next_node(previous)
502  while node:
503  if node.nodeType == xml.dom.Node.ELEMENT_NODE:
504  if node.tagName in macros:
505  body = macros[node.tagName].cloneNode(deep=True)
506  params = body.getAttribute('params').split()
507 
508  # Parse default values for any parameters
509  defaultmap = {}
510  for param in params[:]:
511  splitParam = param.split(':=')
512 
513  if len(splitParam) == 2:
514  defaultmap[splitParam[0]] = splitParam[1]
515  params.remove(param)
516  params.append(splitParam[0])
517 
518  elif len(splitParam) != 1:
519  raise XacroException("Invalid parameter definition")
520 
521  # Expands the macro
522  scoped = Table(symbols)
523  for name, value in node.attributes.items():
524  if not name in params:
525  raise XacroException("Invalid parameter \"%s\" while expanding macro \"%s\"" %
526  (str(name), str(node.tagName)))
527  params.remove(name)
528  scoped[name] = eval_text(value, symbols)
529 
530  # Pulls out the block arguments, in order
531  cloned = node.cloneNode(deep=True)
532  eval_all(cloned, macros, symbols)
533  block = cloned.firstChild
534  for param in params[:]:
535  if param[0] == '*':
536  while block and block.nodeType != xml.dom.Node.ELEMENT_NODE:
537  block = block.nextSibling
538  if not block:
539  raise XacroException("Not enough blocks while evaluating macro %s" % str(node.tagName))
540  params.remove(param)
541  scoped[param] = block
542  block = block.nextSibling
543 
544  # Try to load defaults for any remaining non-block parameters
545  for param in params[:]:
546  if param[0] != '*' and param in defaultmap:
547  scoped[param] = defaultmap[param]
548  params.remove(param)
549 
550  if params:
551  raise XacroException("Parameters [%s] were not set for macro %s" %
552  (",".join(params), str(node.tagName)))
553  eval_all(body, macros, scoped)
554 
555  # Replaces the macro node with the expansion
556  for e in list(child_nodes(body)): # Ew
557  node.parentNode.insertBefore(e, node)
558  node.parentNode.removeChild(node)
559 
560  node = None
561  elif node.tagName == 'xacro:arg':
562  name = node.getAttribute('name')
563  if not name:
564  raise XacroException("Argument name missing")
565  default = node.getAttribute('default')
566  if default and name not in substitution_args_context['arg']:
567  substitution_args_context['arg'][name] = default
568 
569  node.parentNode.removeChild(node)
570  node = None
571 
572  elif node.tagName == 'insert_block' or node.tagName == 'xacro:insert_block':
573  name = node.getAttribute('name')
574 
575  if ("**" + name) in symbols:
576  # Multi-block
577  block = symbols['**' + name]
578 
579  for e in list(child_nodes(block)):
580  node.parentNode.insertBefore(e.cloneNode(deep=True), node)
581  node.parentNode.removeChild(node)
582  elif ("*" + name) in symbols:
583  # Single block
584  block = symbols['*' + name]
585 
586  node.parentNode.insertBefore(block.cloneNode(deep=True), node)
587  node.parentNode.removeChild(node)
588  else:
589  raise XacroException("Block \"%s\" was never declared" % name)
590 
591  node = None
592  elif node.tagName in ['if', 'xacro:if', 'unless', 'xacro:unless']:
593  value = eval_text(node.getAttribute('value'), symbols)
594  try:
595  if value == 'true': keep = True
596  elif value == 'false': keep = False
597  else: keep = float(value)
598  except ValueError:
599  raise XacroException("Xacro conditional evaluated to \"%s\". Acceptable evaluations are one of [\"1\",\"true\",\"0\",\"false\"]" % value)
600  if node.tagName in ['unless', 'xacro:unless']: keep = not keep
601  if keep:
602  for e in list(child_nodes(node)):
603  node.parentNode.insertBefore(e.cloneNode(deep=True), node)
604 
605  node.parentNode.removeChild(node)
606  else:
607  # Evals the attributes
608  for at in node.attributes.items():
609  result = eval_text(at[1], symbols)
610  node.setAttribute(at[0], result)
611  previous = node
612  elif node.nodeType == xml.dom.Node.TEXT_NODE:
613  node.data = eval_text(node.data, symbols)
614  previous = node
615  else:
616  previous = node
617 
618  node = next_node(previous)
619  return macros
620 
621 
622 # Expands everything except includes
624  macros = grab_macros(doc)
625  symbols = grab_properties(doc)
626  eval_all(doc.documentElement, macros, symbols)
627 
628 
629 def print_usage(exit_code=0):
630  print("Usage: %s [-o <output>] <input>" % 'xacro.py')
631  print(" %s --deps Prints dependencies" % 'xacro.py')
632  print(" %s --includes Only evalutes includes" % 'xacro.py')
633  sys.exit(exit_code)
634 
635 
637  substitution_args_context['arg'] = context
638 
639 def open_output(output_filename):
640  if output_filename is None:
641  return sys.stdout
642  else:
643  return open(output_filename, 'w')
644 
645 def main():
646  try:
647  opts, args = getopt.gnu_getopt(sys.argv[1:], "ho:", ['deps', 'includes'])
648  except getopt.GetoptError as err:
649  print(str(err))
650  print_usage(2)
651 
652  just_deps = False
653  just_includes = False
654 
655  output_filename = None
656  for o, a in opts:
657  if o == '-h':
658  print_usage(0)
659  elif o == '-o':
660  output_filename = a
661  elif o == '--deps':
662  just_deps = True
663  elif o == '--includes':
664  just_includes = True
665 
666  if len(args) < 1:
667  print("No input given")
668  print_usage(2)
669 
670  # Process substitution args
672 
673  f = open(args[0])
674  doc = None
675  try:
676  doc = parse(f)
677  except xml.parsers.expat.ExpatError:
678  sys.stderr.write("Expat parsing error. Check that:\n")
679  sys.stderr.write(" - Your XML is correctly formed\n")
680  sys.stderr.write(" - You have the xacro xmlns declaration: " +
681  "xmlns:xacro=\"http://www.ros.org/wiki/xacro\"\n")
682  sys.stderr.write("\n")
683  raise
684  finally:
685  f.close()
686 
687  process_includes(doc, os.path.dirname(args[0]))
688  if just_deps:
689  for inc in all_includes:
690  sys.stdout.write(inc + " ")
691  sys.stdout.write("\n")
692  elif just_includes:
693  doc.writexml(open_output(output_filename))
694  print()
695  else:
697  banner = [xml.dom.minidom.Comment(c) for c in
698  [" %s " % ('=' * 83),
699  " | This document was autogenerated by xacro from %-30s | " % args[0],
700  " | EDITING THIS FILE BY HAND IS NOT RECOMMENDED %-30s | " % "",
701  " %s " % ('=' * 83)]]
702  first = doc.firstChild
703  for comment in banner:
704  doc.insertBefore(comment, first)
705 
706  open_output(output_filename).write(doc.toprettyxml(indent=' '))
707  print()
708 
709 main()
def process_includes(elt, macros=None, symbols=None)
def grab_properties(elt, table)
def isnumber(x)
Definition: xacro.py:61
def eval_text(text, symbols)
Definition: xacro.py:461
def print_usage(exit_code=0)
Definition: xacro.py:629
def first_child_element(elt)
Definition: xacro.py:157
def eval_all(root, macros, symbols)
Definition: xacro.py:494
def open_output(output_filename)
Definition: xacro.py:639
ssize_t len
def eval_factor(lex, symbols)
Definition: xacro.py:389
def set_substitution_args_context(context={})
Definition: xacro.py:636
def next_node(node)
Definition: xacro.py:189
def fixed_writexml(self, writer, indent="", addindent="", newl="")
Definition: xacro.py:71
def eval_expr(lex, symbols)
Definition: xacro.py:435
def resolve_args(arg_str, context=None, resolve_anon=True)
def eval_lit(lex, symbols)
Definition: xacro.py:360
def next_sibling_element(elt)
Definition: xacro.py:166
def child_nodes(elt)
Definition: xacro.py:199
def eval_self_contained(doc)
Definition: xacro.py:623
def next_element(elt)
Definition: xacro.py:176
def grab_macros(elt, macros)
def eat_ignore(lex)
Definition: xacro.py:355
def load_mappings(argv)
Definition: names.py:65
def main()
Definition: xacro.py:645
def eval_term(lex, symbols)
Definition: xacro.py:412
def eval_extension(s)


rotors_gazebo
Author(s): Fadri Furrer, Michael Burri, Mina Kamel, Janosch Nikolic, Markus Achtelik
autogenerated on Mon Feb 28 2022 23:39:12