33 from __future__
import print_function, division
42 from copy
import deepcopy
43 from .color
import warning, error, message
44 from .xmlutils
import *
45 from .cli
import process_args
50 encoding = {
'encoding':
'utf-8' }
57 substitution_args_context = {}
65 Push a new filename to the filestack. 66 Instead of directly modifying filestack, a deep-copy is created and modified, 67 while the old filestack is returned. 68 This allows to store the filestack that was active when a macro or property is defined 72 filestack = deepcopy(filestack)
73 filestack.append(filename)
83 Prepend the dirname of the currently processed file 84 if filename_spec is not yet absolute 86 if not os.path.isabs(filename_spec):
87 parent_filename = filestack[-1]
88 basedir = os.path.dirname(parent_filename)
if parent_filename
else '.' 89 return os.path.join(basedir, filename_spec)
97 raise XacroException(
"yaml support not available; install python-yaml")
108 all_includes.append(filename)
115 global_symbols = {
'__builtins__': {k: __builtins__[k]
for k
in [
'list',
'dict',
'map',
'str',
'float',
'int',
'True',
'False',
'min',
'max',
'round']}}
117 global_symbols.update(math.__dict__)
119 global_symbols.update(dict(load_yaml=load_yaml))
124 XacroException allows to wrap another exception (exc) and to augment 125 its error message: prefixing with msg and suffixing with suffix. 126 str(e) finally prints: msg str(exc) suffix 128 def __init__(self, msg=None, suffix=None, exc=None, macro=None):
129 super(XacroException, self).
__init__(msg)
132 self.
macros = []
if macro
is None else [macro]
136 return ' '.join([
unicode(e)
for e
in items
if e
not in [
'',
'None']])
147 warning(
"deprecated: xacro tags should be prepended with 'xacro' xml namespace.")
148 message(
"""Use the following script to fix incorrect usage: 149 find . -iname "*.xacro" | xargs sed -i 's#<\([/]\\?\)\(if\|unless\|include\|arg\|property\|macro\|insert_block\)#<\\1xacro:\\2#g'""")
150 print_location(filestack)
151 print(file=sys.stderr)
155 allow_non_prefixed_tags =
True 160 Check whether tagName starts with xacro prefix. If not, issue a warning. 162 :return: True if tagName is accepted as xacro tag 163 False if tagName doesn't start with xacro prefix, but the prefix is required 165 if tag_name.startswith(
'xacro:'):
168 if allow_non_prefixed_tags:
170 return allow_non_prefixed_tags
185 from roslaunch
import substitution_args
186 from rospkg.common
import ResourceNotFound
187 return substitution_args.resolve_args(s, context=substitution_args_context, resolve_anon=
False)
188 except ImportError
as e:
190 except substitution_args.ArgException
as e:
192 except ResourceNotFound
as e:
212 if isinstance(value, _basestr):
214 if len(value) >= 2
and value[0] ==
"'" and value[-1] ==
"'":
218 for f
in [int, float,
lambda x: get_boolean_value(x,
None)]:
231 self.recursive.append(key)
233 self.unevaluated.remove(key)
234 self.recursive.remove(key)
237 value = self.
table[key]
238 if (verbosity > 2
and self.
parent is None)
or verbosity > 3:
239 print(
"{indent}use {key}: {value} ({loc})".format(
240 indent=self.
depth*
' ', key=key, value=value, loc=filestack[-1]), file=sys.stderr)
246 if key
in self.
table:
254 if do_check_order
and key
in self.
used and key
not in self.
redefined:
257 if key
in global_symbols:
258 warning(
"redefining global property: %s" % key)
259 print_location(filestack)
262 self.
table[key] = value
263 if unevaluated
and isinstance(value, _basestr):
265 self.unevaluated.add(key)
268 self.unevaluated.remove(key)
269 if (verbosity > 2
and self.
parent is None)
or verbosity > 3:
270 print(
"{indent}set {key}: {value} ({loc})".format(
271 indent=self.
depth*
' ', key=key, value=value, loc=filestack[-1]), file=sys.stderr)
274 self.
_setitem(key, value, unevaluated=
True)
278 key
in self.
table or \
283 if isinstance(self.
parent, Table):
297 return self.__getitem__(item)
301 super(PropertyNameSpace, self).
__init__(parent)
305 super(MacroNameSpace, self).
__init__(*args, **kwargs)
313 self.__dict__.update(other.__dict__)
316 for k, v
in kwargs.items():
317 self.__setattr__(k, len(self.
res))
318 self.res.append(re.compile(v))
333 for i
in range(len(self.
res)):
334 m = self.
res[i].match(self.
str)
336 self.
top = (i, m.group(0))
337 self.
str = self.
str[m.end():]
344 include_no_matches_msg =
"""Include tag's filename spec \"{}\" matched no files.""" 350 if elt.tagName
not in [
'xacro:include',
'include']:
354 if elt.tagName ==
'include':
359 if elt.childNodes
and not (len(elt.childNodes) == 1
and 360 elt.childNodes[0].nodeType == elt.TEXT_NODE):
372 except XacroException
as e:
373 if e.exc
and isinstance(e.exc, NameError)
and symbols
is None:
374 raise XacroException(
'variable filename is supported with --inorder option only')
378 if re.search(
'[*[?]+', filename_spec):
380 filenames = sorted(glob.glob(filename_spec))
381 if len(filenames) == 0:
382 warning(include_no_matches_msg.format(filename_spec))
385 filenames = [filename_spec]
387 for filename
in filenames:
389 all_includes.append(filename)
394 """import all namespace declarations into parent""" 395 for name, value
in attributes.items():
396 if name.startswith(
'xmlns:'):
397 oldAttr = parent.getAttributeNode(name)
398 if oldAttr
and oldAttr.value != value:
399 warning(
"inconsistent namespace redefinitions for {name}:" 400 "\n old: {old}\n new: {new} ({new_file})".format(
401 name=name, old=oldAttr.value, new=value,
402 new_file=filestack[-1]))
404 parent.setAttribute(name, value)
409 filename_spec, namespace_spec =
check_attrs(elt, [
'filename'], [
'ns'])
412 namespace_spec = eval_text(namespace_spec, symbols)
418 raise XacroException(
'namespaces are supported with --inorder option only')
423 include = parse(
None, filename).documentElement
426 func(include, macros, symbols)
427 included.append(include)
433 remove_previous_comments(elt)
453 Checks whether name is a valid property or macro identifier. 454 With python-based evaluation, we need to avoid name clashes with python keywords. 458 root = ast.parse(name)
460 if isinstance(root, ast.Module)
and \
461 len(root.body) == 1
and isinstance(root.body[0], ast.Expr)
and \
462 isinstance(root.body[0].value, ast.Name)
and root.body[0].value.id == name:
470 re_macro_arg = re.compile(
r'''\s*([^\s:=]+?):?=(\^\|?)?((?:(?:'[^']*')?[^\s'"]*?)*)(?:\s+|$)(.*)''')
474 parse the first param spec from a macro parameter string s 475 accepting the following syntax: <param>[:=|=][^|]<default> 476 :param s: param spec string 477 :return: param, (forward, default), rest-of-string 478 forward will be either param or None (depending on whether ^ was specified) 479 default will be the default string or None 480 If there is no default spec at all, the middle pair will be replaced by None 482 m = re_macro_arg.match(s)
485 param, forward, default, rest = m.groups()
486 if not default: default =
None 487 return param, (param
if forward
else None, default), rest
490 result = s.lstrip().split(
None, 1)
491 return result[0],
None, result[1]
if len(result) > 1
else '' 495 assert(elt.tagName
in [
'macro',
'xacro:macro'])
496 remove_previous_comments(elt)
498 name, params =
check_attrs(elt, [
'name'], [
'params'])
500 warning(
"deprecated use of macro name 'call'; xacro:call became a new keyword")
501 if name.find(
'.') != -1:
502 warning(
"macro names must not contain '.': %s" % name)
504 if not name.startswith(
'xacro:'):
505 name =
'xacro:' + name
508 macro = macros.get(name,
Macro())
510 macro.history.append(filestack)
515 macro.defaultmap = {}
518 macro.params.append(param)
519 if value
is not None:
520 macro.defaultmap[param] = value
531 if elt.tagName
in [
'macro',
'xacro:macro'] \
541 assert(elt.tagName
in [
'property',
'xacro:property'])
542 remove_previous_comments(elt)
544 name, value, default, scope =
check_attrs(elt, [
'name'], [
'value',
'default',
'scope'])
546 raise XacroException(
'Property names must be valid python identifiers: ' + name)
547 if value
is not None and default
is not None:
548 raise XacroException(
'Property cannot define both a default and a value: ' + name)
550 if default
is not None:
551 if scope
is not None:
552 warning(
"%s: default property value can only be defined on local scope" % name)
553 if name
not in table:
565 if scope
and scope ==
'global':
566 target_table = table.root()
568 elif scope
and scope ==
'parent':
570 target_table = table.parent
573 warning(
"%s: no parent scope at global scope " % name)
579 if not unevaluated
and isinstance(value, _basestr):
580 value = eval_text(value, table)
582 target_table._setitem(name, value, unevaluated=unevaluated)
590 if elt.tagName
in [
'property',
'xacro:property'] \
592 if "default" in elt.attributes.keys():
593 raise XacroException(
'default property value supported with --inorder option only')
601 LEXER =
QuickLexer(DOLLAR_DOLLAR_BRACE=
r"^\$\$+(\{|\()",
602 EXPR=
r"^\$\{[^\}]*\}",
603 EXTENSION=
r"^\$\([^\)]*\)",
604 TEXT=
r"[^$]+|\$[^{($]+|\$$")
608 def eval_text(text, symbols):
611 return eval(eval_text(s, symbols), global_symbols, symbols)
612 except Exception
as e:
615 suffix=os.linesep +
"when evaluating expression '%s'" % s)
617 def handle_extension(s):
626 results.append(handle_expr(lex.next()[1][2:-1]))
627 elif id == lex.EXTENSION:
628 results.append(handle_extension(lex.next()[1][2:-1]))
630 results.append(lex.next()[1])
631 elif id == lex.DOLLAR_DOLLAR_BRACE:
632 results.append(lex.next()[1][1:])
634 if len(results) == 1:
638 return ''.join(map(unicode, results))
641 def eval_default_arg(forward_variable, default, symbols, macro):
642 if forward_variable
is None:
643 return eval_text(default, symbols)
645 return symbols[forward_variable]
647 if default
is not None:
648 return eval_text(default, symbols)
650 raise XacroException(
"Undefined property to forward: " + forward_variable, macro=macro)
653 def handle_dynamic_macro_call(node, macros, symbols):
656 raise XacroException(
"xacro:call is missing the 'macro' attribute")
657 name =
unicode(eval_text(name, symbols))
660 node.removeAttribute(
'macro')
663 result = handle_macro_call(node, macros, symbols)
665 raise XacroException(
"unknown macro name '%s' in xacro:call" % name)
668 def resolve_macro(fullname, macros):
670 namespaces = fullname.split(
'.')
671 name = namespaces.pop(-1)
673 if namespaces
and namespaces[0].startswith(
'xacro:'):
674 namespaces[0] = namespaces[0].replace(
'xacro:',
'')
675 name =
'xacro:' + name
677 def _resolve(namespaces, name, macros):
679 for ns
in namespaces:
685 if allow_non_prefixed_tags
and not name.startswith(
'xacro:'):
686 return _resolve([],
'xacro:' + name, macros)
689 m = _resolve([], fullname, macros)
691 elif namespaces:
return _resolve(namespaces, name, macros)\
694 def handle_macro_call(node, macros, symbols):
696 m = resolve_macro(node.tagName, macros)
697 body = m.body.cloneNode(deep=
True)
699 except (KeyError, TypeError, AttributeError):
701 if node.tagName ==
'xacro:call':
702 return handle_dynamic_macro_call(node, macros, symbols)
706 scoped =
Table(symbols)
708 for name, value
in node.attributes.items():
709 if name
not in params:
712 scoped._setitem(name, eval_text(value, symbols), unevaluated=
False)
713 node.setAttribute(name,
"")
716 eval_all(node, macros, symbols)
720 for param
in params[:]:
725 scoped[param] = block
728 if block
is not None:
729 raise XacroException(
"Unused block \"%s\"" % block.tagName, macro=m)
732 for param
in params[:]:
734 if param[0] ==
'*':
continue 737 name, default = m.defaultmap.get(param, (
None,
None))
738 if name
is not None or default
is not None:
739 scoped._setitem(param, eval_default_arg(name, default, symbols, m), unevaluated=
False)
743 raise XacroException(
"Undefined parameters [%s]" %
",".join(params), macro=m)
746 eval_all(body, macros, scoped)
747 except Exception
as e:
749 if hasattr(e,
'macros'):
756 remove_previous_comments(node)
761 def get_boolean_value(value, condition):
763 Return a boolean value that corresponds to the given Xacro condition value. 764 Values "true", "1" and "1.0" are supposed to be True. 765 Values "false", "0" and "0.0" are supposed to be False. 766 All other values raise an exception. 768 :param value: The value to be evaluated. The value has to already be evaluated by Xacro. 769 :param condition: The original condition text in the XML. 770 :return: The corresponding boolean value, or a Python expression that, converted to boolean, corresponds to it. 771 :raises ValueError: If the condition value is incorrect. 774 if isinstance(value, _basestr):
775 if value ==
'true' or value ==
'True':
return True 776 elif value ==
'false' or value ==
'False':
return False 777 else:
return bool(int(value))
781 raise XacroException(
"Xacro conditional \"%s\" evaluated to \"%s\", " 782 "which is not a boolean expression." % (condition, value))
785 _empty_text_node = xml.dom.minidom.getDOMImplementation().createDocument(
None,
"dummy",
None).createTextNode(
'\n\n')
786 def remove_previous_comments(node):
787 """remove consecutive comments in front of the xacro-specific node""" 788 next = node.nextSibling
789 previous = node.previousSibling
791 if previous.nodeType == xml.dom.Node.TEXT_NODE
and \
792 previous.data.isspace()
and previous.data.count(
'\n') <= 1:
793 previous = previous.previousSibling
795 if previous
and previous.nodeType == xml.dom.Node.COMMENT_NODE:
797 previous = previous.previousSibling
798 node.parentNode.removeChild(comment)
802 if next
and _empty_text_node != next: node.parentNode.insertBefore(_empty_text_node, next)
806 def eval_all(node, macros, symbols):
807 """Recursively evaluate node, expanding macros, replacing properties, and evaluating expressions""" 809 for name, value
in node.attributes.items():
810 result =
unicode(eval_text(value, symbols))
811 node.setAttribute(name, result)
813 node = node.firstChild
815 next = node.nextSibling
816 if node.nodeType == xml.dom.Node.ELEMENT_NODE:
817 if node.tagName
in [
'insert_block',
'xacro:insert_block'] \
821 if (
"**" + name)
in symbols:
823 block = symbols[
'**' + name]
825 elif (
"*" + name)
in symbols:
827 block = symbols[
'*' + name]
833 block = block.cloneNode(deep=
True)
835 eval_all(block, macros, symbols)
841 elif node.tagName
in [
'property',
'xacro:property'] \
845 elif node.tagName
in [
'macro',
'xacro:macro'] \
849 elif node.tagName
in [
'arg',
'xacro:arg'] \
851 name, default =
check_attrs(node, [
'name',
'default'], [])
852 if name
not in substitution_args_context[
'arg']:
853 substitution_args_context[
'arg'][name] = eval_text(default, symbols)
855 remove_previous_comments(node)
858 elif node.tagName ==
'xacro:element':
859 name = eval_text(*
reqd_attrs(node, [
'xacro:name']), symbols=symbols)
863 node.removeAttribute(
'xacro:name')
864 node.nodeName = node.tagName = name
867 elif node.tagName ==
'xacro:attribute':
868 name, value = [eval_text(a, symbols)
for a
in reqd_attrs(node, [
'name',
'value'])]
872 node.parentNode.setAttribute(name, value)
875 elif node.tagName
in [
'if',
'xacro:if',
'unless',
'xacro:unless'] \
877 remove_previous_comments(node)
879 keep = get_boolean_value(eval_text(cond, symbols), cond)
880 if node.tagName
in [
'unless',
'xacro:unless']:
884 eval_all(node, macros, symbols)
889 elif handle_macro_call(node, macros, symbols):
894 if node.tagName.startswith(
"xacro:"):
897 eval_all(node, macros, symbols)
900 elif node.nodeType == xml.dom.Node.TEXT_NODE:
901 node.data =
unicode(eval_text(node.data, symbols))
906 def parse(inp, filename=None):
908 Parse input or filename into a DOM tree. 909 If inp is None, open filename and load from there. 910 Otherwise, parse inp, either as string or file object. 911 If inp is already a DOM tree, this function is a noop. 912 :return:xml.dom.minidom.Document 913 :raise: xml.parsers.expat.ExpatError 918 inp = f = open(filename)
925 if isinstance(inp, _basestr):
926 return xml.dom.minidom.parseString(inp)
927 elif hasattr(inp,
'read'):
928 return xml.dom.minidom.parse(inp)
937 in_order=
False, just_deps=
False, just_includes=
False,
938 mappings=
None, xacro_ns=
True, **kwargs):
939 global verbosity, do_check_order
940 verbosity = kwargs.get(
'verbosity', verbosity)
941 do_check_order = kwargs.get(
'do_check_order', do_check_order)
944 if mappings
is not None:
945 substitution_args_context[
'arg'] = mappings
947 global allow_non_prefixed_tags
948 allow_non_prefixed_tags = xacro_ns
955 if (just_deps
or just_includes)
and not in_order:
967 eval_all(doc.documentElement, macros, symbols)
970 substitution_args_context[
'arg'] = {}
972 if do_check_order
and symbols.redefined:
973 warning(
"Document is incompatible to --inorder processing.")
974 warning(
"The following properties were redefined after usage:")
975 for k, v
in symbols.redefined.items():
976 message(k,
"redefined in", v, color=
'yellow')
979 def open_output(output_filename):
980 if output_filename
is None:
983 dir_name = os.path.dirname(output_filename)
986 os.makedirs(dir_name)
993 return open(output_filename,
'w')
998 def print_location(filestack, err=None, file=sys.stderr):
999 macros = getattr(err,
'macros', [])
if err
else []
1000 msg =
'when instantiating macro:' 1002 name = m.body.getAttribute(
'name')
1003 location =
'(%s)' % m.history[-1][-1]
1004 print(msg, name, location, file=file)
1005 msg =
'instantiated from:' 1007 msg =
'in file:' if macros
else 'when processing file:' 1008 for f
in reversed(filestack):
1009 if f
is None: f =
'string' 1010 print(msg, f, file=file)
1011 msg =
'included from:' 1013 def process_file(input_file_name, **kwargs):
1014 """main processing pipeline""" 1018 doc = parse(
None, input_file_name)
1020 process_doc(doc, **kwargs)
1023 banner = [xml.dom.minidom.Comment(c)
for c
in 1024 [
" %s " % (
'=' * 83),
1025 " | This document was autogenerated by xacro from %-30s | " % input_file_name,
1026 " | EDITING THIS FILE BY HAND IS NOT RECOMMENDED %-30s | " %
"",
1027 " %s " % (
'=' * 83)]]
1028 first = doc.firstChild
1029 for comment
in banner:
1030 doc.insertBefore(comment, first)
1036 if opts.in_order ==
False and not opts.just_includes:
1037 warning(
"xacro: Traditional processing is deprecated. Switch to --inorder processing!")
1038 message(
"To check for compatibility of your document, use option --check-order.", color=
'yellow')
1039 message(
"For more infos, see http://wiki.ros.org/xacro#Processing_Order", color=
'yellow')
1043 doc = process_file(input_file_name, **vars(opts))
1045 out = open_output(opts.output)
1048 except xml.parsers.expat.ExpatError
as e:
1049 error(
"XML parsing error: %s" %
unicode(e), alt_text=
None)
1051 print_location(filestack, e)
1052 print(file=sys.stderr)
1053 print(
"Check that:", file=sys.stderr)
1054 print(
" - Your XML is well-formed", file=sys.stderr)
1055 print(
" - You have the xacro xmlns declaration:",
1056 "xmlns:xacro=\"http://www.ros.org/wiki/xacro\"", file=sys.stderr)
1059 except Exception
as e:
1061 if not msg: msg = repr(e)
1064 print_location(filestack, e)
1066 print(file=sys.stderr)
1073 out.write(
" ".join(set(all_includes)))
1078 out.write(doc.toprettyxml(indent=
' ', **encoding))
def __init__(self, args, kwargs)
def _setitem(self, key, value, unevaluated)
def check_deprecated_tag(tag_name)
def process_includes(elt, macros=None, symbols=None)
def grab_properties(elt, table)
def __init__(self, msg=None, suffix=None, exc=None, macro=None)
def __setitem__(self, key, value)
def next_sibling_element(node)
def abs_filename_spec(filename_spec)
def process_include(elt, macros, symbols, func)
def restore_filestack(oldstack)
def grab_property(elt, table)
def __init__(self, args, kwargs)
def first_child_element(elt)
def import_xml_namespaces(parent, attributes)
def grab_macro(elt, macros)
def reqd_attrs(tag, attrs)
def message(msg, args, kwargs)
def __getattr__(self, item)
def replace_node(node, by, content_only=False)
def __contains__(self, key)
def warning(args, kwargs)
def process_args(argv, require_input=True)
def __init__(self, parent=None)
def grab_macros(elt, macros)
def check_attrs(tag, required, optional)
def __getitem__(self, key)
def __init__(self, parent=None)
def deprecated_tag(_issued=[False])
def get_include_files(filename_spec, symbols)